Comparing C to Machine Language

Isshiki🐈
4 min readDec 25, 2020

--

Based on Ben Eater’s YouTube video, Comparing C to machine language

Let’s say we have a piece of code that can generate a list of fibonacci numbers, like

#include <stdio.h>int main()
{
int x, y, z;
while (1) {
x = 0;
y = 1;
do {
printf("%d\n", x);
z = x + y;
x = y;
y = z;
} while (x < 255);
}
}

To get the corresponding assembly code, I compile this piece of code using gcc -O0 -o fib fib.c and analyse the generated executable file using otool -tv fib and the result is like

fib:
(__TEXT,__text) section
_main:
0000000100003f30 pushq %rbp
0000000100003f31 movq %rsp, %rbp
0000000100003f34 subq $0x10, %rsp
0000000100003f38 movl $0x0, -0x4(%rbp)
0000000100003f3f movl $0x0, -0x8(%rbp)
0000000100003f46 movl $0x1, -0xc(%rbp)
0000000100003f4d movl -0x8(%rbp), %esi
0000000100003f50 leaq 0x4f(%rip), %rdi
0000000100003f57 movb $0x0, %al
0000000100003f59 callq 0x100003f86
0000000100003f5e movl -0x8(%rbp), %ecx
0000000100003f61 addl -0xc(%rbp), %ecx
0000000100003f64 movl %ecx, -0x10(%rbp)
0000000100003f67 movl -0xc(%rbp), %ecx
0000000100003f6a movl %ecx, -0x8(%rbp)
0000000100003f6d movl -0x10(%rbp), %ecx
0000000100003f70 movl %ecx, -0xc(%rbp)
0000000100003f73 cmpl $0xff, -0x8(%rbp)
0000000100003f7a jl 0x100003f4d
0000000100003f80 jmp 0x100003f3f

The moment we run our program, our program is loaded into the memory: our instructions are read by the CPU one by one (as pointed to by the instruction pointer) and data will be read into various registers temporarily and then stored in an internal stack (as depicted below) for later use.

The assembly code above can then be split into two parts. The first part of the code is a function prologue and it’s used mainly to prepare the CPU and registers for a new program.

0000000100003f30 pushq %rbp
0000000100003f31 movq %rsp, %rbp
0000000100003f34 subq $0x10, %rsp

In this case, it does three things: First, it pushes the content of the base pointer rbp to the internal stack used by the CPU (at this point, the base pointer store the position of the beginning of the caller’s stack frame); second, it updates rbp with the content of rsp, which always stores the top of the stack, so rbp now stores the location of the beginning of the stack frame of our program instead of the program that invokes our program. Finally, there is an subq $0x10, %rsp, which allocates necessary space for local variables. I don’t know why it’s $0x10 since gcc -S fib.c -O0 uses $16 instead, which is exactly the space necessary for three local variables (12 bytes) plus some extra space it needs (4 bytes for an extra integer).

After the prologue is the assembly version of the code we wrote. At first, we declare three variables and accordingly the first three lines puts our three variables into the internal stack. We should see that, though I have used only a very low level of optimization -O0, the assembly code has combined int x, y, z; and x = 0; and y = 0;

0000000100003f38 movl $0x0, -0x4(%rbp)     # x
0000000100003f3f movl $0x0, -0x8(%rbp) # z
0000000100003f46 movl $0x1, -0xc(%rbp) # y

After variable declarations, the following four lines is about using printf. At first, it fetches the value of x from the stack and stores it in the esi index register for immediate and temporary use; then it fetches the format string from memory; then, it sets return value to zero; and finally, it calls printf from 0x100003f86, which I guess is the location of printf in the shared library, glibc.

0000000100003f4d movl -0x8(%rbp), %esi
0000000100003f50 leaq 0x4f(%rip), %rdi
0000000100003f57 movb $0x0, %al
0000000100003f59 callq 0x100003f86

Then there are more assignments. Line 1 moves z to a temporary general register ecx, line 2 adds the value of y to ecx, and line 3 stores the content of ecx to -0x10(%rbp) (it’s weird since this is between -0x8(%rbp) and -0xc(%rbp) and gcc -S fib.c -O0 uses $16 instead). Line 4 and 5 store the value of y to ecx and store the content of ecx to -0xc(%rbp), in effect assigning y to x. The last two lines, line 6 and 7, store the content we just set aside in -0x10(%rbp) (or -0x16(%rbp)) back to the temporary register ecx and to y.

0000000100003f5e movl -0x8(%rbp), %ecx
0000000100003f61 addl -0xc(%rbp), %ecx
0000000100003f64 movl %ecx, -0x10(%rbp) # or -0x16(%rbp)
0000000100003f67 movl -0xc(%rbp), %ecx
0000000100003f6a movl %ecx, -0x8(%rbp)
0000000100003f6d movl -0x10(%rbp), %ecx # or -0x16(%rbp)
0000000100003f70 movl %ecx, -0xc(%rbp)

The last part of our assembly code is about our control statement do {} while (x < 255);. In line 1, cmpl compares $0xff, which is 255 in hexadecimal, with x and store the result (condition code) in a condition code register, CCR (which is not one of the sixteen integer registers). And jl will fetch the result from CCR and if x is less than $0xff, and it will continue to run 0x100003f4d. Otherwise, it will continue to run 0x100003f3f.

0000000100003f73 cmpl $0xff, -0x8(%rbp)
0000000100003f7a jl 0x100003f4d
0000000100003f80 jmp 0x100003f3f

That is

fib:
(__TEXT,__text) section
_main:
0000000100003f30 pushq %rbp
0000000100003f31 movq %rsp, %rbp
0000000100003f34 subq $0x10, %rsp
0000000100003f38 movl $0x0, -0x4(%rbp)
0000000100003f3f movl $0x0, -0x8(%rbp) < if x is not less than $0xff
0000000100003f46 movl $0x1, -0xc(%rbp)
0000000100003f4d movl -0x8(%rbp), %esi < if x is less than $0xff
0000000100003f50 leaq 0x4f(%rip), %rdi
......

--

--

Isshiki🐈
Isshiki🐈

No responses yet