A Dive into Assembly Programming: AArch64 vs. x86_64 (LAB4)

Writing and debugging assembly code is a unique and challenging experience that offers a deep understanding of the underlying architecture of a computer system. In this blog post, I'll share my experiences with writing and debugging assembly code while completing the tasks for the lab 4, and drawing comparisons between the AArch64 and x86_64 architectures.

Task 1: Hello World Loop

Starting with a simple "Hello World" loop in both AArch64 and x86_64, we encountered the raw power of registers. The minimalistic yet powerful nature of assembly language became apparent as we directly manipulated registers, delving into the bare metal of the architecture.

In AArch64, the mov, add, cmp, and b.ne instructions orchestrated the loop. Meanwhile, in x86_64, the dance of %rax, %rdi, and %rsi registers choreographed the elegant routine. Writing assembly code felt like composing a symphony of registers, each playing its part in harmony.

For AArch64 Assembly :-

.text
.globl _start

min = 0           /* starting value for the loop index; note that this is a symbol (constant), not a variable */
max = 10                     /* loop exits when the index hits this number (loop condition is i<max) */

_start:
    mov     x19, min

loop:
   // Hello World assembly code
    mov     x8, 1            // file descriptor for stdout (1)
    ldr     x0, =hello       // load the address of the hello string
    ldr     x1, =hello_len   // load the length of the hello string
    mov     x2, 0            // flags (unused)
    mov     x8, 64           // write is syscall #64
    svc     0                // invoke syscall

    // Increment the loop counter
    add     x19, x19, 1

    // Compare the loop counter with the maximum value
    cmp     x19, max
    b.ne    loop

    // Exit the program
    mov     x0, 0            // status -> 0
    mov     x8, 93           // exit is syscall #93
    svc     0                // invoke syscall

.data
hello:
    .ascii "Loop\n"
hello_len = . - hello

Task 2: Hello World Loop with System Calls


The art of invoking system calls unveiled itself as we integrated "Hello World" loops with system calls. Whether it was AArch64's svc instruction or x86_64's syscall, the act of communicating with the operating system felt like orchestrating a grand performance.

In AArch64, the system call numbers were specified directly in registers, while in x86_64, the syscall number dictated the operation. The simplicity of the system call interface showcased the elegance of assembly language in interacting with the broader computing environment.

For AArch64 Assembly :-

.text
.globl _start

min = 0                          /* starting value for the loop index; note that this is a symbol (constant), not a variable */
max = 10                         /* loop exits when the index hits this number (loop condition is i<max) */

_start:

    mov     x19, min

loop:
    // Hello World with loop index assembly code
    mov     x8, 1            // file descriptor for stdout (1)
    ldr     x0, =loop_msg    // load the address of the loop_msg string
    ldr     x1, =loop_msg_len// load the length of the loop_msg string
    mov     x2, 0            // flags (unused)
    mov     x8, 64           // write is syscall #64
    svc     0                // invoke syscall

    // Increment the loop counter
    add     x19, x19, 1

    // Compare the loop counter with the maximum value
    cmp     x19, max
    b.ne    loop

    // Exit the program
    mov     x0, 0            // status -> 0
    mov     x8, 93           // exit is syscall #93
    svc     0                // invoke syscall

.data
loop_msg:
    .asciz "Loop: %d\n"
loop_msg_len = . - loop_msg

For x86_64 Assembly :-

.section .data
loop_msg:
    .ascii "Loop: %d\n"
loop_msg_len = . - loop_msg

.section .text
.globl _start

min = 0                         /* starting value for the loop index; note that this is a symbol (constant), not a variable */
max = 10                        /* loop exits when the index hits this number (loop condition is i<max) */

_start:
    mov     $min, %r15          /* loop index */

loop:
    // Hello World with loop index assembly code
    mov     $1, %rdi            // file descriptor for stdout (1)
    lea     rsi, [loop_msg]     // load the address of the loop_msg string
    mov     rdx, loop_msg_len   // load the length of the loop_msg string
    mov     rax, 1              // write is syscall #1
    syscall

    // Increment the loop counter
    inc     %r15

    // Compare the loop counter with the maximum value
    cmp     $max, %r15
    jne     loop

    // Exit the program
    mov     $0, %rdi            // status -> 0
    mov     $60, %rax           // exit is syscall #60
    syscall

Task 3 & 4 : Looping with 2-Digit Decimal Numbers / Suppressing Leading Zeros

Diverging into more complex tasks, the elegance of AArch64's instruction set became evident. Converting loop indices to 2-digit decimal numbers required the use of udiv and umod instructions, showcasing the streamlined design of AArch64.

On the x86_64 side, the same task showcased the legacy nature of the architecture. The div instruction, while powerful, demands careful setup with registers, providing a stark contrast to the more streamlined approach of AArch64.

Suppressing leading zeros in the 2-digit decimal display brought forth the delicate nature of assembly programming. In AArch64, a simple branch instruction sufficed, emphasizing the architecture's simplicity. On the x86_64 side, conditional jumps provided a similar but subtly different approach.

