assemblydosx86-16integer-divisionsigned-integer

Displaying numbers with DOS


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 $'

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?

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


Solution

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

    Displaying the unsigned 16-bit number held in AX

    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)
    

    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.


        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:

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

    Method 2 : division by const 10

    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.

    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:


    Displaying the unsigned 32-bit number held in DX:AX

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

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

    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.

    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
    

    Displaying the signed 32-bit number held in DX:AX

    The procedure is as follows:

    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
    

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

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