See the x86 tag wiki for the instruction set reference manual, and many good links to reference material and tutorials.
It takes enough code to split up an integer into ASCII digits that you should factor it out into a function.
This is an optimized and bugfixed version of @hobbs's print2Digits function. (I also bugfixed the version in his answer, so it's correct too, but left the optimizations for this one).
print2Digits:
;; input in AL (0-99). (Or preferably already zero-extended to AX so we can omit CBW)
;; clobbers AX and DX
cbw ; zero AH. Sign-extending AL does the job because AL is only allowed to be 0-99.
mov dl, 10
div dl ; quotient in AL(first (high) digit), remainder in AH(second (low) digit)
add ax, 0x3030 ; add '0' to al and ah at the same time.
mov dl, ah ; save the 2nd digit
mov ah, 0x0E ; BIOS call #: print single character
int 0x10 ; print high digit first. Doesn't clobber anything, so AH still holds 0x0E after
mov al, dl
int 0x10 ; print the low digit 2nd
ret
Since we used div
to split an integer into two base10 digits, we need ah
to be zero. i.e. for the dividend to be in AX, not just AL with possible garbage in AH. We could save the cbw
or mov ah,0
if the caller did movzx ax, ch
or something to zero ah
.
(Except that 8086 doesn't have movzx
, so you'd actually want xor ax,ax
/ mov al, ch
.)
There's a DOS system call for printing a whole string, so you could store characters into a small buffer and print them all at once, like I do in this AMD64 Linux FizzBuzz. See also How do I print an integer in Assembly Level Programming without printf from the c library? for a more general int->string in a buffer function, or other multi-digit number links in the x86 tag wiki
It's also possible to use aam
to divide AL (instead of AX) by 10, avoiding the need to zero AH first. It's slightly faster than div r8
on current Intel and AMD CPUs. However, it puts the results in the opposite registers from div
, which means extra instructions after the aam
. This balances out the saving on the mov dl, 10
and cbw
.
print2Digits:
;; input in AL (0-99). (Ignores AH because we use AAM instead of div)
;; clobbers AX and DX
aam ; like `div` by 10, but with the outputs reversed, and input from AL only
;; quotient in AH (high digit), remainder in AL(low digit). (Opposite to div)
add ax, 0x3030 ; add '0' to al and ah at the same time.
mov dl, al ; save the low digit
mov al, ah ; print high digit first
mov ah, 0x0E ; BIOS call #: print single character
int 0x10 ; print first digit. Doesn't clobber anything, so AH still holds 0x0E after
mov al, dl
int 0x10 ; print second digit
ret
Even if we wanted to store to a string (and make one call to a print-string function or system call), we'd have to swap al and ah before storing AX to memory (e.g. xchg al,ah
, or more efficiently on modern hardware but requiring 186: rol ax,8
). div
produces them in the right order inside AX.
For 386 where 32bit address-size is available, we can save one instruction:
lea dx, [eax + 0x3030] ; need a 32bit addressing mode to use eax as a source reg. Adds '0' to both digits at once, with a different destination.
mov al, dh ; then get ready to print the high byte first
The lea
needs an address-size prefix and a 2-byte mod/rm, and a 32bit displacement, so it loses badly on code-size, but it does save one instruction.
Using lea
to read from eax
after div
writes ax
will probably be faster on Sandybridge-family CPUs, esp. Haswell and later, but on Intel pre-SnB, the partial register stall will make it better to use the pure 16bit version with separate add and mov instructions.
Of course if you actually cared about performance, you'd use a multiplicative inverse instead of actually dividing by 10. And you usually wouldn't be writing 16-bit code that makes legacy BIOS calls either!