RV32 调用约定
平台:
qemu-riscv32;riscv64-unknown-elf-gcc -march=rv32imac -mabi=ilp32 -O1 -g -fno-inline(一份加-fomit-frame-pointer,一份加-fno-omit-frame-pointer)
工具:gdbsp;JSON/gdbsp/rv32-add1-nofp.json(40 步)、/gdbsp/rv32-add1-fp.json(51 步),都从main到exit
本文:对着下一条要执行的指令,看通用寄存器和$sp附近每个字
配套代码:code/riscv-call-stack/
目录
RV32 的 ilp32 调用约定里,前八个整数参数放在 a0~a7,再多出来的由调用者写到栈上,每个槽 4 字节。add2 有十个参数,进函数时 0(sp)、4(sp) 就是 9 和 10。
1. 对应的 C
int add2(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j) {
return a + b + c + d + e + f + g + h + i + j;
}
int add1(int a, int b) {
return add2(a, b, 3, 4, 5, 6, 7, 8, 9, 10);
}
int main(int argc, char *argv[]) {
volatile int local = 0xffff;
return add1(1, 2);
}add1 还要再 jal add2,得先把返回地址从 ra 存到栈上。无帧指针时 add2 是叶函数,进来之后直接用寄存器和 lw 做加法。
2. 单步回放
| 编译 | 步数 | 回放 |
|---|---|---|
-fomit-frame-pointer | 40 | 无帧指针 |
-fno-omit-frame-pointer | 51 | 有帧指针 |
3. 如何做栈回溯
有帧指针
RV32 的帧指针是 s0(ABI 里也叫 fp)。非叶函数序言会把 ra 和旧的 s0 存进栈,再 addi s0, sp, <帧大小>,让 s0 对准进入本函数时的 sp。有如下规则:
s0 指向进入当前函数前的 sp,[s0-4] = 本函数的 ra,[s0-8] = 上一层 s0。s0 寄存器指在这对的上面。
高地址
[s0] 进入本函数时的 sp ← s0 寄存器指这里
[s0-4] 本函数的 ra
[s0-8] 上一层 s0
…
低地址这份录制里帧大小是 32,所以 addi s0, sp, 32,ra 在 28(sp),旧 s0 在 24(sp)。顺着 [s0-8] 走到 0 就是整条链。
无帧指针
只能根据序言里面保存的 ra 做栈回溯。这份录制里是 sw ra, 28(sp)。
栈帧示意图
进了 add2 之后。示意地址从 164 往下每格 4 字节;红色是 main,绿色是 add1,蓝色是 add2(有 s0 时才提栈)。
ramain 保存的 ra156s0main 保存的上一层 s01521481440xffffmain 的局部变量 local140136132main 的 sp;add1 的 s0 指向这里128raadd1 保存的 ra124164add1 保存的 s0,接到 main 的 s012011611210810410传给 add2 的第 10 个参数1009传给 add2 的第 9 个参数;add2 的 s0 指向这里96132add2 保存的 s0,接到 add1 的 s0928884add2 的 spramain 保存的 ra1561521481440xffffmain 的局部变量 local140136132128raadd1 保存的 ra12412011611210810410传给 add2 的第 10 个参数1009传给 add2 的第 9 个参数,当时的 sp4. 局部变量如何传递
RV32 ilp32 用 a0~a7 传递整数参数,超出的由调用者写到栈上,每个槽 4 字节。
5. 返回值如何传递
RV32 默认使用 a0 传递函数返回值。