0% found this document useful (0 votes)
28 views

Do Hoang Tu - Operating System From 0 To 1 (2022) - Removed - Removed - Removed

The document discusses how high-level C code is compiled into x86 assembly code. It examines how common operations like data transfer, arithmetic expressions, and logical operations are implemented at the assembly level. Key instructions like mov, add, sub, imul, idiv, neg are analyzed.

Uploaded by

Ahmad Ghofar
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
28 views

Do Hoang Tu - Operating System From 0 To 1 (2022) - Removed - Removed - Removed

The document discusses how high-level C code is compiled into x86 assembly code. It examines how common operations like data transfer, arithmetic expressions, and logical operations are implemented at the assembly level. Key instructions like mov, add, sub, imul, idiv, neg are analyzed.

Uploaded by

Ahmad Ghofar
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 21

X86 ASSEMBLY AND C 1

};

To access a name, we simply adjust the column index11 e.g. names[0], 11


The left index is called column index

names[1]. To access individual character within a name, we use the since it changes the index based on a
row column.

index12 e.g. names[0][0] gives the character “J”, names[0][1] gives the 12
Same with column index, the right

character “o” and so on. index is calledrow index since it


changes the index based on a row.
Without such syntax, we need to create a 20-byte array e.g.
names[20], and whenever we want to access a character e.g. to check if
the names contains with a number in it, we need to calculate the index
manually. It would be distracting, since we constantly need to switch
thinkings be- tween the actual problem and the translate problem.
Since this is a repeating pattern, C abstracts away this problem with
the syntax for define and manipulating multi-dimensional array. Through
this example, we can clearly see the power of abstraction through lan-
guage can give us. It would be ideal if a programmer is equipped with
such power to define whatever syntax suitable for a problem at hands.
Not many languages provide such capacity. Fortunately, through C macro,
we can partially achieve that goal .

In all cases, an array is guaranteed to generate contiguous bytes of mem-


ory, regardless of the dimensions it has.

Exercise 4.8.3. What is the difference between a multi-dimensional ar-


ray and an array of pointers, or even pointers of pointers?

4.9 Examine compiled code

This section will explore how compiler transform high level code into as-
sembly code that CPU can execute, and see how common assembly
pat- terns help to create higher level syntax. -S option is added to
objdump to better demonstrate the connection between high and low
level code.
In this section, the option --no-show-raw-insn is added to objdump
command to omit the opcodes for clarity:

$ objdump --no-show-raw-insn -M intel -S -D <object file> | less


2 OPERATING SYSTEMS : FROM 0 TO 1

4.9.1 Data Transfer


Previous section explores how various types of data are created, and how
they are laid out in memory. Once memory storages are allocated for vari-
ables, they must be accessible and writable. Data transfer instructions
move data (bytes, words, doublewords or quadwords) between memory
and registers, and between registers, effectively read from a storage source
and write to another storage source.
Source
#include <stdint.h>

int32_t i = 0x12345678;

int main(int argc, char *argv[]) {


int j = i;
int k = 0xabcdef;

return 0;
}

Assembly 080483db <main>:


#include <stdint.h>
int32_t i =
0x12345678;
int main(int argc, char *argv[])
{ 80483db: push ebp
80483dc: mov ebp,esp
80483de: sub esp,0x10
int j = i;
80483e1: mov eax,ds:0x804a018
80483e6: mov DWORD PTR [ebp-
0x8],eax
int k = 0xabcdef;
80483e9: mov DWORD PTR [ebp-
0x4],0xabcdef return 0;
80483f0: mov eax,0x0
}
80483f5: leave
X86 ASSEMBLY AND C 3

80483f6: ret
80483f7: xchg ax,ax
80483f9: xchg ax,ax
80483fb: xchg ax,ax
80483fd: xchg ax,ax
80483ff: nop

The general data movement is performed with the mov instruction. Note
that despite the instruction being called mov, it actually copies data
from one destination to another.

