Assembly programming allows us absolute control over hardware, but advancing in this language is a very slow process. Also, our code is so specific to the CPU we are using that it makes it less portable to other architectures. For this reason, we will use C for most of our OS code. In this article, we will see how C manages the stack and variable definition.
Before we start, it is recommended that you read these previous articles:
- Boot Sector
- Interrupts
- Memory
- Stack
- IF-ELSE
- Functions
- Memory Segmentation
- Reading Data from Disk
- Entering Protected Mode 32bits
In my case, I am using FreeBSD, and the compiler is the following:
FreeBSD clang version 8.0.1 (tags/RELEASE_801/final 366581) (based on LLVM 8.0.1)
Target: x86_64-unknown-freebsd12.1
Thread model: posix
InstalledDir: /usr/bin
The best way to understand how a compiler works is by compiling a small program and seeing the result:
int my_function () {
return 0xbaba ;
}
We compile the object file, which will contain the textual labels:
We dump the assembly code of the object:
basic.o: file format elf64-x86-64-freebsd
Disassembly of section .text:
0000000000000000 <my_function>:
0: 55 push rbp
1: 48 89 e5 mov rbp,rsp
4: b8 ba ba 00 00 mov eax,0xbaba
9: 5d pop rbp
a: c3 ret
As we can see, the steps to execute are:
- Save the base value of the previous function’s stack
- Adjust our base to the current top of the stack, thus creating a new stack frame for our function
- Move the value 0xbaba to the ax register, where the upper function expects the result
- Restore the value of the previous function’s stack base
- Return
To generate a binary with all the object files, we will need a linker, which will convert all relative memory addresses of the object files to absolute addresses.
For example, call <label_X> will be translated to call 0x12345, where 0x12345 is the offset within the binary where the linker decided to put the code of the routine with label_X, the textual labels disappear.
We link the object files to form the final binary:
The linker can generate output in various formats. Depending on the output format, the binary will preserve metadata about how it should be loaded into memory, information about textual labels to help in software debugging.
By using the -Ttext 0x0 option, we are making all addresses have the base indicated by that parameter, which is equivalent to use the org ASM instruction that we saw . For now, this is not very important, but it will be when we load our kernel from the bootloader.
A binary can be disassembled into assembly code. The only drawback of obtaining assembly code from the binary is that some parts are data and will be displayed as ASM instructions. In our example code, we will not have this problem.
cat basic.dis
00000000 55 push ebp
00000001 48 dec eax
00000002 89E5 mov ebp,esp
00000004 B8BABA0000 mov eax,0xbaba
00000009 5D pop ebp
0000000A C3 ret
0000000B 0000 add [eax],al
0000000D 0000 add [eax],al
0000000F 00 db 0x00
Basically, it is the same code that we obtained from the object file. The linking has added some extra steps. For the curious, the first column (00000000) shown is the offset where the instructions will reside in memory.
Let’s now give an example with local variables in the function:
int my_function () {
int my_var = 0xbaba ;
return my_var ;
}
objdump -M intel -d local_var.o
local_var.o: file format elf64-x86-64-freebsd
Disassembly of section .text:
0000000000000000 <my_function>:
0: 55 push rbp
1: 48 89 e5 mov rbp,rsp
4: c7 45 fc ba ba 00 00 mov DWORD PTR [rbp-0x4],0xbaba
b: 8b 45 fc mov eax,DWORD PTR [rbp-0x4]
e: 5d pop rbp
f: c3 ret
The process is very similar to the previous one:
- We save the previous bp
- We adjust the new bp to sp
- We move the value 0xbaba to the memory position bp-4 (the stack grows downwards)
- We save the return value in the ax register
- We restore the previous bp
- Return
To debug it in a more interactive way, we can program a complete code with its main() function and load it into
GDB-gef
:
int my_function();
int main(int argc, char *argv[])
{
my_function();
}
int my_function() {
int my_var = 0xbaba;
return my_var;
}
gdb -q ./test
GEF for freebsd ready, type `gef' to start, `gef config' to configure
80 commands loaded for GDB 9.2 [GDB v9.2 for FreeBSD] using Python engine 3.7
Reading symbols from ./test...
Let’s see the source code in C:
gef➤ l
1 int main(int argc, char *argv[])
2 {
3 my_function();
4 }
5
6 int my_function() {
7 int my_var = 0xbaba;
8 return my_var;
9 }
10
Let’s see the ASM code of our function:
gef➤ disassemble my_function
Dump of assembler code for function my_function:
0x0000000000201310 <+0>: push rbp
0x0000000000201311 <+1>: mov rbp,rsp
0x0000000000201314 <+4>: mov DWORD PTR [rbp-0x4],0xbaba
0x000000000020131b <+11>: mov eax,DWORD PTR [rbp-0x4]
0x000000000020131e <+14>: pop rbp
0x000000000020131f <+15>: ret
End of assembler dump.
We put a breakpoint just before calling our function, line 3 in the C code:
gef➤ break 3
We run the program:
gef➤ r
Let’s see in ASM where we are in the code:
gef➤ disassemble
Dump of assembler code for function main:
0x00000000002012e0 <+0>: push rbp
0x00000000002012e1 <+1>: mov rbp,rsp
0x00000000002012e4 <+4>: sub rsp,0x20
0x00000000002012e8 <+8>: mov DWORD PTR [rbp-0x4],edi
0x00000000002012eb <+11>: mov QWORD PTR [rbp-0x10],rsi
=> 0x00000000002012ef <+15>: call 0x201310 <my_function>
0x00000000002012f4 <+20>: xor edi,edi
0x00000000002012f6 <+22>: mov DWORD PTR [rbp-0x14],eax
0x00000000002012f9 <+25>: mov eax,edi
0x00000000002012fb <+27>: add rsp,0x20
0x00000000002012ff <+31>: pop rbp
0x0000000000201300 <+32>: ret
End of assembler dump.
We execute a step in ASM:
=> 0x00000000002012ef <+15>: call 0x201310 <my_function>
gef➤ si
We check the value of bp and sp:
gef➤ registers $rbp $rsp
$rsp : 0x7fffffffe0e8
$rbp : 0x7fffffffe110
We check the values in memory near sp:
gef➤ x/10x $rsp-10
0x7fffffffe0de: 0x00000000 0x00000000 0x12f40000 0x00000020
0x7fffffffe0ee: 0xe1800000 0x7fffffff 0x00000000 0x00000000
0x7fffffffe0fe: 0xe1880000 0x7fffffff
We take one more step:
→ 0x201310 <my_function+0> push rbp
gef➤ si
We check the values near sp again:
gef➤ x/10x $rsp-10
0x7fffffffe0d6: 0x3e020000 0x0008003e 0xe1100000 0x7fffffff
0x7fffffffe0e6: 0x12f40000 0x00000020 0xe1800000 0x7fffffff
0x7fffffffe0f6: 0x00000000 0x00000000
We can see the value of bp in RAM:
$rbp : 0x7fffffffe110 -> 0xe1100000 0x7fffffff
sp will have decremented:
gef➤ registers $rsp
$rsp : 0x7fffffffe0e0
The next instruction is to create the stack frame of our function, we check the value of the bp/sp registers:
gef➤ registers $rbp $rsp
$rsp : 0x7fffffffe0e0
$rbp : 0x7fffffffe110
We execute a step:
=> 0x0000000000201311 <+1>: mov rbp,rsp
gef➤ si
We consult the value of the bp/sp registers:
gef➤ registers $rbp $rsp
$rsp : 0x7fffffffe0e0
$rbp : 0x7fffffffe0e0
The next instruction is to push the variable onto the stack, memory position bp-4:
=> 0x0000000000201314 <+4>: mov DWORD PTR [rbp-0x4],0xbaba
As we can see, it does not use sp but calculates the positions by subtracting the variable size from the value of bp. From this, we can draw several interesting conclusions:
- sp is only used to adjust the value of bp when defining the stack frame.
- sp does not change its value when variables are defined, only when they are pushed/popped.
Depending on the compiler or its version, the ASM code may vary. Sometimes sp is decremented for memory alignment issues, thus wasting part of the RAM but accessing the data with a single access. One must choose between speed or optimizing the use of RAM.
Before executing the order, we check the value of that memory position:
gef➤ x/4x $rbp-4
0x7fffffffe0dc: 0x00000008 0xffffe110 0x00007fff 0x002012f4
We define the variable in RAM:
=> 0x0000000000201314 <+4>: mov DWORD PTR [rbp-0x4],0xbaba
gef➤ si
We check that it has been saved:
gef➤ x/4x $rbp-4
0x7fffffffe0dc: 0x0000baba 0xffffe110 0x00007fff 0x002012f4
Correct, “baba” appears in the appropriate position.
The next instruction is to copy “baba” to the eax register as a return value. We check the current value:
gef➤ registers $rax
$rax : 0x0000000000203028 → 0x0000000000000001 → 0x0000000000000001
We execute the instruction:
=> 0x000000000020131b <+11>: mov eax,DWORD PTR [rbp-0x4]
gef➤ si
We check it again:
gef➤ registers $rax
$rax : 0xbaba
The next instruction is to recover the previously pushed value before returning to the main function. Remember that before returning, we must leave the stack frame as it was before calling the function. We will have to adjust both sp and bp to their original values. In our case, sp has not been touched, so we only do it with bp.
We check the status of bp:
gef➤ registers $rbp
$rbp : 0x7fffffffe0e0
We execute the instruction:
0x20131e <my_function+14> pop rbp
gef➤ si
We check the status of bp:
gef➤ registers $rbp
$rbp : 0x7fffffffe110
Correct, that was the value of bp just before executing the function.