Linux Function Calls and Stack Frames
Understand calling conventions, stack frames, call/ret behavior, debugging observation, and security implications from the assembly view.
This article starts with a minimal C example and expands into how Linux function calls build stack frames, pass parameters, store return addresses, follow register conventions, and can be observed in objdump/gdb. If you care about binary analysis, crash triage, performance tuning, or security work (e.g., overflows and ROP), the call stack is foundational.
1. Example and goal
A small three-level call chain:
int bar(int c, int d) {
int e = c + d;
return e;
}
int foo(int a, int b) {
return bar(a, b);
}
int main(void) {
foo(2, 3);
return 0;
}
Recommended build flags:
gcc -g -O0 -fno-omit-frame-pointer foo_bar.c -o a.out
-gkeeps debug info for mixed source/asm.-O0avoids optimizations that reshape the frame.-fno-omit-frame-pointerkeepsebp/rbpfor easier inspection.
2. How the stack grows
On x86 (32-bit), the stack grows from high addresses to low addresses. esp points to the top of the stack, ebp points to the base of the current frame. Each call creates a new frame; each return destroys it. The core items stored on the stack are:
- Return address (pushed by
call). - Previous frame pointer (saved old
ebp). - Parameters and local variables.
Simplified frame layout (high addresses at the top):
High addresses
| arg b | <- ebp+12
| arg a | <- ebp+8
| return addr | <- ebp+4
| old ebp | <- ebp
| local var e | <- ebp-4
Low addresses (esp grows downward)
These offsets come from the common cdecl convention, but other conventions exist.
3. What call/ret really do
call performs two actions:
- Push the next instruction address (return address).
- Jump to the callee entry address (update
eip).
ret does the reverse:
- Pop the return address into
eip. - Move
espup by one word.
This explains why return addresses appear on the stack and why stack overflows can hijack control flow.
4. Function prologue/epilogue
Typical function entry:
push %ebp
mov %esp, %ebp
sub $0x10, %esp
Meaning:
- Save old
ebp. - Establish a new frame base.
- Reserve space for locals.
Typical function exit:
leave
ret
leave is equivalent to:
mov %ebp, %esp
pop %ebp
Restores the previous frame, then ret returns.
4.1 Fuller objdump output (excerpt)
Below is a longer disassembly excerpt to show the call chain end-to-end:
$ objdump -dS a.out
...
080483dc <bar>:
80483dc: 55 push %ebp
80483dd: 89 e5 mov %esp,%ebp
80483df: 83 ec 10 sub $0x10,%esp
80483e2: 8b 45 0c mov 0xc(%ebp),%eax
80483e5: 8b 55 08 mov 0x8(%ebp),%edx
80483e8: 01 d0 add %edx,%eax
80483ea: 89 45 fc mov %eax,-0x4(%ebp)
80483ed: 8b 45 fc mov -0x4(%ebp),%eax
80483f0: c9 leave
80483f1: c3 ret
080483f2 <foo>:
80483f2: 55 push %ebp
80483f3: 89 e5 mov %esp,%ebp
80483f5: 83 ec 08 sub $0x8,%esp
80483f8: 8b 45 0c mov 0xc(%ebp),%eax
80483fb: 89 44 24 04 mov %eax,0x4(%esp)
80483ff: 8b 45 08 mov 0x8(%ebp),%eax
8048402: 89 04 24 mov %eax,(%esp)
8048405: e8 d2 ff ff ff call 80483dc <bar>
804840a: c9 leave
804840b: c3 ret
0804840c <main>:
804840c: 55 push %ebp
804840d: 89 e5 mov %esp,%ebp
804840f: 83 ec 08 sub $0x8,%esp
8048412: c7 44 24 04 03 movl $0x3,0x4(%esp)
804841a: c7 04 24 02 00 movl $0x2,(%esp)
8048421: e8 cc ff ff ff call 80483f2 <foo>
8048426: b8 00 00 00 00 mov $0x0,%eax
804842b: c9 leave
804842c: c3 ret
...
5. Parameters, return values, and register rules
For 32-bit cdecl:
- Args are pushed right-to-left.
- Return value is in
eax. - The caller cleans the stack (e.g.,
add $0xN, %esp).
In our example, main places 3 at esp+4 and 2 at esp, then call foo. Inside foo, a and b are read via ebp+8 and ebp+12.
6. How frames chain together
Each frame stores the previous ebp, forming a linked list. bt in gdb walks this chain to show the call stack. If frame pointers are omitted (e.g., -fomit-frame-pointer), backtraces can be incomplete and rely on DWARF or heuristics.
7. Observing the real stack with gdb
Useful commands:
start
disas
bt
info registers
x/20x $esp
btshowsmain -> foo -> bar.info registersshowsesp/ebp/eip.x/20x $espdumps the stack words so you can locate return addresses and arguments.
7.1 Longer gdb session (excerpt)
Below is a longer debug session excerpt (addresses vary by build/runtime):
$ gdb a.out
(gdb) start
Temporary breakpoint 1 at 0x8048412: file foo_bar.c, line 20.
Starting program: ./a.out
Temporary breakpoint 1, main () at foo_bar.c:20
20 foo(2, 3);
(gdb) s
foo (a=2, b=3) at foo_bar.c:14
14 return bar(a, b);
(gdb) s
bar (c=2, d=3) at foo_bar.c:8
8 int e = c + d;
(gdb) disas
Dump of assembler code for function bar:
0x080483dc <+0>: push %ebp
0x080483dd <+1>: mov %esp,%ebp
0x080483df <+3>: sub $0x10,%esp
=> 0x080483e2 <+6>: mov 0xc(%ebp),%eax
0x080483e5 <+9>: mov 0x8(%ebp),%edx
0x080483e8 <+12>: add %edx,%eax
0x080483ea <+14>: mov %eax,-0x4(%ebp)
0x080483ed <+17>: mov -0x4(%ebp),%eax
0x080483f0 <+20>: leave
0x080483f1 <+21>: ret
End of assembler dump.
(gdb) bt
#0 bar (c=2, d=3) at foo_bar.c:9
#1 0x0804840a in foo (a=2, b=3) at foo_bar.c:14
#2 0x08048426 in main () at foo_bar.c:20
(gdb) info registers
eax 0x5
esp 0xbffff678
ebp 0xbffff688
eip 0x080483f0
(gdb) x/20x $esp
0xbffff678: 0x0804840a 0x00000002 0x00000003 0xbffff698
0xbffff688: 0xbffff6a8 0x08048426 0x00000002 0x00000003
...
7.2 Stack memory diagram (illustrative)
Mapping the stack words into a simple diagram (values are illustrative):
High addresses
0xbffff6a8 [0xbffff6b8] saved ebp (main)
0xbffff6a4 [0x08048426] return -> main
0xbffff6a0 [0x00000002] arg a (foo)
0xbffff69c [0x00000003] arg b (foo)
0xbffff698 [0xbffff6a8] saved ebp (foo)
0xbffff694 [0x0804840a] return -> foo
0xbffff690 [0x00000002] arg c (bar)
0xbffff68c [0x00000003] arg d (bar)
0xbffff688 [0x00000005] local e
Low addresses (esp grows downward)
Actual addresses and values vary with compiler, optimization, and alignment, but the structural relationship is consistent.
8. Call flow breakdown (matching the example)
8.1 main calls foo
main prepares arguments, then executes call foo:
- Arguments are pushed right-to-left.
callpushes the return address.eipjumps tofoo.
8.2 foo calls bar
foo has its own frame, then pushes bar’s arguments and calls it. The stack now contains:
barreturn address (back tofoo).fooreturn address (back tomain).mainreturn address (back to libc startup).
8.3 bar returns
bar writes the local e into eax, then leave; ret restores foo’s frame and jumps back.
9. x86-64 and the SysV ABI differences
On 64-bit:
- Parameters go to registers (
rdi, rsi, rdx, rcx, r8, r9). - Return value is in
rax. - The stack still stores return addresses and locals, but fewer args land on the stack.
- There is a red zone (128 bytes below
rspfor leaf functions).
The same C code therefore produces cleaner stack layouts on x86-64, so always confirm the target architecture when reading disassembly.
10. Caller-saved vs callee-saved registers
ABI defines who must preserve which registers:
- Caller-saved: the caller must save them if needed.
- Callee-saved: the callee saves/restores if it uses them.
This affects how many push/pop instructions appear and the stack frame size.
11. How optimization changes frames
Compiler optimization can:
- Omit frame pointers.
- Keep locals in registers.
- Apply tail-call optimization, reusing frames.
- Reorder instructions to reduce stack traffic.
For learning or debugging, prefer -O0 and keep frame pointers.
12. Security relevance
Because return addresses and locals live on the stack:
- Overwriting past a buffer can overwrite a return address.
- Modern defenses (canary, NX/DEP, ASLR, PIE) mitigate exploitation.
- Still, understanding frames is necessary for analyzing overflows and ROP.
13. Practical checklist
When debugging:
- Check build flags (frame pointers or not).
- Use
btfirst, then inspect registers. - Dump the stack with
x/20x $esp. - Use
disasnear the call site to verify flow.
14. Common pitfalls
- Assuming
ebpmust exist: it may be omitted under optimization. - Assuming all args are on the stack: on x86-64 they are mostly in registers.
- Confusing stack addresses with instruction addresses:
eipis execution, stack holds return addresses.
FAQ
Q1: Why use ebp as a fixed base?
A: It provides a stable reference for arguments and locals. Optimizers may remove it.
Q2: Why does the stack grow downward?
A: Historical ABI choice; it could have been the opposite.
Q3: Is the return value always in eax/rax?
A: For integers/pointers typically yes; structs may use hidden pointers or multiple registers.
Q4: Why is gdb backtrace sometimes incomplete?
A: Missing frame pointers, optimization, or missing debug info.
Q5: How does tail-call optimization affect the stack?
A: Tail calls can reuse the current frame, reducing one stack layer.