The red instruction copies data from the register esp to the register
ebp. This mov instruction moves data between registers and is assigned
the opcode 89.
The blue instructions copies data from one memory location (the i
variable) to another (the j variable). There exists no data movement
from memory to memory; it requires two mov instructions, one for copying
the data from a memory location to a register, and one for copying the
data
from the register to the destination memory location.

The pink instruction copies an immediate value into memory. Finally,


the green instruction copies immediate data into a register.

4.9.2 Expressions
Source
int expr(int i, int j)
{
int add = i +
j; int sub = i
- j; int mul =
i * j; int div
= i / j; int
mod = i % j;
int neg = -i;
int and = i &
j; int or = i |
j; int xor = i
^ j; int not =
~i;
int shl = i << 8;
4 OPERATING SYSTEMS : FROM 0 TO 1

int shr = i >> 8;


char equal1 = (i == j); int equal2 = (i == j); char greater = (i > j); char less = (i < j);
char greater_equal = (i >= j); char less_equal = (i <= j); int logical_and = i && j;
int logical_or = i || j;
++i;
--i;
int i1 = i++; int i2 = ++i; int i3 = i--; int i4 = --i;

return 0;
}

int main(int argc, char *argv[]) {


return 0;
}

Assembly The full assembly listing is really long. For that reason, we ex-
amine expression by expression.

Expression: int add = i + j;

80483e1: mov edx,DWORD PTR [ebp+0x8]


80483e4: mov eax,DWORD PTR [ebp+0xc]
80483e7: add eax,edx
80483e9: mov DWORD PTR [ebp-0x34],eax

The assembly code is straight forward: variable i and j are stored


in eax and edx respectively, then added together with the add in-
struction, and the final result is stored into eax. Then, the result
is saved into the local variable add, which is at the location [ebp-0x34].
X86 ASSEMBLY AND C 5

Expression: int sub = i - j;


80483ec: mov eax,DWORD PTR [ebp+0x8]
80483ef: sub eax,DWORD PTR [ebp+0xc]
80483f2: mov DWORD PTR [ebp-0x30],eax
Similar to add instruction, x86 provides a sub instruction for sub-
traction. Hence, gcc translates a subtraction into sub instruction,
with eax is reloaded with i, as eax still carries the result from pre-
vious expression. Then, j is subtracted from i. After the subtrac-
tion, the value is saved into the variable sub, at location [ebp-0x30].
Expression: int mul = i * j;
80483f5: mov eax,DWORD PTR [ebp+0x8]
80483f8: imul eax,DWORD PTR [ebp+0xc]
80483fc: mov DWORD PTR [ebp-0x34],eax
Similar to sub instruction, only eax is reloaded, since it carries the
result of previous calculation. imul performs signed multiply13. eax 13
Unsigned multiply is perform by mul

is first loaded with i, then is multiplied with j and stored the re- instruction.

sult back into eax, then stored into the variable mul at location [ebp-0x34].
Expression: int div = i / j;
80483ff: mov eax,DWORD PTR
[ebp+0x8] 8048402: cdq
8048403: idiv DWORD PTR [ebp+0xc]
8048406: mov DWORD PTR [ebp-
0x30],eax
Similar to imul, idiv performs sign divide. But, different from imul
above idiv only takes one operand:
1. First, i is reloaded into eax.
2. Then, cdq converts the double word value in eax into a quad-
word value stored in the pair of registers edx:eax, by copying
the signed (bit 31th) of the value in eax into every bit position
in edx. The pair edx:eax is the dividend, which is the variable
i, and the operand to idiv is the divisor, which is the variable
j.
3. After the calculation, the result is stored into the pair edx:eax
registers, with the quotient in eax and remainder in edx. The
quotient is stored in the variable div, at location [ebp-0x30].
6 OPERATING SYSTEMS : FROM 0 TO 1

Expression: int mod = i % j;

8048409: mov eax,DWORD PTR [ebp+0x8]


