JOS - Call Stack
13 Dec 2020Perhaps the most fundamental characteristic of modern software, call stacks are in virtually every high level software (i.e. non-machine code), including OS and even some BIOS.
Its prominence in software is rather intuitive, and Dijkstra explains it best in Recursive Programming. Simply put, the recursive nature of how we view procedures (and their sub-procedures) makes stack extremely suitable for storing their content. It is memory-efficient and fast.
x86 Stack
OS development is always a combination of software and hardware, so it is important to understand what kind of support does the hardware provide for call stacks. memory is allocated for stack.
For x86 architecture, the stack starts at a high memory address and grows downwards:
A few key registers and instructions are listed below:
Registers
Below are register names in 16-bit mode (since their names are the original ones):
-
SP
: Stack Pointer Register. It tracks the ‘top’ (bottom of x86 stack because the stack grows downwards) of the stack. -
BP
: Stack Base Pointer Register. It points to the start address of current frame and holds the value of the start address of previous frame. TheBP
chain is used by debuggers to build up a representation of the call stack. -
IP
: Instruction Pointer Register. It contains the address of the next instruction to be executed.
Instructions
-
PUSH
andPOP
: they exist to manage the stack and operate onSP
register.PUSH SRC
decreasesSP
and writesSRC
to the address that the updatedSP
points to;POP DST
copies from the address inSP
toDST
and increasesSP
.# 'push ax' is roughly equivalent to: sub $SIZE, %sp mov %ax, (%sp) # 'pop ax' is roughly equivalent to: mov (%sp), %ax add $SIZE, %sp # Note: $SIZE depends on machine address size. There are slight differences in the results, e.g. flags.
-
CALL
andRET
: they exist to correctly transfer the execution flow between frames and operate onSP
andIP
.CALL DST
saves theIP
register on the stack and then sets it toDST
;RET
pops from the stack toIP
register.# 'call ax' can be thought of: push $ip mov $ax, $ip # 'ret' can be thought of: pop $ip # Note: directly writing to IP register is forbidden. This is an extremely simplified visualization.
-
ENTER
andLEAVE
: they exist to create/destroy the memory used on entry/exit of a frame and operate onSP
andBP
.ENTER
saves theBP
register on the stack and updates it to point to current stack top;LEAVE
undoes that.# 'enter' is equivalent to: push %bp mov %sp, %bp # 'leave' is equivalent to: move %bp, %sp pop %bp
Apart from those listed, there are some other instructions that modify the stack registers, many of them being expansion of the ones above, but also a few independent ones, e.g. int
(interrupt) creates a new frame for the interrupt handling and thus the stack pointer is modified.
Stack vs Heap
Now that we know what is a call stack and how does it work, let’s see when it is used. The stack memory is used to save the function frame, namely arguments, instruction pointers, base addresses and local variables.
One interesting topic that comes up from time to time is stack vs heap. In contrast to stack memory where the residing local variables live and die with the function frame, variables allocated from heap memory are independent of the function frame. By this definition, we say heap memory is dynamically allocated. With added flexibility for its variables’ lifespan, a few sacrifices are made:
- its memory is more fragmented;
- memory may not be contiguous for adjacent allocations, and it leads to worse cache performance;
- deallocation is not automatic, and it’s more likely to cause memory issues.
It should be emphasized that the key factor for choosing between stack (e.g. local variables) vs heap (e.g. malloc/free, new/delete) are
- whether they need to outlive the function scopes;
- required memory size: stack memory is usually more limited than heap memory.
For legacy reasons (prior to c99 for C Language, and for C++ its current standards although most compilers support it), static (variable size known at compile time) is sometimes a requirement for using stack memory:
// when compiled with '-pedantic-errors', it fails with
// "error: ISO C++ forbids variable length array 'c' [-Wvla]"
// But most compilers supress this 'not-standard-conforming error'
// Example of variable length array allocation:
// a: is a local var alloc-ed on stack
// b: alloc-ed from heap
// c: is var-length local array
int a = 5, *b = new int(6), c[rand()%10];
// print the storage location of each one of them
printf("a:0x%p\nb:0x%p\nc:0x%p",&a, b, c);
// sample result
// a:0x0x7ffd9073d564
// b:0x0x1a69eb0
// c:0x0x7ffd9073d550
We can see that c
resides in stack memory.
JOS Stack
Now let’s take a look on how stack is set up in JOS.
When the bootloader start, it overwrites the stack pointer value from BIOS to start
(at 0x7c00
):
# boot/boot.S
movl $start, %esp
call bootmain
and grows into the opposite direction of the code section:
kernel destroys the bootloader stack
# kern/entry.S
.data
###################################################################
# boot stack
###################################################################
.p2align PGSHIFT # force page alignment
.globl bootstack
bootstack:
.space KSTKSIZE
.globl bootstacktop
bootstacktop:
Once we enter kernel
, the stack used in boot
is destroyed, and a new stack is create at a designated address:
# kern/entry.S
## clear ebp and esp
relocated:
movl $0x0,%ebp # nuke frame pointer
movl $(bootstacktop),%esp
call i386_init
## stack location
.data
.p2align PGSHIFT # force page alignment
.globl bootstack
bootstack:
.space KSTKSIZE
.globl bootstacktop
bootstacktop:
Refer to previous post for memory layout. With that in mind, and
// inc/mmu.h
#define PGSIZE 4096 // 4k
#define PGSHIFT 12 // log2(PGSIZE)
// inc/memlayout.h
#define KSTKSIZE (8*PGSIZE) // 32k
we can infer that JOS kernel stack is of size 32k.
Further inspecting $esp
during execution or objdump -h kernel
, we know that it resides in 0xf0110000-0xf0108000
over the kernel code (.text
) section (and .rodata
, .stab
, .stabstr
).
Reconstructing the Stack
Lastly, the stack information is valuable in the sense that we can reconstruct the stack graph from the information stored on the stack.
Debuggers use these information to implement backtrace/bt functionality.
The ebp
can be seen as a chain/linked-list of frame starting address, since each $ebp
points to the location that contains the previous $ebp
.
Combining with the eip
stored on the stack and .fstab
information, we can fully reconstruct the call stack:
// kern/monitor.c
struct Eipdebuginfo info;
uint32_t* ebp = (uint32_t*)read_ebp();
while (ebp != 0) {
// eip sits atop of ebp
uintptr_t eip = *(ebp+1);
...
// debuginfo_eip() fetches the debug info related to eip from .stab
debuginfo_eip(eip, &info);
...
ebp = (uint32_t*) *ebp;
}