【问题标题】:Displaying numbers with DOS用 DOS 显示数字
【发布时间】:2018-02-04 20:13:10
【问题描述】:

我的任务是编写一个程序来显示我的线性地址 程序的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's interrupt list) 并没有找到一个输出数字的函数! 我错过了它,我该怎么办?

我想以十进制显示DX:AX 中的数字。

【问题讨论】:

  • 还有 Assembly, printing ascii number 用于在进行一次 int 21h / ah=9 调用之前存储到缓冲区中的循环。
  • 还有Displaying Time in Assembly 用于一个简单的两位非循环版本,使用一个div 的商和余数。 (并使用 BIOS int 10h/ah=0Eh 而不是 DOS 打印,只是为了多样化)

标签: assembly dos x86-16 integer-division signed-integer


【解决方案1】:

确实,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。(避免这个 silly 除以 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 位,我们最终会希望,所涉及的部门会得到 真的有问题。

所以方法一不实用,所以很少使用。

方法 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 first 的值是我们要显示的第一个数字。我们使用一个 使用 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 个除法来将 32 位值除以 DX:AX 10.
第一除法除以高股息(以 0 扩展)产生高 商。第 2 师划分低股息(与 第一次除法的余数)产生低商。这是剩下的 从我们保存在堆栈中的第二个分区开始。

要检查DX:AX 中的双字是否为零,我已经OR-ed 两半 注册。

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

除了这个sn-p和上面的方法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

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

程序如下:

首先通过测试符号位来判断有符号数是否为负。
如果是,则取反数字并输出一个“-”字符,但要注意不要 在进程中销毁DX:AX中的号码。

sn-p 的其余部分与无符号数相同。

    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 位版本并使用下一个小wrappers 尺寸:

; 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
    ...

或者,如果您不介意破坏 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
    ...

【讨论】:

  • 您可以通过延迟 xchg(并且仅使用 mov 来提高速度而不是代码大小)来收紧 10 次幂的递减版本。 div/push dx/add al,'0'(短编码)/mov dl, al/mov ah, 2。或者,您可以利用商将 ah 零到 add ax, '0' + (2<<8) / mov dx, ax 的事实留下 ah=2 和 dl=ASCII_quotient,但这是以牺牲可读性为代价的,因此对初学者不利。
  • 更新了x86 tag wiki 的多位字符串常见问题解答条目中的链接。感谢您撰写了精彩的规范问答。
猜你喜欢
  • 2017-11-28
  • 1970-01-01
  • 2020-12-23
  • 2016-02-20
相关资源
最近更新 更多