SPO600 Lab 5: Adventures in Assembly Language
Amir Mullagaliev

Amir Mullagaliev @amullagaliev

About: Seneca Computer Programming & Analysis student

Location:
Ontario, Toronto
Joined:
Sep 6, 2024

SPO600 Lab 5: Adventures in Assembly Language

Publish Date: Apr 18
9 0

Table of Contents

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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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          
Enter fullscreen mode Exit fullscreen mode

And don't even get me started on the syntax differences! In AArch64, destination register comes first:

mov x0, 1  
Enter fullscreen mode Exit fullscreen mode

But in x86_64, it's the other way around:

mov $1,%rax  
Enter fullscreen mode Exit fullscreen mode

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:
Enter fullscreen mode Exit fullscreen mode

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                
Enter fullscreen mode Exit fullscreen mode

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.

Comments 0 total

    Add comment