CFN Cloud
Cloud Future New Life
en zh
2026-01-09 · 0 views

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
  • -g keeps debug info for mixed source/asm.
  • -O0 avoids optimizations that reshape the frame.
  • -fno-omit-frame-pointer keeps ebp/rbp for 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:

  1. Push the next instruction address (return address).
  2. Jump to the callee entry address (update eip).

ret does the reverse:

  1. Pop the return address into eip.
  2. Move esp up 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
  • bt shows main -> foo -> bar.
  • info registers shows esp/ebp/eip.
  • x/20x $esp dumps 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.
  • call pushes the return address.
  • eip jumps to foo.

8.2 foo calls bar

foo has its own frame, then pushes bar’s arguments and calls it. The stack now contains:

  • bar return address (back to foo).
  • foo return address (back to main).
  • main return 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 rsp for 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:

  1. Check build flags (frame pointers or not).
  2. Use bt first, then inspect registers.
  3. Dump the stack with x/20x $esp.
  4. Use disas near the call site to verify flow.

14. Common pitfalls

  • Assuming ebp must 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: eip is 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.

References