用 DOS 显示数字 [英] Displaying numbers with DOS

查看:50
本文介绍了用 DOS 显示数字的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我的任务是编写一个程序来显示我的线性地址程序的 PSP.我写了以下内容:

I was tasked to write a program that displays the linear address of my program's PSP. I wrote the following:

        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 的中断列表)并没有找到一个函数来输出一个数字!我错过了吗,我该怎么办?

I searched the DOS api (using Ralph Brown's interrupt list) and didn't find a single function to output a number! Did I miss it, and what can I do?

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

I want to display the number in DX:AX in decimal.

推荐答案

确实,DOS 没有提供直接输出数字的功能.
您必须首先自己转换数字,然后让 DOS 显示它使用文本输出功能之一.

It's true that DOS doesn't offer us a function to output a number directly.
You'll have to first convert the number yourself and then have DOS display it using one of the text output functions.

在处理转换数字的问题时,有助于了解组成数字的数字相互关联.
让我们考虑数字 65535 及其分解:

When tackling the problem of converting a number, it helps to see how the digits that make up a number relate to each other.
Let's consider the number 65535 and its decomposition:

(6 * 10000) + (5 * 1000) + (5 * 100) + (3 * 10) + (5 * 1)

方法一:10的减幂除法

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

Method 1 : division by decreasing powers of 10

Processing the number going from the left to the right is convenient because it allows us to display an individual digit as soon as we've extracted it.

  • 通过将数字 (65535) 除以 10000,我们得到一个个位数的商(6) 我们可以直接输出为字符.我们也得到了余数(5535) 将成为下一步的红利.

  • By dividing the number (65535) by 10000, we obtain a single digit quotient (6) that we can output as a character straight away. We also get a remainder (5535) that will become the dividend in the next step.

将上一步 (5535) 的余数除以 1000,我们得到我们可以直接输出为字符的一位数商 (5).我们还得到一个余数 (535),它将成为下一步的红利.

By dividing the remainder from the previous step (5535) by 1000, we obtain a single digit quotient (5) that we can output as a character straight away. We also get a remainder (535) that will become the dividend in the next step.

将上一步 (535) 的余数除以 100,我们得到我们可以直接输出为字符的一位数商 (5).我们还得到一个余数 (35),它将成为下一步的红利.

By dividing the remainder from the previous step (535) by 100, we obtain a single digit quotient (5) that we can output as a character straight away. We also get a remainder (35) that will become the dividend in the next step.

将上一步 (35) 的余数除以 10,我们得到我们可以直接输出为字符的一位数商 (3).我们还得到一个余数 (5),它将成为下一步的红利.

By dividing the remainder from the previous step (35) by 10, we obtain a single digit quotient (3) that we can output as a character straight away. We also get a remainder (5) that will become the dividend in the next step.

将上一步(5)的余数除以1,我们得到我们可以直接输出为字符的一位数商 (5).这里的余数总是 0.(避免这种愚蠢的除以 1需要一些额外的代码)

By dividing the remainder from the previous step (5) by 1, we obtain a single digit quotient (5) that we can output as a character straight away. Here the remainder will always be 0. (Avoiding this silly division by 1 requires some extra code)


    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

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

Although this method will of course produce the correct result, it has a few drawbacks:

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

  • Consider the smaller number 255 and its decomposition:

(0 * 10000) + (0 * 1000) + (2 * 100) + (5 * 10) + (5 * 1)

如果我们使用相同的 5 步过程,我们会得到00255".那2个领先零是不可取的,我们必须包含额外的指令才能获得摆脱它们.

If we were to use the same 5 step process we'd get "00255". Those 2 leading zeroes are undesirable and we would have to include extra instructions to get rid of them.

分隔符随着每一步的变化而变化.我们必须在其中存储一个分隔符列表记忆.动态计算这些分频器是可能的,但引入了很多额外的部门.

The divider changes with each step. We had to store a list of dividers in memory. Dynamically calculating these dividers is possible but introduces a lot of extra divisions.

如果我们想将此方法应用于显示更大的数字,请说32 位,我们最终希望,所涉及的部门会得到确实有问题.

If we wanted to apply this method to displaying even larger numbers say 32-bit, and we will want to eventually, the divisions involved would get really problematic.

因此方法1不切实际,因此很少使用.

So method 1 is impractical and therefore it is seldom used.

处理从右到左的数字似乎违反直觉因为我们的目标是首先显示最左边的数字.但是当你即将发现,它工作得很好.

Processing the number going from the right to the left seems counter-intuitive since our goal is to display the leftmost digit first. But as you're about to find out, it works beautifully.

  • 通过将数字 (65535) 除以 10,我们得到商 (6553),它将成为下一步的红利.我们还得到余数(5),我们还不能输出,所以我们必须保存在某个地方.堆栈是一个方便的地方.

  • By dividing the number (65535) by 10, we obtain a quotient (6553) that will become the dividend in the next step. We also get a remainder (5) that we can't output just yet and so we'll have to save in somewhere. The stack is a convenient place to do so.

通过将上一步 (6553) 的商除以 10,我们得到将成为下一步红利的商 (655).我们也得到我们还不能输出的余数 (3),所以我们必须保存它某处.堆栈是一个方便的地方.

By dividing the quotient from the previous step (6553) by 10, we obtain a quotient (655) that will become the dividend in the next step. We also get a remainder (3) that we can't just yet output and so we'll have to save it somewhere. The stack is a convenient place to do so.

将上一步 (655) 的商除以 10,我们得到将成为下一步红利的商 (65).我们也得到我们还不能输出的余数 (5),所以我们必须保存它某处.堆栈是一个方便的地方.

By dividing the quotient from the previous step (655) by 10, we obtain a quotient (65) that will become the dividend in the next step. We also get a remainder (5) that we can't just yet output and so we'll have to save it somewhere. The stack is a convenient place to do so.

将上一步 (65) 的商除以 10,我们得到将成为下一步红利的商 (6).我们也得到我们还不能输出的余数 (5),所以我们必须保存它某处.堆栈是一个方便的地方.

By dividing the quotient from the previous step (65) by 10, we obtain a quotient (6) that will become the dividend in the next step. We also get a remainder (5) that we can't just yet output and so we'll have to save it somewhere. The stack is a convenient place to do so.

将上一步 (6) 的商除以 10,我们得到商 (0) 表示这是最后一个除法.我们也得到我们可以直接输出一个字符的余数 (6),但是不这样做被证明是最有效的,所以我们会将其保存在堆栈中.

By dividing the quotient from the previous step (6) by 10, we obtain a quotient (0) that signals that this was the last division. We also get a remainder (6) that we could output as a character straight away, but refraining from doing so turns out to be most effective and so as before we'll save it on the stack.

此时堆栈保存了我们的 5 个余数,每个余数都是一位数[0,9] 范围内的数字.由于堆栈是 LIFO(后进先出),因此我们首先POP 的值是我们想要显示的第一个数字.我们使用一个带有 5 个 POP 的单独循环以显示完整的数字.但在实践中,因为我们希望这个例程也能够处理具有少于 5 位,我们会在他们到达时计算数字,然后再计算许多POP.

At this point the stack holds our 5 remainders, each being a single digit number in the range [0,9]. Since the stack is LIFO (Last In First Out), the value that we'll POP first is the first digit we want displayed. We use a separate loop with 5 POP's to display the complete number. But in practice, since we want this routine to be able to also deal with numbers that have fewer than 5 digits, we'll count the digits as they arrive and later do that many 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

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

This second method has none of the drawbacks of the first method:

  • 因为我们在商变为零时停止,所以永远不会有任何问题带有丑陋的前导零.
  • 分隔线是固定的.这很容易.
  • 将此方法应用于显示更大的数字和这正是接下来要发生的事情.

上,2 个分区的级联是需要将 32 位值除以DX:AX 由 10.
第一个除法将高股息(用 0 扩展)除以产生高股息商.第 2 部分划分低股息(扩展为第一次除法的余数)产生低商.这是剩下的来自我们保存在堆栈中的第二个分区.

On 8086 a cascade of 2 divisions is needed to divide the 32-bit value in DX:AX by 10.
The 1st division divides the high dividend (extended with 0) yielding a high quotient. The 2nd division divides the low dividend (extended with the remainder from the 1st division) yielding the low quotient. It's the remainder from the 2nd division that we save on the stack.

要检查 DX:AX 中的 dword 是否为零,我将 OR 的两半都从头开始注册.

To check if the dword in DX:AX is zero, I've OR-ed both halves in a scratch register.

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

Instead of counting the digits, requiring a register, I chose to put a sentinel on the stack. Because this sentinel gets a value (10) that no digit can ever have ([0,9]), it nicely allows to determine when the display loop has to stop.

除此之外,此代码段类似于上面的方法 2.

Other than that this snippet is similar to method 2 above.

    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中的数字.

First find out if the signed number is negative by testing the sign bit.
If it is, then negate the number and output a "-" character but beware to not destroy the number in DX:AX in the process.

代码段的其余部分与无符号数相同.

The rest of the snippet is the same as for an unsigned number.

    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 用于较小的尺寸:


Will I need separate routines for different number sizes?

In a program where you need to display on occasion AL, AX, or DX:AX, you could just include the 32-bit version and use next little wrappers for the smaller sizes:

; 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 寄存器,请使用这个失败的解决方案:

Alternatively, if you don't mind the clobbering of the AX and DX registers use this fall-through solution:

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

这篇关于用 DOS 显示数字的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