SPO Lab 3 - Assembly
In this lab, I am going to dive into basics of assembly language for both x86_64 and ARM architectures. Simple loop structure will be used to print a message with incremented integer value.
Inserting digit into a string
The first task is to add zero integers to the loop message:
Loop: 0 Loop: 0 Loop: 0 ..
Lets start by storing ‘0’ as ASCII code in a register
mov $0x30, %r10 /* init to 0 in ASCII */
We can add ’0’ to the “Loop : “ message by substituting ‘space’ character with our zero digit. I defined position of that space character in .data section as :
.set fd , msg + 6 /* Position of our first digit ("Loop: <digit>") */
We need to move only one byte of our zero code into $msg at position fd by using
b postfix with
%r0 register (If you move the whole thing,
\n will be overwritten as well):
mov %r10b, fd
Full Code : part1_x86_64
Now, lets increment the zero digit by adding loop counter to it. I defined loop counter as register
%r15 at the beginning :
_start: mov $start, %r15 /* loop index */
Which gets incremented in the loop body:
inc %r15 /* increment index */ cmp $max, %r15 /* see if we're done */ jne loop /* loop if we're not */
We can add the counter to our zero ASCII digit and display it using logic described in previous section :
add %r15, %r10 mov %r10b, fd
This should result in :
Loop: 0 Loop: 1 Loop: 2 Loop: 3 Loop: 4 Loop: 5 Loop: 6 Loop: 7 Loop: 8 Loop: 9
Full Code: part2_x86_64
The goal is to split counter into two digits so we can insert them separately into the message string using 2 registers. One way to do this is to divide counter by 10 and store remainder as our second digit and result of the division as our first digit:
e.g : 13 / 10 should yield 1 as quotient and 3 as remainder.
Take a look at division instruction in x86_64:
Unlike add or sub instructions, div takes only one argument. It divides
%rax by given register (
%r10 in this case) and places quotient (result of division operation) into
%rax and remainder into
%rdx (which should be initiated to zero prior to division).
Let’s translate the logic into assembly code:
- initiate our
%raxregisters as well as setting the divisor to 10 :
mov $0 , %rdx /* init remainder */ mov %r15 , %rax /* place counter (divident) */ mov $10 , %r14 /* place divisor */
- Dividing counter by 10 and adding the results (quotient & remainder) to the first and second digit :
div %r14 /* Add result of division and remainder */ add %rax, %r10 add %rdx, %r11 /* Add numbers to our string output */ mov %r10b, fd mov %r11b, sd
Where sd is the position of our second digit:
.set sd , fd + 1 /* Position of our second digit = pos of 1st digit + 1 */
This will, however, print zeroes in front of 1-digit number :
Loop: 00 Loop: 01 Loop: 02 Loop: 03 Loop: 04 Loop: 05 Loop: 06 Loop: 07 Loop: 08 Loop: 09 Loop: 10 Loop: 11 Loop: 12 ... ...
In order to get rid of zeros, additional logic needs to be added:
If counter is less than 10, remainder will we be stored in the first digit and the second digit will be space (
$0x20 hex stands for space char in ASCII).
cmp $10 , %r15 jl shift /* Add result of division and remainder */ add %rax, %r10 add %rdx, %r11 jmp printl shift: add %rdx, %r10 mov $0x20, %r11 printl: /* Add number to our string: */ mov %r10b, fd mov %r11b, sd /* Print Message to the stdout */ mov $len, %rdx /* message length */ mov $msg, %rsi ... ...
Full Code (with zeros) : part3_x86_64
Full Code (final) : part4_x86_64
Aarch 64 Version:
I found Aarch code to be very similar to x86_64 version. There are couple of differences related to register names, order of registers in instructions and even instructions themselves.
For example, getting remainder and quotient in x86_64 took only one line, while aarch required 2 operations :
/* Getting quotient */ udiv w0, w15, w2 /* Getting remainder */ msub w3, w0, w2, w15
Overall, translating one code to another seems to be pretty intuitive.
Links to code snippets in aarch:
Print Zeros : part1
Print 0-10 : part2
Print 0-30 (with zeros) : part3
Print 0-30 (final) : part4
Debugging in assembly can be quite hard; most of the time I’d get ambiguous seg-faults which required commenting code out (I wonder if something like GDB can be used with it). Unlike other languages, assembly requires you to spell every instruction out to it. I found x86_64 a little bit less intuitive, although it might be just because it was the first one I attempted to tackle. Overall, I can’t say one assembler version excites me more than another.