804840c: cdq
804840d: idiv DWORD PTR [ebp+0xc]
8048410: mov DWORD PTR [ebp-0x2c],edx

The same idiv instruction also performs the modulo operation, since
it also calculates a remainder and stores in the variable mod, at lo-
cation [ebp-0x2c].

Expression: int neg = -i;

8048413: mov eax,DWORD PTR [ebp+0x8]


8048416: neg eax
8048418: mov DWORD PTR [ebp-0x28],eax

neg replaces the value of operand (the destination operand) with


its two’s complement (this operation is equivalent to subtracting
the operand from 0). In this example, the value i in eax is replaced
replaced with -i using neg instruction. Then, the new value is stored
in the variable neg at [ebp-0x28].

Expression: int and = i & j;

804841b: mov eax,DWORD PTR [ebp+0x8]


804841e: and eax,DWORD PTR [ebp+0xc]
8048421: mov DWORD PTR [ebp-0x24],eax

and performs a bitwise AND operation on two operands, and stores


the result in the destination operand, which is the variable and at
[ebp-0x24].

Expression: int or = i | j;

8048424: mov eax,DWORD PTR [ebp+0x8]


8048427: or eax,DWORD PTR [ebp+0xc]
804842a: mov DWORD PTR [ebp-0x20],eax

Similar to and instruction, or performs a bitwise OR operation on


two operands, and stores the result in the destination operand, which
is the variable or at [ebp-0x20] in this case.

Expression: int xor = i ^ j;


X86 ASSEMBLY AND C 7

804842d: mov eax,DWORD PTR [ebp+0x8]


8048430: xor eax,DWORD PTR [ebp+0xc]
8048433: mov DWORD PTR [ebp-0x1c],eax

Similar to and/or instruction, xor performs a bitwise XOR opera-


tion on two operands, and stores the result in the destination operand,
which is the variable xor at [ebp-0x1c].

Expression: int not = ~i;

8048436: mov eax,DWORD PTR [ebp+0x8]


8048439: not eax
804843b: mov DWORD PTR [ebp-0x18],eax

not performs a bitwise NOT operation (each 1 is set to 0, and each


0 is set to 1) on the destination operand and stores the result in
the destination operand location, which is the variable not at [ebp-0x18].

Expression: int shl = i << 8;

804843e: mov eax,DWORD PTR


[ebp+0x8] 8048441: shl eax,0x8
8048444: mov DWORD PTR [ebp-0x14],eax

shl (shift logical left) shifts the bits in the destination operand to
the left by the number of bits specified in the source operand. In
this case, eax stores i and shl shifts eax by 8 bits to the left. A
dif- ferent name for shl is sal ( shift arithmetic left). Both can be
used
synonymous. Finally, the result is stored in the variable shl at [ebp-0x14].
Here is a visual demonstration of shl/sal and shr instructions:
After shifting to the left, the right most bit is set for Carry Flag in
EFLAGS register.

Expression: int shr = i >> 8;

8048447: mov eax,DWORD PTR


[ebp+0x8] 804844a: sar eax,0x8
804844d: mov DWORD PTR [ebp-0x10],eax

sar is similar to shl/sal, but shift bits to the right and extends
the sign bit. For right shift, shr and sar are two different
instruc-
tions. shr differs to sar is that it does not extend the sign bit. Finally,
the result is stored in the variable shr at [ebp-0x10].
8 OPERATING SYSTEMS : FROM 0 TO 1

Initial State
Initial State
CF
CF
X
10001000100010001000100010001 10001000100010001000100010001
X
After 1-bit SHL/SAL instruction

After 1-bit SHR instruction

1 01000100010001000100010001000111 1
0 0
00010001000100010001000100011

After 1-bit SHL/SAL instruction After 10-bit SHR instruction

0 00000000001000100010001000100010 0
0 0
01000100010001000111100000000

(a) SHL/SAL (Source: Figure 7-6, Volume 1) (b) SHR (Source: Figure 7-7, Volume 1)

Figure 4.9.1: Shift Instructions


