使用DOS显示数字

5

我的任务是编写一个程序来显示我的程序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中的数字。


同时,汇编打印ASCII码的问题还涉及到一个循环,它先将字符存入缓冲区,然后再进行一次int 21h / ah=9的调用。 - Peter Cordes
同时显示汇编中的时间,以一个简单的非循环2位版本为例,使用一个div操作的商和余数。(并且为了增加变化,使用BIOS int 10h/ah=0Eh来打印,而不是DOS) - Peter Cordes
1个回答

11

确实,DOS不能直接输出数字,您需要先自行将数字转换,然后使用文本输出函数让DOS显示它。


显示AX寄存器中保存的无符号16位数字

在解决转换数字问题时,了解组成数字的每个数字之间的关系会有所帮助。例如,我们考虑数字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)
如果我们使用相同的五步过程,我们会得到“00255”。这两个前导零是不可取的,我们需要包含额外的指令来去除它们。
每一步的除数都在变化。我们不得不在内存中存储除数列表。动态计算这些除数可能是可行的,但会引入大量额外的除法。
如果我们想将此方法应用于显示更大的数字,比如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(后进先出),我们将首先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

第二种方法没有第一种方法的任何缺点:

  • 因为我们在商变为零时停止,所以从来没有丑陋的前导零问题。
  • 除数是固定的。那很容易。
  • 将此方法应用于显示更大的数字非常简单,这正是接下来要做的。

显示存储在DX:AX中的无符号32位数字

上,需要级联 2 次除法才能将 DX:AX 中的 32 位值除以10。
第一次除法将高位被除数(扩展为0)除以10,得到一个高商。第二次除法将低位被除数(扩展为第一次除法的余数)除以10,得到一个低商。我们将第二次除法的余数保存在堆栈中。

为了检查 DX:AX 中的双字是否为零,我在一个临时寄存器中对两个半字进行了 OR 运算。

我选择在堆栈上放置一个哨兵值,而不是计算数字并需要一个寄存器。因为这个哨兵得到的值(10)是任何数字都不可能有的值([0,9]),它可以很好地确定显示循环何时应该停止。

除此之外,这段代码与上面的第二种方法类似。

    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

显示存储在 DX:AX 中的32位有符号数

步骤如下:

首先通过测试符号位来确定有符号数是否为负数。
如果是,那么取反该数并输出“-”字符,但要注意不要在过程中破坏 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

我需要为不同的数字大小编写不同的例程吗?

在需要偶尔显示ALAXDX: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
    ...

2
你可以通过延迟xchg指令(并且只使用mov指令来提高速度而不是代码大小)来优化减少10的幂版本。使用divpush dxadd al,'0'(短编码)、mov dl, almov ah, 2。或者你可以利用商数将ah置零的事实,使用add ax, '0' + (2<<8)mov dx, ax,这样ah就等于2,dl等于ASCII码的商数,但这会降低可读性,所以对初学者来说不太好。 - Peter Cordes
1
{btsdaf} - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接