SPO600 Lab 5: Adventures in Assembly Language
Table of Contents Introduction Lab Requirements Implementing the Loop in AArch64 Implementing the Loop in x86_64 Comparing Assembly Languages Debugging Headaches Code Breakdown Lessons Learned Full Source Code Conclusion Introduction I've completed Lab 5 for the SPO600 course, and let me tell you - working with assembly language is like trying to communicate with aliens using only hand gestures. This lab focused on experimenting with assembler on both x86_64 and AArch64 platforms. I had to write programs that looped through numbers, converted them to characters, and printed them to the screen. Sounds simple, right? WRONG. Nothing is simple in assembly! Lab Requirements The lab required me to implement the following in both AArch64 and x86_64 assembly: A basic loop that prints Loop 6 times Modify it to print Loop: # where # is the loop index (0-5) Extend it to print 2-digit numbers (00-32) Suppress leading zeros Change to hexadecimal output (0-20) Implementing the Loop in AArch64 My very first roadblock with AArch64 was figuring out how to actually modify a buffer in memory. With higher-level languages, you'd just do something like message[6] = digit + '0'; but in assembly... nope! You need to load addresses, use registers, and do all kinds of register juggling. For example, to print Loop: # with the index, I had to: mov x20, x19 add x20, x20, 48 ldr x1, =message strb w20, [x1, digit_pos] The hardest part was definitely the 2-digit conversion. I spent way too long figuring out how to divide numbers in AArch64. Turns out you need udiv for division and msub to calculate the remainder: mov x20, x19 mov x21, 10 udiv x22, x20, x21 msub x23, x22, x21, x20 # x23 = x20 - (x22 * x21) = remainder Implementing the Loop in x86_64 Working with x86_64 after AArch64 was like switching from "Japanese" to "German" - still foreign, but somehow differently confusing! The x86_64 division was a total pain. You have to clear specific registers, put values in specific places, and the division gives both quotient AND remainder: mov %r15,%rax mov $0,%rdx mov $10,%rcx div %rcx And don't even get me started on the syntax differences! In AArch64, destination register comes first: mov x0, 1 But in x86_64, it's the other way around: mov $1,%rax I kept mixing them up, and my programs wouldn't assemble. Comparing Assembly Languages Now that I've worked with three assembly languages (6502, x86_64, and AArch64), here's my totally subjective ranking: AArch64: Cleanest syntax and most consistent. The register naming makes sense (x0, x1, etc.), and the instruction names are mostly intuitive. The best part is having separate instructions for quotient and remainder. 6502: Simple and limited, which is actually nice for beginners. x86_64: Most powerful but also most confusing. The register naming is historical (%rax, %rbx, %r15) with no obvious pattern. Instructions are cryptic (%al vs %ax vs %eax vs %rax). Division is a nightmare requiring specific register setup. Debugging Headaches Here's what my debugging process looked like: Write code Compile Get cryptic error message Stare at code for 10 minutes Realize I used // for comments instead of # in GNU assembler Fix and repeat The worst part was when the program assembled but didn't work right. With no debugger (or at least none that I knew how to use properly), I was basically adding write statements to see what was happening inside - like printf debugging. Code Breakdown Let's look at a small piece of the hexadecimal conversion in AArch64: cmp x22, 10 b.ge high_alpha add x22, x22, 48 b high_done high_alpha: add x22, x22, 55 high_done: This code checks if a hex digit is 0-9 or A-F and converts it. For 0-9, we add 48 (ASCII for '0'). For 10-15, we add 55 to get 'A'-'F'. Lessons Learned Assembly is PRECISE: A single wrong register or memory address and everything breaks. Different architectures = different paradigms: x86_64 and AArch64 handle things like division completely differently. Comments are ESSENTIAL: Without comments, I'd have no idea what my own code was doing 5 minutes after writing it. Register allocation matters: In higher level languages, variables just exist. In assembly, you need to carefully plan which registers to use for what. Full Source Code Here are the links to the full source code: AArch64 loop1.s AArch64 loop2.s AArch64 loop3.s AArch64 loop4.s AArch64 loop5.s x86_64 loop1_x86.s x86_64 loop2_x86.s x86_64 loop3_x86.s x86_64 loop4_x86.s x86_64 loop5_x86.s I'll just paste the AArch64 loop5.s code here as an example (I'm probably proudest of this one since it handles hex conversion :D): .data message: .ascii "Loop: ##\n" messag

