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 */
output:


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 */
output:



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

Popular posts from this blog

Lab 2 - Lab Results

Project Stage 1

Lab 5 - Part 1 - Aarch 64