Fizzbuzz in assembler
24 Jan 2022
Assembler is fun to tinker with, especially when one is a complete neophyte.
I set myself the task of writing fizzbuzz in assembler.
The first thing I decided was that the printing was the unimportant part, so I left that to C's `printf()`. I cargo-culted this with help from Stack Overflow, of course. The biggest secret seems to be in how you build the assembler file, so I'll show the Makefile in all of its glory:
.PHONY: all all: fizzbuzz .PHONY: clean clean: rm -rf fizzbuzz fizzbuzz.o fizzbuzz.o: as --gstabs fizzbuzz.s -o fizzbuzz.o fizzbuzz: fizzbuzz.o ld --dynamic-linker /lib64/ld-linux-x86-64.so.2 -o fizzbuzz fizzbuzz.o -lc
With that out of the way, we can focus on the assembly code itself.
First off, one needs to know the C/Linux/x86-64 calling convention. It's like this: The first 6 integer or pointer arguments go in these registers in this order: RDI, RSI, RDX, RCX, R8, R9. The return integer/pointer (after printf is done) ends up in the RAX register. Somewhere along the way, I also learned that there is a count of floating-point args that has to be given to printf (or is it all C function calls?), and that goes in RAX (which I guess gets clobbered when printf returns).
Anyway, a concrete example will be nice. Let's say I have the following printf format string "The number is: %lld\n" in the data section of my ELF file:
.section .data formatstring: .string "The number is: %lld\n"
Let's say I want to do the equivalent of printf("The number is: %lld\n", 42)
.
I would ensure the address formatstring
was in register RDI (the first
argument to a C function), 42 was in the RSI register (the second argument to a C function),
and 0 was in register RAX (the number of float args to a C function). Then, I would call
printf
.
movq $formatstring, %rdi movq $42, %rsi movq $0, %rax call printf
So now I have a way of using printf
.
Next, I need to loop from 1 to 100. (My fizzbuzz prints the numbers 1 through 100. The 100 is hard-coded. Hey, man, this is assembler; I'm not dealing with command-line arguments.) I decided to use register R15 as my counter. R15 doesn't seem to get used by anything (calling-convention-wise, I mean) so I figured it would be a good choice.
So, there's no real looping in assembler, of course; the best you can do is
jump to an address at the "top" of your loop. This reminds me of BASIC with line numbers
and GOTO. At least in assembler, I can label certain addresses and jump to those labels.
I can use the inc
command to increment the contents of the R15
register, and I can use the cmp
command to compare 100 with the
contents of the R15 register. This has side-effects over in the RFLAGS register
such that ja
(jump if above) takes no register argument, because the RFLAGS
register is implied. ja
will jump "out" of the loop (by jumping to
an address later in the program) when R15 contains a number larger than 100,
otherwise the command does not make the jump, allowing the next jump command (a simple
jmp
to return us to the top of the loop.
movq $1, %r15 _looptop: # do work here incq %r15 cmpq $100, %r15 ja _byebye jmp _looptop _byebye: movq $0, %rdi call exit