Отображение номеров с 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 $'

Я искал DOS api (используя список прерываний Ralph Brown) и не нашел ни одной функции для вывода числа! Я пропустил это, и что я могу сделать?

Я хочу показать число в 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: деление на const 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.

    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

В каскад из 2 деления необходимы для деления 32-битного значения в  DX:AX на 10.
1-й дивизион делит высокий дивиденд (расширен с 0), что дает высокий  фактор. Второе подразделение делит низкий дивиденд (расширенный с помощью  остаток от 1-го дивизиона), давая низкий коэффициент. Это остаток  от 2-го деления, которое мы сохраняем в стеке.

Чтобы проверить, равен ли dword в DX:AX нулю, я OR -это обе половины в царапине  регистре.

Вместо подсчета цифр, требующих регистра, я решил поместить sentinel  в стеке. Поскольку этот страж получает значение (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 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
    ...
; 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
    ...