Отображение чисел с DOS
Мне было поручено написать программу, которая отображает линейный адрес PSP моей программы. Я написал следующее:
ORG 256
mov dx,Msg
mov ah,09h ;DOS.WriteStringToStandardOutput
int 21h
mov ax,ds
mov dx,16
mul dx ; -> Linear address is now in DX:AX
???
mov ax,4C00h ;DOS.TerminateWithExitCode
int 21h
; ------------------------------
Msg: db 'PSP is at linear address $'
Я искал API DOS (используя список прерываний Ральфа Брауна) и не нашел ни одной функции для вывода числа! Я пропустил это, и что я могу сделать?
Я хочу отобразить номер в DX:AX
в десятичном.
1 ответ
Это правда, что DOS не предлагает нам функцию для вывода числа напрямую.
Вы должны будете сначала преобразовать число самостоятельно, а затем DOS отобразить его, используя одну из функций вывода текста.
Отображение 16-разрядного числа без знака в AX
При решении проблемы преобразования числа это помогает увидеть, как цифры, составляющие число, связаны друг с другом.
Давайте рассмотрим число 65535 и его разложение:
(6 * 10000) + (5 * 1000) + (5 * 100) + (3 * 10) + (5 * 1)
Метод 1: деление по убыванию степени 10
Обработка числа, идущего слева направо, удобна, поскольку позволяет отображать отдельную цифру, как только мы ее извлечем.
Разделив число (65535) на 10000, мы получим однозначное отношение (6), которое мы можем сразу вывести как символ. Мы также получаем остаток (5535), который станет дивидендом на следующем шаге.
Разделив остаток от предыдущего шага (5535) на 1000, мы получим однозначный коэффициент (5), который мы можем сразу вывести как символ. Мы также получаем остаток (535), который станет дивидендом на следующем шаге.
Разделив остаток от предыдущего шага (535) на 100, мы получим однозначный коэффициент (5), который мы можем сразу вывести как символ. Мы также получаем остаток (35), который станет дивидендом на следующем шаге.
Разделив остаток от предыдущего шага (35) на 10, мы получим однозначный коэффициент (3), который мы можем сразу вывести как символ. Мы также получаем остаток (5), который станет дивидендом на следующем шаге.
Разделив остаток от предыдущего шага (5) на 1, мы получим однозначный коэффициент (5), который мы можем сразу вывести как символ. Здесь остаток всегда будет 0. (Чтобы избежать этого глупого деления на 1, требуется дополнительный код)
mov bx,.List
.a: xor dx,dx
div word ptr [bx] ; -> AX=[0,9] is Quotient, Remainder DX
xchg ax,dx
add dl,"0" ;Turn into character [0,9] -> ["0","9"]
push ax ;(1)
mov ah,02h ;DOS.DisplayCharacter
int 21h ; -> AL
pop ax ;(1) AX is next dividend
add bx,2
cmp bx,.List+10
jb .a
...
.List:
dw 10000,1000,100,10,1
Хотя этот метод, конечно, даст правильный результат, у него есть несколько недостатков:
Рассмотрим меньшее число 255 и его разложение:
(0 * 10000) + (0 * 1000) + (2 * 100) + (5 * 10) + (5 * 1)
Если бы мы использовали тот же 5-шаговый процесс, мы получили бы "00255". Эти 2 ведущих нуля нежелательны, и мы должны были бы добавить дополнительные инструкции, чтобы избавиться от них.
Делитель меняется с каждым шагом. Нам пришлось хранить список разделителей в памяти. Динамическое вычисление этих делителей возможно, но вводит много дополнительных делений.
Если бы мы хотели применить этот метод для отображения еще больших чисел, скажем, 32-битных, и мы захотим в конечном итоге, то связанные с этим разделы станут действительно проблематичными.
Таким образом, метод 1 нецелесообразен, и поэтому он редко используется.
Способ 2: деление на постоянную 10
Обработка числа, идущего справа налево, кажется нелогичным, поскольку наша цель - сначала отобразить самую левую цифру. Но, как вы собираетесь выяснить, это прекрасно работает.
Разделив число (65535) на 10, мы получим частное (6553), которое станет дивидендом на следующем шаге. Мы также получаем остаток (5), который мы пока не можем вывести, и поэтому нам нужно будет где-то сохранить. Стек - это удобное место для этого.
Разделив частное от предыдущего шага (6553) на 10, мы получим частное (655), которое станет дивидендом на следующем шаге. Мы также получаем остаток (3), который мы пока не можем вывести, поэтому нам придется его где-то сохранить. Стек - это удобное место для этого.
Разделив частное от предыдущего шага (655) на 10, мы получим частное (65), которое станет дивидендом на следующем шаге. Мы также получаем остаток (5), который мы пока не можем вывести, поэтому нам придется его где-то сохранить. Стек - это удобное место для этого.
Разделив частное от предыдущего шага (65) на 10, мы получим частное (6), которое станет дивидендом на следующем шаге. Мы также получаем остаток (5), который мы пока не можем вывести, поэтому нам придется его где-то сохранить. Стек - это удобное место для этого.
Разделив частное от предыдущего шага (6) на 10, мы получим частное (0), которое сигнализирует о том, что это было последним делением. Мы также получаем остаток (6), который мы можем вывести сразу как символ, но воздержание от этого оказывается наиболее эффективным и, как и прежде, мы сохраним его в стеке.
На данный момент в стеке находятся наши 5 остатков, каждый из которых представляет собой однозначное число в диапазоне [0,9]. Так как стек является LIFO (Last In First Out), значение, которое мы будем POP
первая - первая цифра, которую мы хотим отобразить. Мы используем отдельный цикл с 5 POP
для отображения полного номера. Но на практике, поскольку мы хотим, чтобы эта подпрограмма могла также обрабатывать числа, содержащие менее 5 цифр, мы будем считать цифры по мере их поступления, а позже сделаем так много POP
"S.
mov bx,10 ;CONST
xor cx,cx ;Reset counter
.a: xor dx,dx ;Setup for division DX:AX / BX
div bx ; -> AX is Quotient, Remainder DX=[0,9]
push dx ;(1) Save remainder for now
inc cx ;One more digit
test ax,ax ;Is quotient zero?
jnz .a ;No, use as next dividend
.b: pop dx ;(1)
add dl,"0" ;Turn into character [0,9] -> ["0","9"]
mov ah,02h ;DOS.DisplayCharacter
int 21h ; -> AL
loop .b
Этот второй метод не имеет ни одного из недостатков первого метода:
- Поскольку мы останавливаемся, когда частное становится нулевым, никогда не возникает проблем с уродливыми ведущими нулями.
- Делитель исправлен. Это достаточно просто.
- Очень просто применить этот метод для отображения больших чисел, и это именно то, что будет дальше.
Отображение 32-разрядного числа без знака в DX:AX
На 8086 необходим каскад из 2 делений для деления 32-битного значения на DX:AX
на 10
1-е деление делит высокий дивиденд (с 0), что дает высокий коэффициент. 2-й дивизион делит низкий дивиденд (расширенный с остатком от 1-го дивизиона), давая низкий коэффициент. Это остаток от 2-го деления, который мы сохраняем в стеке.
Чтобы проверить, если меч в DX:AX
ноль, я OR
-эд обе половинки в скретч-регистре.
Вместо того чтобы считать цифры, требуя регистр, я решил поместить сторожа в стек. Поскольку этот страж получает значение (10), которое ни одна цифра не может иметь ([0,9]), он позволяет определить, когда цикл отображения должен остановиться.
Кроме этого этот фрагмент похож на метод 2 выше.
mov bx,10 ;CONST
push bx ;Sentinel
.a: mov cx,ax ;Temporarily store LowDividend in CX
mov ax,dx ;First divide the HighDividend
xor dx,dx ;Setup for division DX:AX / BX
div bx ; -> AX is HighQuotient, Remainder is re-used
xchg ax,cx ;Temporarily move it to CX restoring LowDividend
div bx ; -> AX is LowQuotient, Remainder DX=[0,9]
push dx ;(1) Save remainder for now
mov dx,cx ;Build true 32-bit quotient in DX:AX
or cx,ax ;Is the true 32-bit quotient zero?
jnz .a ;No, use as next dividend
pop dx ;(1a) First pop (Is digit for sure)
.b: add dl,"0" ;Turn into character [0,9] -> ["0","9"]
mov ah,02h ;DOS.DisplayCharacter
int 21h ; -> AL
pop dx ;(1b) All remaining pops
cmp dx,bx ;Was it the sentinel?
jb .b ;Not yet
Отображение 32-разрядного числа со знаком в DX:AX
Процедура выглядит следующим образом:
Сначала выясните, является ли число со знаком отрицательным, проверив бит знака.
Если это так, тогда отрицайте число и выводите символ "-", но будьте осторожны, чтобы не уничтожить число в DX:AX
в процессе.
Остальная часть фрагмента такая же, как и для номера без знака.
test dx,dx ;Sign bit is bit 15 of high word
jns .a ;It's a positive number
neg dx ;\
neg ax ; | Negate DX:AX
sbb dx,0 ;/
push ax dx ;(1)
mov dl,"-"
mov ah,02h ;DOS.DisplayCharacter
int 21h ; -> AL
pop dx ax ;(1)
.a: mov bx,10 ;CONST
push bx ;Sentinel
.b: mov cx,ax ;Temporarily store LowDividend in CX
mov ax,dx ;First divide the HighDividend
xor dx,dx ;Setup for division DX:AX / BX
div bx ; -> AX is HighQuotient, Remainder is re-used
xchg ax,cx ;Temporarily move it to CX restoring LowDividend
div bx ; -> AX is LowQuotient, Remainder DX=[0,9]
push dx ;(2) Save remainder for now
mov dx,cx ;Build true 32-bit quotient in DX:AX
or cx,ax ;Is the true 32-bit quotient zero?
jnz .b ;No, use as next dividend
pop dx ;(2a) First pop (Is digit for sure)
.c: add dl,"0" ;Turn into character [0,9] -> ["0","9"]
mov ah,02h ;DOS.DisplayCharacter
int 21h ; -> AL
pop dx ;(2b) All remaining pops
cmp dx,bx ;Was it the sentinel?
jb .c ;Not yet
Будут ли мне нужны отдельные процедуры для разных чисел?
В программе, где вам нужно отображать при необходимости AL
, AX
, или же DX:AX
Вы можете просто включить 32-битную версию и использовать следующие маленькие обертки для меньших размеров:
; IN (al) OUT ()
DisplaySignedNumber8:
push ax
cbw ;Promote AL to AX
call DisplaySignedNumber16
pop ax
ret
; -------------------------
; IN (ax) OUT ()
DisplaySignedNumber16:
push dx
cwd ;Promote AX to DX:AX
call DisplaySignedNumber32
pop dx
ret
; -------------------------
; IN (dx:ax) OUT ()
DisplaySignedNumber32:
push ax bx cx dx
...
В качестве альтернативы, если вы не возражаете против AX
а также DX
регистры используют это сквозное решение:
; IN (al) OUT () MOD (ax,dx)
DisplaySignedNumber8:
cbw
; --- --- --- --- -
; IN (ax) OUT () MOD (ax,dx)
DisplaySignedNumber16:
cwd
; --- --- --- --- -
; IN (dx:ax) OUT () MOD (ax,dx)
DisplaySignedNumber32:
push bx cx
...