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.
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:
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.
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.
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.
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.
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.
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)
Although this method will of course produce the correct result, it has a few drawbacks:
Consider the smaller number 255 and its decomposition:
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.
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.
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.
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.
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.
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.
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.
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.
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 5POP
'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 manyPOP
's.This second method has none of the drawbacks of the first method:
Displaying the unsigned 32-bit number held in DX:AX
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.
To check if the dword in
DX:AX
is zero, I'veOR
-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.
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.
Will I need separate routines for different number sizes?
In a program where you need to display on occasion
AL
,AX
, orDX:AX
, you could just include the 32-bit version and use next little wrappers for the smaller sizes:Alternatively, if you don't mind the clobbering of the
AX
andDX
registers use this fall-through solution: