当你在 RISC-V 上用 gcc(实际上是 riscv64-unknown-elf-gcc 或类似交叉编译器)编译 C 代码时,完整的编译流程是:
源代码(.c)
↓ [编译 compile]
汇编代码(.s)
↓ [汇编 assemble]
目标文件(.o)
↓ [链接 link]
可执行文件(a.out 或 .elf)
对应命令:
riscv64-unknown-elf-gcc -S foo.c -o foo.s # 生成汇编
riscv64-unknown-elf-gcc -c foo.s -o foo.o # 汇编成目标文件
riscv64-unknown-elf-gcc foo.o -o foo.elf # 链接生成可执行文件
我们从一个非常简单的 C 函数出发:
int foo(int x) {
int y = 5;
int z = x + y;
return z;
}
这个函数有:
xy 和 zz以下是实际由 GCC 生成的结果(riscv64-unknown-elf-gcc -S -O0 foo.c):
.file "foo.c"
.option pic
.text
.align 1
.globl foo
.type foo, @function
foo:
addi sp,sp,-48 # 为局部变量和保存寄存器分配栈空间
sd s0,40(sp) # 保存 s0(帧指针寄存器)
addi s0,sp,48 # 建立帧指针:s0 = sp + 48
mv a5,a0 # a5 = x (保存传入参数)
sw a5,-36(s0) # 存入栈中,x → [s0-36]
li a5,5 # a5 = 5
sw a5,-20(s0) # y = 5 → [s0-20]
lw a5,-36(s0) # 取出 x
mv a4,a5 # a4 = x
lw a5,-20(s0) # 取出 y
addw a5,a4,a5 # a5 = x + y
sw a5,-24(s0) # z = x + y → [s0-24]
lw a5,-24(s0) # 取出 z
mv a0,a5 # 返回值放到 a0
ld s0,40(sp) # 恢复 s0
addi sp,sp,48 # 释放栈空间
jr ra # 返回调用者
.size foo, .-foo
.ident "GCC: (GNU) 11.2.1 20211120"
.section .note.GNU-stack,"",@progbits
| 汇编指令 | 解释 |
|---|---|
addi sp,sp,-48 |
向下移动栈指针,为本函数分配 48 字节的栈空间。 |
sd s0,40(sp) |
保存旧的帧指针寄存器 s0 到当前栈帧顶部(sp+40)。 |
addi s0,sp,48 |
建立新的帧指针:s0 指向函数栈帧的“上边界”(sp+48)。 |
mv a5,a0 |
将参数 x(在 a0)复制到 a5。 |
sw a5,-36(s0) |
把 x 存入栈中(函数局部保存)。 |
li a5,5 |
a5 ← 5(局部变量 y)。 |
sw a5,-20(s0) |
把 y 存入栈中。 |
lw a5,-36(s0) |
取出 x。 |
mv a4,a5 |
复制 x 到 a4。 |
lw a5,-20(s0) |
取出 y。 |
addw a5,a4,a5 |
执行 32 位加法:a5 = x + y。 |
sw a5,-24(s0) |
把 z = x + y 存入栈。 |
lw a5,-24(s0) |
取出 z。 |
mv a0,a5 |
把 z 放到 a0,用于返回值。 |
ld s0,40(sp) |
恢复原来的 s0(帧指针)。 |
addi sp,sp,48 |
释放本函数栈空间。 |
jr ra |
跳转回返回地址(ra 寄存器)。 |
在本例中,栈空间分配 48 字节(0x30),布局如下:
高地址(s0 指向这里) ← s0 = sp + 48
┌────────────────────────┐
│ 上层函数返回地址 ra │ (通过调用者保存)
│ 保存的 s0 寄存器 │ ← [sp + 40]
│────────────────────────│
│ 局部变量 z (x + y) │ ← [s0 - 24]
│ 局部变量 y (5) │ ← [s0 - 20]
│ 参数副本 x │ ← [s0 - 36]
│────────────────────────│
│ 预留 / 对齐空间 │
└────────────────────────┘ ← sp (当前函数底部)
低地址
s0(frame pointer)作为基准访问局部变量,方便调试和栈回溯。
| 名称 | 用途 | 调用者/被调用者保存 |
|---|---|---|
a0–a7 |
函数参数和返回值 | 调用者负责 |
t0–t6 |
临时寄存器 | 调用者负责 |
s0–s11 |
保存寄存器(帧指针/变量) | 被调用者负责 |
sp |
栈指针 | 被调用者负责维护 |
ra |
返回地址 | 调用者保存或函数内部保存 |
在本例中:
a0 传入参数 xa0 也返回 zs0 被用作帧指针(被调用者保存)sp 管理栈空间ra 由调用者自动处理(函数内未修改)假设:
int main() {
int result = foo(10);
return result;
}
可能对应的汇编:
main:
addi sp, sp, -16
sw ra, 12(sp)
li a0, 10 # 参数 x = 10
call foo # jal ra, foo
mv a5, a0 # 获取返回值 z
mv a0, a5
lw ra, 12(sp)
addi sp, sp, 16
ret
main 调用 foo
↓
foo 建立栈帧
↓
执行 x + 5
↓
恢复 s0,释放栈帧
↓
通过 ra 返回 main
↓
main 取 a0(结果)
返回值 a0 = 15。
| 项目 | 内容 |
|---|---|
| 函数参数 | a0 (x) |
| 局部变量 | 栈中存放 |
| 栈大小 | 48 字节 |
| 帧指针 | s0 = sp + 48 |
| 保存寄存器 | s0 |
| 返回寄存器 | a0 |
| 返回指令 | jr ra |
| 栈方向 | 向下增长 |