Отображение чисел с 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
    ...
Другие вопросы по тегам