Print a number in any base without help
The same program in M68K, RISC-V, Z80, x86.
This program prints 48879 in three bases: BEEF in base 16, 48879 in base 10, and
1011111011101111 in base 2. The print_in_base subroutine makes the digits itself, stores them
as a string, and asks service 4 to print that string.
Pass a nonnegative signed 32-bit integer (0 through 2147483647) in $a0 and a base from 2 through
36 in $a1. The subroutine prints the number followed by a newline. It changes $a0, $t0,
$t1, $v0, hi, and lo; callers that need those old values must save them. Bases below 2 are
invalid for this loop: division by zero has no answer, and dividing a positive number by 1 never
makes it smaller.
.data
buffer: .space 34 # room for 31 binary digits, a zero, and two spare bytes
buffer_end:
newline: .asciiz "\n"
.text
.globl main
# print_in_base(n, base): n in $a0, base in $a1
print_in_base:
la $t0, buffer_end # build the text backwards from the end
addi $t0, $t0, -1
sb $zero, 0($t0) # the terminator goes down first
digit:
div $a0, $a1 # n / base in lo, n % base in hi
mfhi $t1 # the digit
mflo $a0 # n = n / base
blt $t1, 10, number
addi $t1, $t1, 55 # 'A' is 65, so a 10 becomes 'A'
j store
number:
addi $t1, $t1, 48 # '0' is 48, so a 0 becomes '0'
store:
addi $t0, $t0, -1 # next free byte before the digits already stored
sb $t1, 0($t0)
bnez $a0, digit # until nothing is left of n
li $v0, 4 # service 4: print the string that was built
move $a0, $t0
syscall
li $v0, 4
la $a0, newline
syscall
jr $ra
main:
li $a0, 48879
li $a1, 16 # in hexadecimal
jal print_in_base
li $a0, 48879
li $a1, 10 # in decimal
jal print_in_base
li $a0, 48879
li $a1, 2 # in binary
jal print_in_base
li $v0, 10
syscall
The console shows:
BEEF
48879
1011111011101111
Dividing by the base and keeping the remainder gives you one digit, and it is the lowest one:
48879 divided by 16 is 3054 with 15 left over, and 15 is the F at the right hand end of BEEF. So
the digits arrive rightmost first. This program stores them from the end of the buffer toward its
beginning, leaving them in the order service 4 must print.
div is exactly the right instruction for this, because one pass of the loop needs both of its
answers: the remainder is the digit and the quotient is what you carry on dividing. One div, one
mfhi and one mflo, and the pass has everything it needs.
sb writes one character byte at a time. The addi before it moves $t0 to the next free byte;
sb itself does not move the pointer. At the end, $t0 points to the first digit. Service 4
starts there and prints bytes until it reaches the zero terminator placed at the end.
A digit is a number from 0 to base - 1 and it has to become a character. '0' is 48, so adding it
turns 0 to 9 into '0' to '9'; 'A' is 65 and a 10 has to become that, so the amount added
is 55. This assembler does not accept 'A' - 10 as the operand here, so the code uses 55 and
explains it in the comment. blt $t1, 10, number picks between numbers and letters, allowing
digits up to Z for base 36.
bnez $a0, digit checks the quotient copied into $a0 by mflo. When that quotient reaches zero,
there are no more digits to make. The loop still runs once for input zero: 0 / base gives a zero
remainder, so it stores one 0 character before stopping.
The longest input in the stated range, 2147483647, takes 31 binary digits. The last byte of
buffer holds the zero terminator. Up to 31 bytes before it hold digits, and two bytes at the
beginning remain unused. For the 16-digit binary result shown above, $t0 ends at
buffer_end - 17: 16 digits followed by the terminator.
Predict the third line if you change its base from 2 to 36. Select Open in editor, make the
change, then Build and Run: it should print 11PR. Base 36 is the limit because the routine
has characters for ten decimal digits and 26 letters.
Now change that third call to pass 0 in $a0 and base 16 in $a1. Predict how many characters the
loop stores, then Build and Run. The third line should be a single 0. Try 255 in base 16
as another check; the third line should be FF.