roadmap
week 2 · day 12

Functions: call & ret

The return address, and how control comes back

Easy 34 min 180 xp
After this you can
  • Explain exactly what call and ret do in terms of rip, rsp, and the stack
  • Describe what a 'return address' is and where it is stored
  • Trace control flow through a call and back, by hand
  • See why overwriting the saved return address hands an attacker the program

The problem call solves

A function can be called from many places, and it has to return to *whichever* place called it. So the machine needs to remember "where do I go back to?" — and it needs to remember it in a way that nests correctly when functions call functions. You already have the perfect structure for nesting: the stack. call and ret are just push and pop of one specific thing — the return address.

call = push the return address, then jump

When the CPU executes call target:

1. It pushes the address of the next instruction (the current rip, i.e. where to resume) onto the stack. 2. It sets rip = target — jumping into the function.

And ret is the exact inverse:

  • It pops the top of the stack into rip — jumping back to the pushed return address.

That's the whole mechanism. call is "push rip; jmp target." ret is "pop rip." The return address lives on the stack, and ret trusts whatever is there.

call pushes the return address
   main:
     ...
     call add        ; (1) push address of NEXT line (the '...after')
   .after:           ; (2) rip = add ; execution jumps into add
     ...

   add:
     ...              ; does its work
     ret              ; pop the saved address -> rip = .after -> back in main

Trace it, so it's not magic

Say call add sits at address 0x1000 and is 5 bytes long, so the instruction after it is at 0x1005. rsp is 0x7ffe40 before the call.

control and stack through call/ret
  step            | rip     | rsp       | [rsp] holds
  ----------------+---------+-----------+-------------
  at 'call add'   | 0x1000  | 0x7ffe40  | (whatever)
  after call      | add     | 0x7ffe38  | 0x1005  <- return address pushed
  ...inside add...| ...     | 0x7ffe38  | 0x1005
  at 'ret'        | ret     | 0x7ffe38  | 0x1005
  after ret       | 0x1005  | 0x7ffe40  | (popped)
Key idea
The return address is data sitting on the stack, and ret blindly loads it into rip and jumps there. The CPU does not verify it. So if anything overwrites that stack slot before ret runs — say, a buffer that the program wrote past the end of — then ret jumps to *whatever address you put there*. That is the entire idea of a stack buffer overflow, and you now understand its mechanism months before you'll weaponize it. Control flow is only as safe as the return address on the stack.

Nesting just works

Because return addresses are pushed and popped in LIFO order, nested calls unwind perfectly: main calls f, f calls g, g's ret returns to f, f's ret returns to main. Each call pushed one return address; each matching ret pops it. The stack's discipline (day 11) is *exactly* what makes call depth work — that's why the CPU loves it.

Predict first
ret does "pop rip." Suppose, just before a function's ret, a bug has overwritten the 8 bytes at [rsp] (where the return address lives) with the value 0x4141414141414141 ('AAAAAAAA'). What does the CPU do when it executes ret?
Note
In gdb, break at the first instruction of a function and run x/gx $rsp — the 8 bytes shown are the return address ret will use. Step to the ret, run x/gx $rsp again, and stepi over it while watching rip land back at the caller. Seeing the return address explicitly demystifies the whole call/ret dance.
finished reading?
Your task, you write the code

Watch the return address

Write callret.c with main() calling a small function that returns a constant. Compile at -O0 with `gcc -O0 -g`. In gdb: break on the function, print the return address at the top of the stack (x/gx $rsp), note it, then confirm it equals the address of the instruction right after the `call` in main's disassembly. Step over the `ret` and watch rip return exactly there. Write down, in your own words, what call pushed and what ret popped. Uses only call/ret and the stack facts from days 11-12.

deliverable: callret.c (+ your gdb notes on the return address)
build & run
$ gcc -O0 -g callret.c -o callret
$ objdump -d -M intel callret # find the instruction after 'call'
$ gdb ./callret # break the function; x/gx $rsp; stepi over ret
self-review before running
  • You found the return address on the stack and matched it to the post-call instruction
  • You watched ret pop it into rip and land back in main
  • You can state, precisely, what call and ret each do
  • You can explain why overwriting [rsp] before ret would redirect execution
stretchAdd a second function so main→f→g. In gdb, run `backtrace` at the deepest point and match each frame's return address to a `call` site. You're reading the chain of return addresses the stack is holding.

Self-check

01What does `call target` do?
02Where is the return address stored?
03`ret` does:
04Why is call/ret the root of stack buffer-overflow attacks?
0/4 correct · 0/4 checked