In the figure 4.9.1(b), notice that initially, the sign bit is 1, but af- (red is the start bit, blue is the end
bit.)
ter 1-bit and 10-bit shiftings, the shifted-out bits are filled with
ze- ros.
Initial State (Positive Operand)

Operand CF
X Figure 4.9.2: SAR Instruction
0100010001000100010001000100011 Operation (Source: Figure 7-8,
Volume 1)
After 1-bit SAR instruction

00100010001000100010001000100011
1

Initial State (Negative


Operand)
Operand
11000100010001000100010001000111 X

After 10-bit SAR instruction

11100010001000100010001000100011
1

With sar, the sign bit (the most significant bit) is preserved. That
is, if the sign bit is 0, the new bits always get the value 0; if the sign
bit is 1, the new bits always get the value 1.

Expression: char equal1 = (i == j);

8048450: mov eax,DWORD PTR [ebp+0x8]


8048453: cmp eax,DWORD PTR [ebp+0xc]
8048456: sete al
8048459: mov BYTE PTR [ebp-0x41],al
X86 ASSEMBLY AND C 9

cmp and variants of the variants of set instructions make up all the
logical comparisons. In this expression, cmp compares variable i and
j; then sete stores the value 1 to al register if the comparison
from cmp earlier is equal, or stores 0 otherwise. The general name for
vari- ants of set instruction is called SETcc. The suffix cc denotes the
condition being tested for in EFLAGS register. Appendix B in vol-
ume 1, “EFLAGS Condition Codes”, lists the conditions it is possi-
ble to test for with this instruction. Finally, the result is stored in
the variable equal1 at [ebp-0x41].

Expression: int equal2 = (i == j);

804845c: mov eax,DWORD PTR [ebp+0x8]


804845f: cmp eax,DWORD PTR [ebp+0xc]
8048462: sete al
8048465: movzx eax,al
8048468: mov DWORD PTR [ebp-0xc],eax

Similar to equality comparison, this expression also compares for


equality, with an exception that the result is stored in an int type.
For that reason, one more instruction is a added: movzx instruction,
a variant of mov that copies the result into a destination operand
and fills the remaining bytes with 0. In this case, since eax is 4-byte
wide, after copying the first byte in al, the remaining bytes of eax
are filled with 0 to ensure the eax carries the same value as al.

Figure 4.9.3: movzx instruction


12 34 56 78 00 00 00 78
(a) eax before movzx (b) after movzx eax, al

Expression: char greater = (i > j);

804846b: mov eax,DWORD PTR [ebp+0x8]


804846e: cmp eax,DWORD PTR [ebp+0xc]
8048471: setg al
8048474: mov BYTE PTR [ebp-0x40],al

Similar to equality comparison, but used setg for greater compari-


son instead.

Expression: char less = (i < j);

8048477: mov eax,DWORD PTR [ebp+0x8]


10 OPERATING SYSTEMS : FROM 0 TO 1

804847a: cmp eax,DWORD PTR [ebp+0xc]


804847d: setl al
8048480: mov BYTE PTR [ebp-0x3f],al

Applied setl for less comparison.

Expression: char greater_equal = (i >= j);

8048483: mov eax,DWORD PTR [ebp+0x8]


8048486: cmp eax,DWORD PTR [ebp+0xc]
8048489: setge al
804848c: mov BYTE PTR [ebp-0x3e],al

Applied setge for greater or equal comparison.

Expression: char less_equal = (i <= j);

804848f: mov eax,DWORD PTR [ebp+0x8]


8048492: cmp eax,DWORD PTR [ebp+0xc]
8048495: setle al
8048498: mov BYTE PTR [ebp-0x3d],al

Applied setle for less than or equal comparison.

Expression: int logical_and = (i && j);

804849b: cmp DWORD PTR [ebp+0x8],0x0


