🧩 RISC-V 汇编函数示例讲解:从 C 到汇编的全过程


🧩 一、C语言变成可执行程序的流程

当你在 RISC-V 上用 gcc(实际上是 riscv64-unknown-elf-gcc 或类似交叉编译器)编译 C 代码时,完整的编译流程是:

css
源代码(.c)
   ↓   [编译 compile]
汇编代码(.s)
   ↓   [汇编 assemble]
目标文件(.o)
   ↓   [链接 link]
可执行文件(a.out.elf)

对应命令:

bash
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 源代码

我们从一个非常简单的 C 函数出发:

c
int foo(int x) {
    int y = 5;
    int z = x + y;
    return z;
}

这个函数有:


🧩 三、RISC-V GCC 生成的汇编(RV64)

以下是实际由 GCC 生成的结果(riscv64-unknown-elf-gcc -S -O0 foo.c):

asm
    .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)作为基准访问局部变量,方便调试和栈回溯。


🧩 六、寄存器使用规则(RISC-V ABI 摘要)

名称 用途 调用者/被调用者保存
a0–a7 函数参数和返回值 调用者负责
t0–t6 临时寄存器 调用者负责
s0–s11 保存寄存器(帧指针/变量) 被调用者负责
sp 栈指针 被调用者负责维护
ra 返回地址 调用者保存或函数内部保存

在本例中:


🧩 七、调用者(main)的执行过程示意

假设:

c
int main() {
    int result = foo(10);
    return result;
}

可能对应的汇编:

asm
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
栈方向 向下增长