For AArch64 Assembly :-

.text
.globl _start

min = 0                          /* starting value for the loop index; note that this is a symbol (constant), not a variable */
max = 31                         /* loop exits when the index hits this number (loop condition is i<=max) */

_start:

    mov     x19, min

loop:
    // Convert the loop index to a 2-digit decimal number
    udiv    x20, x19, 10        /* x20 = x19 / 10 (quotient) */
    umod    x21, x19, 10        /* x21 = x19 % 10 (remainder) */

    // Print the 2-digit decimal number
    mov     x8, 1                /* file descriptor for stdout (1) */
    ldr     x0, =digit_msg       /* load the address of the digit_msg string */
    ldr     x1, =digit_msg_len   /* load the length of the digit_msg string */
    mov     x2, 0                /* flags (unused) */
    mov     x8, 64               /* write is syscall #64 */
    svc     0                    /* invoke syscall */

    // Print a newline
    mov     x8, 1                /* file descriptor for stdout (1) */
    ldr     x0, =newline         /* load the address of the newline string */
    ldr     x1, =newline_len     /* load the length of the newline string */
    mov     x2, 0                /* flags (unused) */
    mov     x8, 64               /* write is syscall #64 */
    svc     0                    /* invoke syscall */

    // Increment the loop counter
    add     x19, x19, 1

    // Compare the loop counter with the maximum value
    cmp     x19, max
    b.le    loop

    // Exit the program
    mov     x0, 0                /* status -> 0 */
    mov     x8, 93               /* exit is syscall #93 */
    svc     0                    /* invoke syscall */

.data
digit_msg:
    .asciz "%d"
digit_msg_len = . - digit_msg

newline:
    .asciz "\n"
newline_len = . - newline

.section .data
digit_msg:
    .ascii "%02d"
digit_msg_len = . - digit_msg

newline:
    .asciz "\n"
newline_len = . - newline

For x86_64 Assembly :-

.section .text
.globl _start

min = 0                          /* starting value for the loop index; note that this is a symbol (constant), not a variable */
max = 31                         /* loop exits when the index hits this number (loop condition is i<=max) */

_start:
    mov     $min, %r15           /* loop index */

loop:

    // Convert the loop index to a 2-digit decimal number
    mov     %r15, %rdi           /* move loop index to rdi for the syscall */
    mov     $10, %rax            /* divisor for the division */
    xor     %rdx, %rdx           /* clear any previous remainder */
    div     %rax                 /* divide rdx:rax by 10, result in rax, remainder in rdx */

    // Print the 2-digit decimal number
    mov     $1, %rdi             /* file descriptor for stdout (1) */
    lea     rsi, [digit_msg]     /* load the address of the digit_msg string */
    mov     rdx, digit_msg_len   /* load the length of the digit_msg string */
    mov     rax, 1               /* write is syscall #1 */
    syscall

    // Print a newline
    mov     $1, %rdi             /* file descriptor for stdout (1) */
    lea     rsi, [newline]       /* load the address of the newline string */
    mov     rdx, newline_len     /* load the length of the newline string */
    mov     rax, 1               /* write is syscall #1 */
    syscall

    // Increment the loop counter
    inc     %r15

    // Compare the loop counter with the maximum value
    cmp     $max, %r15
    jle     loop

    // Exit the program
    mov     $0, %rdi             /* status -> 0 */
    mov     $60, %rax            /* exit is syscall #60 */
    syscall

Debugging Challenges and Tools

Debugging assembly code is a meticulous process. Lack of high-level abstractions means you have to rely on register values and memory inspection. Tools like gdb (GNU Debugger) become invaluable, providing a closer look at the program's execution flow.

Understanding the intricacies of the architecture, tracking register values, and setting breakpoints are essential for effective debugging. Both AArch64 and x86_64 share common debugging principles, but the specific details can vary.

Personal Perspectives

While both AArch64 and x86_64 assembly languages have their complexities, each offers a unique set of advantages. AArch64 is elegant and streamlined, often favored in energy-efficient devices. x86_64, with its historical dominance in personal computing, provides a robust and flexible platform.

In my experience, AArch64 feels more approachable, with cleaner syntax and a straightforward instruction set. On the other hand, x86_64, though more complex, offers immense power and compatibility.

In conclusion, diving into assembly programming enhances one's understanding of computer architecture and provides a deeper appreciation for the higher-level languages we often take for granted. Whether it's the elegance of AArch64 or the power of x86_64, each architecture has its merits, making assembly programming an enlightening journey for any developer.






 

Comments

Popular posts from this blog

Navigating the Patch Submission Processes in Open Source Communities: Linux Kernel & Python

Building GCC from Source: A Journey into Compiler Construction (Project Stage 1)

Lab 3 - Understanding arithmetic/math and strings in 6502 assembly language(SPO600)