804849f: je 80484ae <expr+0xd3>
80484a1: cmp DWORD PTR [ebp+0xc],0x0
80484a5: je 80484ae <expr+0xd3>
80484a7: mov eax,0x1
80484ac: jmp 80484b3 <expr+0xd8>
80484ae: mov eax,0x0
80484b3: mov DWORD PTR [ebp-0x8],eax

Logical AND operator && is one of the syntaxes that is made entirely
in software14 with simpler instructions. The algorithm from the as- 14
That is, there is no equivalent assem-

sembly code is simple: bly instruction implemented in hard-


ware.
1. First, check if i is 0 with the instruction at 0x804849b.

(a) If true, jump to 0x80484ae and set eax to 0.


(b) Set the variable logical_and to 0, as it is the next instruc-
tion after 0x80484ae.
X86 ASSEMBLY AND C 11

2. If i is not 0, check if j is 0 with the instruction at 0x80484a1.


(a) If true, jump to 0x80484ae and set eax to 0.
(b) Set the variable logical_and to 0, as it is the next instruc-
tion after 0x80484ae.
3. If both i and j are not 0, the result is certainly 1, or true.
(a) Set it accordingly with the instruction at 0x80484a7.
(b) Then jump to the instruction at 0x80484b3 to set the
vari- able logical_and at [ebp-0x8] to 1.

Expression: int logical_or = (i || j);

80484b6: cmp DWORD PTR [ebp+0x8],0x0


80484ba: jne 80484c2 <expr+0xe7>
80484bc: cmp DWORD PTR
[ebp+0xc],0x0 80484c0: je 80484c9
<expr+0xee> 80484c2: mov eax,0x1
80484c7: jmp 80484ce
<expr+0xf3> 80484c9: mov eax,0x0
80484ce: mov DWORD PTR [ebp-0x4],eax

Logical OR operator || is similar to logical and above. Understand


the algorithm is left as an exercise for readers.

Expression: ++i; and --i; (or i++ and i--)

80484d1: add DWORD PTR [ebp+0x8],0x1


80484d5: sub DWORD PTR [ebp+0x8],0x1

The syntax of increment and decrement is similar to logical AND and


logical OR in that it is made from existing instruction, that is add.
The difference is that the CPU actually does has a built-in instruc-
tion, but gcc decided not to use the instruction because inc and
dec cause a partial flag register stall, occurs when an instruction
modifies a part of the flag register and the following instruction is
dependent on the outcome of the flags (section 3.5.2.6, Intel Optimization
Manual, 2016b). The manual even suggests that inc and dec should
be replaced with add and sub instructions (section 3.5.1.1, Intel Optimization
Manual, 2016b).

Expression: int i1 = i++;


12 OPERATING SYSTEMS : FROM 0 TO 1

80484d9: mov eax,DWORD PTR [ebp+0x8]


80484dc: lea edx,[eax+0x1]
80484df: mov DWORD PTR [ebp+0x8],edx
80484e2: mov DWORD PTR [ebp-0x10],eax

First, i is copied into eax at 80484d9. Then, the value of eax + 0x1
is copied into edx as an effective address at 80484dc. The lea (load
effective address) instruction copies a memory address into a reg-
ister. According to Volume 2, the source operand is a memory ad-
dress specified with one of the processors addressing modes. This
means, the source operand must be specified by the addressing modes
defined in 16-bit/32-bit ModR/M Byte tables, 4.5.1 and 4.5.2.
After loading the incremented value into edx, the value of i is in-
creased by 1 at 80484df. Finally, the previous i value is stored back
to i1 at [ebp-0x8] by the instruction at 80484e2.

Expression: int i2 = ++i;

80484e5: add DWORD PTR [ebp+0x8],0x1


80484e9: mov eax,DWORD PTR [ebp+0x8]
80484ec: mov DWORD PTR [ebp-0xc],eax

The primary differences between this increment syntax and the pre-
vious one are:

D add is used instead of lea to increase i directly.


D the newly incremented i is stored into i2 instead of the old value.
D the expression only costs 3 instructions instead of 4.

