Lab 5 : Explore Assembly @x86 Server

Building on Part 1's work, I will delve deeper into x86_64 assembly programming in this lab's second section.


1. To learn how the machine code is derived from the human-readable instructions, I will compare the assembly source code with the object file that is produced. The distinctions between the code we write and the assembler's low-level output will be demonstrated in this exercise.


Source code




Object file


2. Make an x86 assembly loop program produce a "loop" six times by revising it.

Below is the modified Makefile I used, which now contains a rule for opening loopholes. I simply added a new entry to the BINARIES list and replicated the build processes 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

After putting that in place, I focused on the actual assembly code. I started by writing a program that would just print the word "loop" a certain number of times—in this example, six. This is how my file loop-gas.s appeared:


        .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:


I got precisely six lines of the word "loop," which was my initial task when I ran this application. I then intended to add a feature that would display the current iteration number. I accomplished this by setting aside a tiny portion of the .data file for writing "loop:" and then appending a single digit for the iteration number. I started by using %r15 as the loop counter, added the character code for '0' to convert it to ASCII, and then typed it out. The updated version that prints "loop: 0," "loop: 1," and so on is shown below:

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:



It felt like a minor but important advancement when the output now listed each loop iteration immediately following the word "loop:." The next stage was to display two-digit numbers and increase my loop's range from 0 to 32. To do that, I had to extract the tens and ones' digits by dividing the loop counter by 10. I put the loop counter into %rax, zeroed %rdx, and loaded 10 into another register before calling div because the div instruction in x86_64 utilizes %rax for the dividend and %rdx for the remainder.

Following the division, the one's digit was retained by %rdx and the tens digit by %rax. I then added '0' to convert them to ASCII and put them in a two-character buffer. This code allowed "00," "01," "02," and up to "32" to be shown. Naturally, I also wanted to conceal the leading zero for single-digit integers, so I added a little check that would substitute a space for the tens digit, %rax if it was zero. A cleaner output such as "0," "1," "10," "11," etc., was produced as a result.

At last, I decided to take a risk and print the loop index in hexadecimal. The reasoning was essentially the same, with the exception that I substituted 16 for div and added 7 to go from '9' to 'A' if the result was more than 9. This enabled me to display a range of values from 0 to 1F (31 in decimal), with digits above 9 displayed in capital characters. I was also able to determine whether to print a space or a real hex numeral by making sure the quotient was zero.

4. Adjust the code so that it omits any unnecessary zero at the beginning of single-digit numbers.

By changing the loop code to print out a two-digit number for each iteration—ranging from "00" to "32"—I hoped to expand my knowledge of x86_64 assembly with this job. On AArch64, I had previously configured distinct tens and unit digits using a technique. I chose to divide the loop counter by 10 to determine the two-digit display for the x86_64 version. This method makes use of the division instruction, which divides the iteration counter into a remainder (which represents the one's digit) and a quotient (which represents the tens digit).

The important takeaway here is to zero off %rdx (to get ready for the division), shift the loop counter into %rax (as the div instruction uses %rax as the dividend), and then divide by 10 (stored in %r10). Following the division, the one's digit is held by %rdx and the tens digit by %rax. I then append the character constant for '0' to these numerical values to transform them into their ASCII equivalents before writing them back into a little buffer. The write syscall is then used to output this updated buffer.

.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. Change the code such that the loop counter is reported in hexadecimal. You will alter the division and conversion stages to represent the counter as a hexadecimal number, displaying values like 0, 1, 2, …, up to 1F, rather than presenting the count in decimal.

.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
By contrasting the produced object file with the human-readable source code, I was able to gain a deeper grasp of the x86_64 assembly in this experiment. Seeing how straightforward commands are converted into machine code was fascinating since it showed the inner workings of the CPU and how careful assembly programming is. This experiment made the hidden complexity of computer operations much more real by highlighting how each line of code adds to the final executable and reinforcing the significance of proper syntax and register management.

Comments

Popular posts from this blog

Project Stage 3: Enhancing GCC Pass for Multiple Function Clone Handling: Progress Update

Project Stage 1: Troubleshooting a bit - Coming soon

Lab - 1: Exploring 6502 Assembly