Table of Contents
- Introduction
- Lab Requirements
- Implementing the Loop in AArch64
- Implementing the Loop in x86_64
- Comparing Assembly Languages
- Debugging Headaches
- Code Breakdown
- Lessons Learned
- Full Source Code
- Conclusion
Introduction
I've completed Lab 5 for the SPO600 course, and let me tell you - working with assembly language is like trying to communicate with aliens using only hand gestures.
This lab focused on experimenting with assembler on both x86_64 and AArch64 platforms. I had to write programs that looped through numbers, converted them to characters, and printed them to the screen. Sounds simple, right? WRONG. Nothing is simple in assembly!
Lab Requirements
The lab required me to implement the following in both AArch64 and x86_64 assembly:
- A basic loop that prints
Loop
6 times - Modify it to print
Loop: #
where # is the loop index (0-5) - Extend it to print 2-digit numbers (00-32)
- Suppress leading zeros
- Change to hexadecimal output (0-20)
Implementing the Loop in AArch64
My very first roadblock with AArch64 was figuring out how to actually modify a buffer in memory. With higher-level languages, you'd just do something like message[6] = digit + '0';
but in assembly... nope! You need to load addresses, use registers, and do all kinds of register juggling.
For example, to print Loop: #
with the index, I had to:
mov x20, x19
add x20, x20, 48
ldr x1, =message
strb w20, [x1, digit_pos]
The hardest part was definitely the 2-digit conversion. I spent way too long figuring out how to divide numbers in AArch64. Turns out you need udiv
for division and msub
to calculate the remainder:
mov x20, x19
mov x21, 10
udiv x22, x20, x21
msub x23, x22, x21, x20 # x23 = x20 - (x22 * x21) = remainder
Implementing the Loop in x86_64
Working with x86_64 after AArch64 was like switching from "Japanese" to "German" - still foreign, but somehow differently confusing!
The x86_64 division was a total pain. You have to clear specific registers, put values in specific places, and the division gives both quotient AND remainder:
mov %r15,%rax
mov $0,%rdx
mov $10,%rcx
div %rcx
And don't even get me started on the syntax differences! In AArch64, destination register comes first:
mov x0, 1
But in x86_64, it's the other way around:
mov $1,%rax
I kept mixing them up, and my programs wouldn't assemble.
Comparing Assembly Languages
Now that I've worked with three assembly languages (6502
, x86_64
, and AArch64
), here's my totally subjective ranking:
AArch64: Cleanest syntax and most consistent. The register naming makes sense (
x0
,x1
, etc.), and the instruction names are mostly intuitive. The best part is having separate instructions for quotient and remainder.6502: Simple and limited, which is actually nice for beginners.
x86_64: Most powerful but also most confusing. The register naming is historical (
%rax
,%rbx
,%r15
) with no obvious pattern. Instructions are cryptic (%al
vs%ax
vs%eax
vs%rax
). Division is a nightmare requiring specific register setup.
Debugging Headaches
Here's what my debugging process looked like:
- Write code
- Compile
- Get cryptic error message
- Stare at code for 10 minutes
- Realize I used
//
for comments instead of#
in GNU assembler - Fix and repeat
The worst part was when the program assembled but didn't work right. With no debugger (or at least none that I knew how to use properly), I was basically adding write statements to see what was happening inside - like printf
debugging.
Code Breakdown
Let's look at a small piece of the hexadecimal conversion in AArch64:
cmp x22, 10
b.ge high_alpha
add x22, x22, 48
b high_done
high_alpha:
add x22, x22, 55
high_done:
This code checks if a hex digit is 0-9 or A-F and converts it. For 0-9, we add 48 (ASCII for '0'). For 10-15, we add 55 to get 'A'-'F'.
Lessons Learned
Assembly is PRECISE: A single wrong register or memory address and everything breaks.
Different architectures = different paradigms: x86_64 and AArch64 handle things like division completely differently.
Comments are ESSENTIAL: Without comments, I'd have no idea what my own code was doing 5 minutes after writing it.
Register allocation matters: In higher level languages, variables just exist. In assembly, you need to carefully plan which registers to use for what.
Full Source Code
Here are the links to the full source code:
- AArch64 loop1.s
- AArch64 loop2.s
- AArch64 loop3.s
- AArch64 loop4.s
- AArch64 loop5.s
- x86_64 loop1_x86.s
- x86_64 loop2_x86.s
- x86_64 loop3_x86.s
- x86_64 loop4_x86.s
- x86_64 loop5_x86.s
I'll just paste the AArch64 loop5.s
code here as an example (I'm probably proudest of this one since it handles hex conversion :D):
.data
message:
.ascii "Loop: ##\n"
message_len = . - message
hex1_pos = 6
hex2_pos = 7
space = 32
.text
.globl _start
min = 0
max = 33
_start:
mov x19, min
loop:
mov x20, x19
mov x21, 16
udiv x22, x20, x21
msub x23, x22, x21, x20 # x23 = x20 - (x22 * x21) = remainder
cmp x22, 10
b.ge high_alpha
add x22, x22, 48
b high_done
high_alpha:
add x22, x22, 55
high_done:
# Convert low nibble to ASCII
cmp x23, 10
b.ge low_alpha
add x23, x23, 48
b low_done
low_alpha:
add x23, x23, 55
low_done:
ldr x1, =message
cmp x22, 48
b.ne print_both
mov x24, space
strb w24, [x1, hex1_pos]
b print_low
print_both:
strb w22, [x1, hex1_pos]
print_low:
strb w23, [x1, hex2_pos]
mov x0, 1 # 1 is stdout
mov x2, message_len # message length
mov x8, 64 # 64 is write
svc 0
add x19, x19, 1
cmp x19, max
b.ne loop
mov x0, 0 # set exit status to 0
mov x8, 93 # exit is syscall #93
svc 0
Conclusion
In conclusion, would I write assembly code in my free time? Probably not. But I have a much better understanding of what's happening under the hood of my programs now.