This prefix-increment syntax is faster than the post-fix one used


previously. It might not matter much which version to use if the
increment is only used once or a few hundred times in a small loop,
but it matters when a loop runs millions or more times. Also, de-
pends on different circumstances, it is more convenient to use
one over the other e.g. if i is an index for accessing an array, we
want to use the old value for accessing previous array element and
newly incremented i for current element.

Expression: int i3 = i--;

80484ef: mov eax,DWORD PTR [ebp+0x8]


X86 ASSEMBLY AND C 13

80484f2: lea edx,[eax-0x1]


80484f5: mov DWORD PTR [ebp+0x8],edx
80484f8: mov DWORD PTR [ebp-0x8],eax

Similar to i++ syntax, and is left as an exercise to readers.

Expression: int i4 = --i;

80484fb: sub DWORD PTR [ebp+0x8],0x1


80484ff: mov eax,DWORD PTR [ebp+0x8]
8048502: mov DWORD PTR [ebp-0x4],eax

Similar to ++i syntax, and is left as an exercise to readers.

Exercise 4.9.1. Read section 3.5.2.4, “Partial Register Stalls” to un-


derstand register stalls in general.

Exercise 4.9.2. Read the sections from 7.3.1 to 7.3.7 in volume 1.

4.9.3 Stack
A stack is a contiguous array of memory locations that holds a
collection of discrete data. When a new element is added, a stack
grows down in memory toward lesser addresses, and shrinks up toward
greater addresses when an element is removed. x86 uses the esp register
to point to the top of the stack, at the newest element. A stack can be
originated any- where in main memory, as esp can be set to any
memory address. x86 provides two operations for manipulating
stacks:

D push instruction and its variants add a new element on top of the stack

D pop instructions and its variants remove the top-most element from
the stack.

0x10000 00 0x10000 00 0x10000 00


0x10001 00 0x10001 00 0x10001 00
0x10002 00 0x10002 78 ← 0x10002 00
es
0x10003 00 0x10003 56 0x10003 00
0x10004 12 ← 0x10004 12 0x10004 12 ← es
es
(b) After (c) After executing pop word
(a) Initial state at address 0x10004 executing push word
0x5678
Figure 4.9.4: Stack operations
14 OPERATING SYSTEMS : FROM 0 TO 1

4.9.4 Automatic variables


Local variables are variables that exist within a scope. A scope is
delim- ited by a pair of braces: {..}. The most common scope to define
local variables is at function scope. However, scope can be unnamed, and
vari- ables created inside an unnamed scope do not exist outside of its
scope and its inner scope.

Example 4.9.1. Function scope:


void foo() { int a; int b;
}

a and b are variables local to the function foo.

Example 4.9.2. Unnamed scope:


int foo() {
int i;

{
int a = 1;
int b = 2;
{
return i = a + b;
}
}
}

a and b are local to where it is defined and local into its inner child
scope that return i = a + b. However, they do not exist at the
function scope that creates i.

When a local variable is created, it is pushed on the stack; when a lo-


cal variable goes out of scope, it is pop out of the stack, thus destroyed.
When an argument is passed from a caller to a callee, it is pushed on
the stack; when a callee returns to the caller, the arguments are popped
out
X86 ASSEMBLY AND C 15

the stack. The local variables and arguments are automatically allocated
upon enter a function and destroyed after exiting a function, that’s
why it’s called automatic variables.

A base frame pointer points to the start of the current function


frame, and is kept in ebp register. Whenever a function is called, it is
allocated with its own dedicated storage on stack, called stack frame. A
stack frame is where all local variables and arguments of a function are
placed on a
stack15. 15
Data and only data are exclusively
allocatedon stackforevery stack
When a function needs a local variable or an argument, it uses ebp frame. No code resides here.
to access a variable:

D All local variables are allocated after the ebp pointer. Thus, to access
a local variable, a number is subtracted from ebp to reach the loca-
tion of the variable.

D All arguments are allocated before ebp pointer. To access an argument,


