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

Apr 19, 2025 - 01:04
 0
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:

  1. A basic loop that prints Loop 6 times
  2. Modify it to print Loop: # where # is the loop index (0-5)
  3. Extend it to print 2-digit numbers (00-32)
  4. Suppress leading zeros
  5. 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:

  1. 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.

  2. 6502: Simple and limited, which is actually nice for beginners.

  3. 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:

  1. Write code
  2. Compile
  3. Get cryptic error message
  4. Stare at code for 10 minutes
  5. Realize I used // for comments instead of # in GNU assembler
  6. 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

  1. Assembly is PRECISE: A single wrong register or memory address and everything breaks.

  2. Different architectures = different paradigms: x86_64 and AArch64 handle things like division completely differently.

  3. Comments are ESSENTIAL: Without comments, I'd have no idea what my own code was doing 5 minutes after writing it.

  4. 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:

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.