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
Post a Comment