a number is added to ebp to reach the location of the argument.

D The ebp itself pointer points to the return address of its caller.

Previous Frame Current Frame


Function Arguments ebp Local variables
A1 A2 A3 ........ An Return Address Old ebp L1 L2 L3 ........ Ln
Figure 4.9.5: Function arguments
A = Argument and local variables
L = Local Variable
Here is an example to make it more concrete:
Source
int add(int a, int b) {
int i = a + b;

return i;
}

Assembly 080483db
<add>: #include
<stdint.h>
int add(int a, int b) {
80483db: push ebp
16 OPERATING SYSTEMS : FROM 0 TO 1

80483dc: mov ebp,esp


80483de: sub esp,0x10
int i = a + b;
80483e1: mov edx,DWORD PTR [ebp+0x8]
80483e4: mov eax,DWORD PTR [ebp+0xc]
80483e7: add eax,edx
80483e9: mov DWORD PTR [ebp-
0x4],eax return i;
80483ec: mov eax,DWORD PTR [ebp-0x4]
}
80483ef: leave
80483f0: ret

In the assembly listing, [ebp-0x4] is the local variable i, since it is allo-


cated after ebp, with the length of 4 bytes (an int). On the other hand,
a and b are arguments and can be accessed with ebp:

D [ebp+0x8] accesses a.

D [ebp+0xc] access b.

For accessing arguments, the rule is that the closer a variable on stack
to ebp, the closer it is to a function name.

↓ ↓ ↓ ↓
ebp+0x ebp+0x ebp+0x e
00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
0x10000 b a Return Address Old ebp

↓ ↓
ebp+0x ebp+0x
00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
0xffe0 N i
Figure 4.9.6: Function arguments
N = Next local variable starts here and local variables in memory

From the figure, we can see that a and b are laid out in memory
with the exact order as written in C, relative to the return address.

4.9.5 Function Call and Return


X86 ASSEMBLY AND C 17

Source
#include <stdio.h>

int add(int a, int b) {


int local = 0x12345;

return a + b;
}

int main(int argc, char *argv[]) {


add(1,1);

return 0;
}

Assembly For every function call, gcc pushes arguments on the stack in
reversed order with the push instructions. That is, the arguments pushed
on stack are in reserved order as it is written in high level C code, to
ensure the relative order between arguments, as seen in previous sec-
tion how function arguments and local variables are laid out. Then,
gcc generates a call instruction, which then implicitly pushes a re-
turn address before transferring the control to add function:

080483f2 <main>:
int main(int argc, char *argv[])
{ 80483f2: push ebp
80483f3: mov ebp,esp
add(1,2);
80483f5: push 0x2
80483f7: push 0x1
80483f9: call 80483db
<add> 80483fe: add esp,0x8
return 0;
8048401: mov eax,0x0
}
8048406: leave
18 OPERATING SYSTEMS : FROM 0 TO 1

8048407: ret

Upon finishing the call to add function, the stack is restored by adding
0x8 to stack pointer esp (which is equivalent to 2 pop instructions).
Finally, a leave instruction is executed and main returns with a ret
instruction.
A ret instruction transfers the program execution back to the caller to
the instruction right after the call instruction, the add instruction. The
reason ret can return to such location is that the return address implic-
itly pushed by the call instruction, which is the address right after the
call instruction; whenever the CPU executes ret instruction, it retrieves
the return address that sits right after all the arguments on the stack:

At the end of a function, gcc places a leave instruction to clean up


all spaces allocated for local variables and restore the frame pointer to
frame pointer of the caller.

080483db <add>:
#include <stdio.h>
int add(int a, int b) {
80483db: push ebp
80483dc: mov ebp,esp
80483de: sub esp,0x10
int local = 0x12345;
80483e1: DWORD PTR [ebp-0x4],0x12345
return a + b;
80483e8: mov edx,DWORD PTR [ebp+0x8]
80483eb: mov eax,DWORD PTR [ebp+0xc]
80483ee: add eax,edx
}
80483f0: leave
80483f1: ret

