Reading — step 1 of 5
Read
~2 min readExecution
Calling Convention
The RISC-V ABI specifies:
| Register | ABI name | Use | Saved by |
|---|---|---|---|
| x0 | zero | constant 0 | — |
| x1 | ra | return address | caller |
| x2 | sp | stack pointer | callee |
| x3 | gp | global pointer | — |
| x4 | tp | thread pointer | — |
| x5-x7 | t0-t2 | temporaries | caller |
| x8 | s0/fp | saved / frame ptr | callee |
| x9 | s1 | saved | callee |
| x10-x17 | a0-a7 | function arguments / return | caller |
| x18-x27 | s2-s11 | saved | callee |
| x28-x31 | t3-t6 | temporaries | caller |
Caller-saved (volatile): caller saves before call if it cares. Callee-saved (non-volatile): callee saves before clobbering, restores before return.
Standard prologue (callee setup):
function:
addi sp, sp, -16 # allocate frame
sw ra, 12(sp) # save return address
sw s0, 8(sp) # save frame pointer
addi s0, sp, 16 # new frame pointer
# function body...
Standard epilogue:
lw s0, 8(sp) # restore frame pointer
lw ra, 12(sp) # restore return address
addi sp, sp, 16 # deallocate frame
ret # jalr x0, 0(ra)
Argument passing:
- a0-a7: first 8 integer args.
- Beyond 8 args: spilled to stack.
- Return value(s): a0, a1.
Stack:
- Grows DOWNWARD (decreasing addresses).
- Aligned to 16 bytes at function call boundaries.
Example: int add(int a, int b) { return a + b; }
add:
add a0, a0, a1 # a0 = a + b
ret
That's it. Smaller functions don't even need a stack frame.
Recursive function:
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
factorial:
addi sp, sp, -16
sw ra, 12(sp)
sw s0, 8(sp)
mv s0, a0 # save n
li t0, 1
bge t0, a0, .base # if n <= 1, return 1
addi a0, a0, -1 # n - 1
jal ra, factorial # recursive call
mul a0, a0, s0 # n * factorial(n-1)
j .end
.base:
li a0, 1
.end:
lw s0, 8(sp)
lw ra, 12(sp)
addi sp, sp, 16
ret
The simulator just executes these. The convention is software-level.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…