Lab 5 - Part 2 - x86
For the second part of this lab, I will further explore x86_64 assembly programming, building on what was done in Part 1.
1. I'll compare the assembly source code with its generated object file to understand how the human-readable instructions are translated into machine code. This exercise will highlight the differences between the code we write and the low-level output produced by the assembler.
Source code
Object file
2. Revise an x86 assembly loop program so that it outputs "loop" six times.
Below is the revised Makefile I used, which includes a rule for building loop-gas
. I simply added another entry in the BINARIES
list and replicated the same build steps used for hello-gas.s
:
BINARIES=hello-nasm hello-gas loop-gas all: ${BINARIES} AS_ARGS=-g hello-nasm: hello-nasm.s nasm -g -o hello-nasm.o -f elf64 hello-nasm.s ld -o hello-nasm hello-nasm.o hello-gas: hello-gas.s as ${AS_ARGS} -o hello-gas.o hello-gas.s ld -o hello-gas hello-gas.o loop-gas: loop-gas.s as ${AS_ARGS} -o loop-gas.o loop-gas.s ld -o loop-gas loop-gas.o clean: rm ${BINARIES} *.o || true
With that in place, I turned my attention to the assembly code itself. First, I wrote a program that would simply print the word “loop” a certain number of times (six, in this case). My file
loop-gas.s
looked like this: .text
.globl _start
min = 0 /* starting value for the loop index */
max = 6 /* loop will run until the index equals 6 */
_start:
mov $min, %r15 /* initialize loop counter in register r15 */
loop:
/* Write "loop\n" to stdout using the write syscall */
movq $len, %rdx /* message length */
movq $msg, %rsi /* pointer to message string */
movq $1, %rdi /* file descriptor 1 (stdout) */
movq $1, %rax /* syscall number for write */
syscall
inc %r15 /* increment loop counter */
cmp $max, %r15 /* compare counter with max value */
jne loop /* if not equal, loop again */
/* Exit program using the exit syscall */
movq $0, %rdi /* exit status 0 */
movq $60, %rax /* syscall number for exit */
syscall
.section .data
msg: .ascii "loop\n" /* string to print each iteration */
len = . - msg /* calculates the length of msg */
Running this program, I got exactly six lines of the word “loop,” which was my first task. Next, I wanted to enhance the code to show the current iteration number. To do that, I reserved a small section in
.data
for printing “loop: ” and then appended a single digit for the iteration number. I used %r15
as the loop counter, converted it to ASCII by adding the character code for '0'
, and then wrote it out. Below is the modified version that prints “loop: 0,” “loop: 1,” etc.:3.Revise the code so that it also displays the current iteration number in the output.
.text
.globl _start
min = 0
max = 6
_start:
mov $min, %r15
loop:
movq $len, %rdx
movq $msg, %rsi
movq $1, %rdi
movq $1, %rax
syscall
/* Convert loop index to ASCII digit and store it. */
mov %r15, %r10
add $'0', %r10
mov %r10b, num
/* Write the digit to stdout. */
movq $len2, %rdx
movq $num, %rsi
movq $1, %rdi
movq $1, %rax
syscall
inc %r15
cmp $max, %r15
jne loop
movq $0, %rdi
movq $60, %rax
syscall
.section .data
msg: .ascii "loop: "
len = . - msg
num: .byte '0'
.byte 10
len2 = . - num
output:
Now, the output listed each loop iteration right after the word “loop: ”, which felt like a small but significant step forward. The next step was to extend the range of my loop from 0 to 32 and display two-digit numbers. To achieve that, I needed to divide the loop counter by 10 to extract the tens and ones digits. In x86_64, the
div
instruction uses %rax
for the dividend and %rdx
for the remainder, so I moved the loop counter into %rax
, zeroed %rdx
, and loaded 10 into another register before calling div
.After the division, %rax
held the tens digit, and %rdx
held the ones digit. I then converted each to ASCII by adding '0'
and stored them into a two-character buffer. This code made it possible to display “00,” “01,” “02,” up to “32.” Of course, I also wanted to suppress the leading zero for single-digit numbers, so I included a small check that, if %rax
(the tens digit) was zero, I replaced that digit with a space. This gave a neater output like “ 0,” “ 1,” “10,” “11,” etc.
Finally, I got adventurous and decided to print the loop index in hexadecimal. The logic was much the same, except I replaced div
by 16, and if the remainder was greater than 9, I added 7 to jump from '9'
to 'A'
. This allowed me to show a range of values from 0 to 1F (31 in decimal), with uppercase letters for digits above 9. Checking if the quotient was zero also helped me decide whether to print a space or an actual hex digit.
4.Adjust the code so that it omits any unnecessary zero at the beginning of single-digit numbers.
In this task, I wanted to push my x86_64 assembly skills a bit further by modifying the loop code so that it prints out a two-digit number for each iteration—ranging from “00” to “32.” Previously, I had used a method on AArch64 to set up separate tens and units digits. For the x86_64 version, I decided to calculate the two-digit display directly from the loop counter by dividing it by 10. This approach leverages the division instruction, which splits the iteration counter into a quotient (representing the tens digit) and a remainder (representing the ones digit).
The key insight here is to move the loop counter into %rax
(since the div
instruction uses %rax
as the dividend), zero out %rdx
(to prepare for the division), and then divide by 10 (stored in %r10
). After the division, %rax
holds the tens digit and %rdx
holds the ones digit. I then convert these numerical values into their ASCII equivalents by adding the character constant for '0'
before writing them back into a small buffer. This updated buffer is then printed via the write syscall.
.text
.globl _start
min = 0
max = 33
_start:
mov $min,%r15
loop:
movq $len,%rdx
movq $msg,%rsi
movq $1,%rdi
movq $1,%rax
syscall
mov %r15, %rax
xor %rdx, %rdx
mov $10, %r10
div %r10
cmp $0, %rax
je space
mov %rax, %r10
add $'0', %r10
jmp update
space: mov $' ', %r10
update: mov %r10b, num
mov %rdx, %r11
add $'0', %r11
mov %r11b, num+1
movq $len2,%rdx
movq $num,%rsi
movq $1,%rdi
movq $1,%rax
syscall
inc %r15
cmp $max,%r15
jne loop
movq $0,%rdi
movq $60,%rax
syscall
.section .data
msg: .ascii "loop: "
len = . - msg
num: .ascii "00\n"
len2 = . - num
output:
5. Revise the code so that it outputs the loop counter in hexadecimal notation. Instead of showing the count in decimal, you'll update the division and conversion steps to represent the counter as a hexadecimal number, displaying values such as 0, 1, 2, …, up to 1F.
.text .globl _start min = 0 /* initial loop counter value (a constant symbol, not a variable) */ max = 33 /* loop will terminate once the counter reaches this value (i < max) */ _start: mov $min, %r15 /* set up the loop counter in register r15 */ loop: movq $len, %rdx /* load the length of the output message into rdx */ movq $msg, %rsi /* load the address of the message string into rsi */ movq $1, %rdi /* specify stdout (file descriptor 1) in rdi */ movq $1, %rax /* place the write syscall number (1) in rax */ syscall /* execute the syscall to print the message */ /* Divide the current counter to extract digits for display */ mov %r15, %rax /* transfer the current loop counter into rax for division */ xor %rdx, %rdx /* clear rdx to ensure proper division */ mov $16, %r10 /* load 16 into r10 as the divisor for hexadecimal conversion */ div %r10 /* perform division: quotient in rax, remainder in rdx */ /* Convert the quotient (tens digit) to an ASCII character */ cmp $0, %rax /* check if the quotient is zero */ je space /* if zero, jump to assign a space */ mov %rax, %r10 /* otherwise, move the quotient into r10 */ add $'0', %r10 /* convert quotient to its ASCII equivalent */ jmp check /* proceed to verify the remainder */ space: mov $' ', %r10 /* if quotient is zero, use a space character */ /* Adjust the remainder (units digit) if needed for hexadecimal output */ check: cmp $9, %rdx /* compare the remainder with 9 */ jg add /* if remainder > 9, jump to adjustment */ jmp update /* otherwise, continue without change */ add: add $7, %rdx /* add 7 to the remainder so that values 10-15 become 'A'-'F' */ update: mov %r10b, num /* store the ASCII character (or space) for the tens digit */ mov %rdx, %r11 /* move the remainder into r11 */ add $'0', %r11 /* convert the remainder to its ASCII character */ mov %r11b, num+1 /* store the ASCII character for the units digit */ /* Output the two-digit iteration number */ movq $len2, %rdx /* load the length of the number string into rdx */ movq $num, %rsi /* load the address of the number buffer into rsi */ movq $1, %rdi /* set the file descriptor to stdout in rdi */ movq $1, %rax /* load the syscall number for write into rax */ syscall /* execute the syscall to print the number */ inc %r15 /* increment the loop counter */ cmp $max, %r15 /* compare the counter to the maximum value */ jne loop /* if counter is still less than max, repeat the loop */ /* Exit the program */ movq $0, %rdi /* load the exit status (0) into rdi */ movq $60, %rax /* load the exit syscall number (60) into rax */ syscall /* execute the exit syscall */ .section .data msg: .ascii "loop: " /* base message to be printed before the iteration number */ len = . - msg /* compute the length of the message */ num: .ascii "00\n" /* buffer for displaying a two-character number and a newline */ len2 = . - num /* compute the length of the number buffer */
Reflections
In this lab, I deepened my understanding of x86_64 assembly by comparing the human-readable source code to its compiled object file. It was enlightening to see how simple instructions transform into machine code, revealing the inner workings of the processor and the meticulous nature of assembly programming. This exercise not only reinforced the importance of precise syntax and register management but also highlighted how every line of code contributes to the final executable, making the hidden complexity of computer operations much more tangible.
Comments
Post a Comment