Exercise 4.9.3. The above code that gcc generated for function call-
ing is actually the standard method x86 defined. Read chapter 6, “Produce
Calls, Interrupts, and Exceptions”, Intel manual volume 1.
X86 ASSEMBLY AND C 19

4.9.6 Loop
Loop is simply resetting the instruction pointer to an already executed
instruction and starting from there all over again. A loop is just one ap-
plication of jmp instruction. However, because looping is a pervasive pat-
tern, it earned its own syntax in C.
Source
#include <stdio.h>

int main(int argc, char *argv[]) {


for (int i = 0; i < 10; i++) {
}

return 0;
}

Assembly 080483db
<main>: #include
<stdio.h>
int main(int argc, char *argv[])
{ 80483db: push ebp
80483dc: mov ebp,esp
80483de: sub esp,0x10
for (int i = 0; i < 10; i++)
{
80483e1: mov DWORD PTR [ebp-0x4],0x0
80483e8: jmp 80483ee <main+0x13>
80483ea: add DWORD PTR [ebp-
0x4],0x1 80483ee: cmp DWORD PTR [ebp-
0x4],0x9 80483f2: jle 80483ea
<main+0xf>
}
return 0;
80483f4: b8 00 00 00 00 mov eax,0x0
}
80483f9: c9 leave
80483fa: c3 ret
80483fb: 66 90 xchg ax,ax
80483fd: 66 90 xchg ax,ax
20 OPERATING SYSTEMS : FROM 0 TO 1

80483ff: 90 nop

The colors mark corresponding high level code to assembly code:

1. The red instruction initialize i to 0.

2. The green instructions compare i to 10 by using jle and


compare it to 9. If true, jump to 80483ea for another iteration.

3. The blue instruction increase i by 1, making the loop able to ter-


minate once the terminate condition is satisfied.

Exercise 4.9.4. Why does the increment instruction (the blue instruc-
tion) appears before the compare instructions (the green instructions)?

Exercise 4.9.5. What assembly code can be generated for while and
do...while?

4.9.7 Conditional
Again, conditional in C with if...else... construct is just another ap-
plication of jmp instruction under the hood. It is also a pervasive pat-
tern that earned its own syntax in C.
Source
#include <stdio.h>

int main(int argc, char *argv[]) {


int i = 0;

if (argc) {
i = 1;
} else {
i = 0;
}

return 0;
}

Assembly int main(int argc, char *argv[])


{ 80483db: push ebp
X86 ASSEMBLY AND C 21

80483dc: mov ebp,esp


80483de: sub esp,0x10
int i = 0;
80483e1: mov DWORD PTR [ebp-0x4],0x0
if (argc) {
80483e8: cmp DWORD PTR [ebp+0x8],0x0
80483ec: je 80483f7
<main+0x1c> i = 1;
80483ee: mov DWORD PTR [ebp-
0x4],0x1 80483f5: jmp 80483fe
<main+0x23>
} else {
i = 0;
80483f7: mov DWORD PTR [ebp-0x4],0x0
}
return 0;
80483fe: mov eax,0x0
}
8048403: leave
8048404: ret
The generated assembly code follows the same order as the correspond-
ing high level syntax:

D red instructions represents if branch.


D blue instructions represents else branch.
D green instruction is the exit point for both if and else branch.

if branch first compares whether argc is false (equal to 0) with cmp


instruction. If true, it proceeds to else branch at 80483f7. Otherwise,
if branch continues with the code of its branch, which is the next in-
struction at 80483ee for copying 1 to i. Finally, it skips over else
branch and proceeds to 80483fe, which is the next instruction pasts
the if..else... construct.
else branch is entered when cmp instruction from if branch is true.
else branch starts at 80483f7, which is the first instruction of else
branch. The instruction copies 0 to i, and proceeds naturally to the
next instruction pasts the if...else... construct without any jump.

You might also like