Functions, Calling Convention, and Stack Frames
TL;DR
- You’ll learn how function calls are implemented on RV32 using the ABI (Application Binary Interface):
jal,jalr, thera(Return Address) register, and thesp(Stack Pointer). - You’ll learn to recognize function prologues/epilogues, understand why registers are saved/restored, and map stack offsets to C locals/arguments.
- You’ll practice debugging the stack with GDB: inspecting frames, backtraces, and memory around
sp.
1. The two instructions that define calls
jal (Jump And Link)
- Saves
pc+4intora(x1) - Jumps to a target
jalr (Jump And Link Register)
- Used for indirect calls and returns
- A return is commonly:
| |
Pseudo-instruction form:
| |
2. Leaf vs non-leaf functions
Leaf function: calls no other functions
- can often avoid saving
ra - can sometimes avoid touching the stack
- can often avoid saving
Non-leaf function: calls other functions
- must preserve return address across those calls
- typically saves
rato the stack
3. A canonical stack frame
A typical (not universal) RV32 prologue/epilogue looks like:
| |
What this is doing
- Reserve 16 bytes of stack space
- Save callee-saved regs used by the function (often
s0) - Save
raif needed - Optionally set
s0as a frame pointer
-fomit-frame-pointer) in optimized builds, which makes stack unwinding harder.4. Hands-on: force a clear stack frame
Create:
| |
Build with flags that help readability:
| |
Disassemble:
| |
What to locate
- The prologue/epilogue of
outerandinner - Where locals live (stack offsets)
- Where arguments are placed (registers, and sometimes stack if too many)
5. Debug the stack with GDB (practical commands)
Run with a GDB stub:
| |
In another terminal:
| |
Now inspect:
(gdb) info registers sp ra s0 a0 a1
(gdb) bt
(gdb) info frame
(gdb) x/32wx $sp
(gdb) x/16i $pc
Interpreting x/32wx $sp
You are dumping 32 words of memory starting at the current stack pointer.
- saved
rawill often be visible as a code address - saved
s0may be visible - locals may appear as patterns near the saved regs
$sp and $ra, step, then dump again. You’ll see the new frame being built.6. Stack corruption: how it happens
Common causes:
- writing past the end of a local array
- wrong
memcpylength - treating a pointer as a bigger type than it is
- mismatched function prototypes (especially with variadic functions)
- hand-written assembly that forgets to restore callee-saved registers
Hands-on bug: off-by-one overwrite
Create:
| |
Build and run:
| |
Now debug it and watch the canary:
| |
(gdb) b victim
(gdb) c
(gdb) p/x canary
(gdb) next
(gdb) p/x canary
7. Arguments beyond a0..a7
RISC-V uses a0..a7 for the first 8 integer/pointer arguments.
Extra arguments are passed on the stack.
You can demonstrate this by writing a function with 10 arguments and inspecting how the call site prepares them.
Exercises
- In
stack_demo, identify which registers carry parameters intoinner. - In
stack_demo, determine the stack offset (fromspors0) oflocal1andlocal2by correlating disassembly with GDB memory dumps. - Create a function with 9 integer parameters and find where parameter #9 lives.
- In the overflow example, change the loop back to
i < 16and confirm the canary stays unchanged.
How to test your answers
- Verify offsets by printing addresses (
&local1) in C and comparing to$spin GDB. - Use
objdump -d -M numeric,no-aliasesto confirm stores/loads match the offsets you believe.
Summary
You now understand how RV32 function calls work: ra, sp, prologues/epilogues, and stack-frame structure-and you practiced debugging stack state directly.
Next: linker scripts and memory maps-we’ll control where code/data live, generate .map files, and explain why firmware images often start at addresses like 0x80000000.