Functions: call & ret
The return address, and how control comes back
- › 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.
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 mainTrace 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.
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)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.
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?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.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.
$ 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- › 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