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

Modulus is even stranger in assembler. We actually use the processor's integer division instruction, which always does division and modulus at the same time: the instruction simply does an integer division and leaves the quotient in one register, and the remainder in another register. For intel in particular, the dividend is assumed to be a 128-bit number where the high bits need to be in the RDX register, and the low bits need to be in the RAX register. The divisor can be in a register of your choosing, but cannot be a literal. The quotient ends up in the RAX register, and the remainder ends up in the RDX register.

Remember how we were using R15 as our counter? Here is how we would divide that number by 3, check if the remainder was 0, and, if so, jump to the part of the program that prints "fizz":

movq $0,    %rdx   # dividend high bits
movq %r15,  %rax   # dividend low bits
movq $3,    %r14   # divisor is 3
divq %r14          # call div; quotient will be in %rax; remainder in %rdx
cmpq $0,    %rdx   # is the remainder 0?
je   _printfizz    # if so, jump to _printfizz

The printing itself requires some jumping around. What I decided to do was when a number modulo 3 (for instance) was 0, I would jump to the part of the program that prints "fizz" and then jump right back to where I was.

  cmpq $0, %rdx
  je   _printfizz
_doneprintfizz:

...

_printfizz:
  movq $fizz, %rdi
  movq $0, %rax  # 0 floating point parameters to printf
  call printf
  jmp _doneprintfizz

That's pretty much it! Here is the full assembler program in all of its glory. (A final reminder: this assembler is written by a hobbyist, and not someone who is paid to do this in his day job.)

.section .data

formatstring:
.string "%lld "

newline:
.string "\n"

fizz:
.string "fizz"

buzz:
.string "buzz"

.section .text

.globl _start
_start:

  movq $1, %r15

_looptop:

  movq $formatstring, %rdi
  movq %r15, %rsi
  movq $0, %rax  # 0 floating point parameters to printf
  call printf

  movq $0, %rdx
  movq %r15, %rax
  movq $3, %r14
  divq %r14
  cmpq $0, %rdx
  je   _printfizz
_doneprintfizz:

  movq $0, %rdx
  movq %r15, %rax
  movq $5, %r14
  divq %r14
  cmpq $0, %rdx
  je   _printbuzz
_doneprintbuzz:

  movq $newline, %rdi
  movq $0, %rax  # 0 floating point parameters to printf
  call printf

  incq %r15
  cmpq $100, %r15
  ja   _byebye

  jmp _looptop

_printfizz:
  movq $fizz, %rdi
  movq $0, %rax  # 0 floating point parameters to printf
  call printf
  jmp _doneprintfizz

_printbuzz:
  movq $buzz, %rdi
  movq $0, %rax  # 0 floating point parameters to printf
  call printf
  jmp _doneprintbuzz

_byebye:
  movq $0, %rdi
  movq $0, %rax  # 0 floating point parameters to exit
  call exit