用DOS显示数字

Sep*_*and 4 assembly dos integer-division x86-16 signed-integer

我的任务是编写一个显示程序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 $'
Run Code Online (Sandbox Code Playgroud)

我搜索了DOS api(使用Ralph Brown的中断列表)并没有找到输出数字的单个函数!我错过了吗,我该怎么办?

我想以DX:AX十进制显示数字.

Sep*_*and 8

DOS确实没有为我们提供直接输出数字的功能.
您必须先自己转换数字然后让DOS使用其中一个文本输出函数显示它.

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

在解决转换数字的问题时,有助于了解构成数字的数字如何相互关联.
我们考虑数字65535及其分解:

(6 * 10000) + (5 * 1000) + (5 * 100) + (3 * 10) + (5 * 1)
Run Code Online (Sandbox Code Playgroud)

方法1:通过降低10的幂来除法

处理从左到右的数字很方便,因为它允许我们在提取它后立即显示单个数字.

  • 通过将数字(65535)除以10000,我们得到一个数字商(6),我们可以直接输出为一个字符.我们还得到一个剩余部分(5535),它将成为下一步的股息.

  • 由通过将来自前一步骤(5535)余数1000,我们得到一个数字商(5),我们可以作为字符输出马上.我们还得到一个剩余部分(535),它将成为下一步的股息.

  • 通过从由前一步骤(535)将所述剩余部分100,我们得到一个数字商(5),我们可以作为字符输出马上.我们还得到一个剩余部分(35),它将成为下一步的股息.

  • 通过将前一步骤(35)的余数除以10,我们得到一个数字商(3),我们可以立即输出为一个字符.我们还得到一个余数(5),它将成为下一步的红利.

  • 通过将前一步骤(5)的余数除以1,我们得到一个单位数(5),我们可以立即输出为一个字符.这里的余数总是为0.(避免这个愚蠢的除法需要一些额外的代码)


    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
Run Code Online (Sandbox Code Playgroud)

虽然这种方法当然会产生正确的结果,但它有一些缺点:

  • 考虑较小的数字255及其分解:

    (0 * 10000) + (0 * 1000) + (2 * 100) + (5 * 10) + (5 * 1)
    
    Run Code Online (Sandbox Code Playgroud)

    如果我们使用相同的5步过程,我们将获得"00255".这两个前导零是不可取的,我们将不得不包括额外的指令来摆脱它们.

  • 分隔符随每一步而变化.我们必须在内存中存储一​​个分隔符列表.动态计算这些分频器是可能的,但引入了许多额外的划分.

  • 如果我们想要应用这种方法来显示更大的数字,比如说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(后进先出),因此我们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
Run Code Online (Sandbox Code Playgroud)

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

  • 因为当商变为零时我们停止,所以丑陋的前导零从来没有任何问题.
  • 分频器是固定的.这很容易.
  • 将这种方法应用于显示更大的数字真的很简单,这正是接下来要做的事情.

显示DX:AX中保存的无符号32位数

一个2级的级联来将32位值 DX:AX除以10.
第1级除以高被除数(用0扩展)产生高商.第二个除法将低被除数(从第一个除法的余数延伸)除以产生低商数.它是我们在堆栈上保存的第二个分区的剩余部分.

为了检查dword 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
Run Code Online (Sandbox Code Playgroud)

显示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
Run Code Online (Sandbox Code Playgroud)

我是否需要针对不同数量大小的单独例程?

在你需要显示有时一个程序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
    ...
Run Code Online (Sandbox Code Playgroud)

或者,如果你不介意AXDX寄存器的破坏使用这个落后的解决方案:

; 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
    ...
Run Code Online (Sandbox Code Playgroud)

  • 您可以通过延迟`xchg`(并且仅使用`mov`来提高速度而不是使用代码大小)来加强10级递减功能。`div` /`push dx` /`add al,'0'`(短编码)/`mov dl,al` /`mov ah,2`。或者,您可以利用以下事实:商将ah保留为零以添加斧头,'0'+(2 << 8)`/`mov dx,ax`保留ah = 2和dl = ASCII_quotient,但是这是以牺牲可读性为代价的,因此对初学者不利。 (2认同)