欢迎光临
我们一直在努力

【risc-v专栏02】base ISA 与 ABI 寄存器约定

本篇定位:RISC-V 专题第 2 篇。目标是让你看懂汇编、调通死机、会写 inline asm。核心是两件事:① 32 个寄存器各自叫什么、干什么、谁负责保存;② 指令长什么样、怎么编码、RVC 压缩怎么回事。这一格通了,你打开任何一个 .s 启动文件、任何一段反汇编,都不再是乱码。

先建立心智模型:RISC-V 是 load-store 架构 RISC-V 的运算指令绝不直接碰内存。要做 c = a + b,必须先 lw 把 a、b 从内存 load 进寄存器,再 add 寄存器,再 sw 把结果存回内存。只有 load/store 指令访问内存,运算只在寄存器间。这叫 load-store 架构(RISC 的标志),和 x86 的"运算能直接操作内存"相反。


目录

  • 一、32 个通用寄存器 + ABI 别名
    • 1.1 寄存器全表(rv32i/rv64i 通用)
    • 1.2 两个核心概念:caller-saved vs callee-saved
    • 1.3 ra 的特殊地位
  • 二、base 指令集(rv32i):40 条左右
    • 2.1 整数运算(R/I 型)
    • 2.2 访存指令(load-store,唯一碰内存的指令)
    • 2.3 控制流(分支 + 跳转)
    • 2.4 系统指令
    • 2.5 伪指令(汇编器展开成真指令)
  • 三、指令格式:六种
  • 四、RVC 压缩指令:16 位变长
    • 4.1 原理
    • 4.2 C 扩展的常见压缩形式
    • 4.3 编译时控制
  • 五、调用约定(ILP32/LP64)详解
    • 5.1 参数传递
    • 5.2 栈帧布局
    • 5.3 ILP32 / LP64 / f / d 后缀
  • 六、gp 相对寻址:RISC-V 特有
  • 七、ARM vs RISC-V 寄存器/调用约定对照
  • 八、本篇小结
    • 速查表

一、32 个通用寄存器 + ABI 别名

RISC-V 有 32 个通用寄存器 x0-x31。但汇编里几乎不写 x5、x10 这种数字,而是用 ABI 约定的别名(ra/sp/gp/a0…)。这层别名是"调用约定"的一部分——大家约定哪个寄存器干什么活,函数之间才能配合。

1.1 寄存器全表(rv32i/rv64i 通用)

寄存器ABI 名用途保存责任你要记住的
x0 zero 硬连线 0 — 永远是 0,写进去也被忽略。要常量 0 就用它
x1 ra 返回地址 caller-saved jal/jalr 自动存这。函数返回用 ret(=jalr x0,ra,0)
x2 sp 栈指针 callee-saved 每个函数入口 sp 下移,出口上移
x3 gp 全局指针 — 指向 .sdata 中间,gp 相对寻址用。启动时设一次
x4 tp 线程指针 — RTOS 常用它存当前 TCB 指针(类似 ARM 的 PSP 上下文)
x5-x7 t0-t2 临时寄存器 caller-saved 函数里随便用,不保留
x8-x9 s0/fp, s1 保存寄存器 callee-saved s0 常当帧指针 fp
x10-x11 a0-a1 参数/返回值 caller-saved 前 2 个参数 + 函数返回值
x12-x17 a2-a7 参数 caller-saved 参数 3-8(a0-a7 共传 8 个整型参数)
x18-x27 s2-s11 保存寄存器 callee-saved 跨函数调用要保留
x28-x31 t3-t6 临时寄存器 caller-saved 同 t0-t2

1.2 两个核心概念:caller-saved vs callee-saved

这是调用约定的灵魂,不懂这个你看不懂任何 prologue/epilogue。

  • caller-saved(t0-t6, a0-a7, ra):调用方负责保存。意思是"函数 A 要调函数 B,A 如果还想要这些寄存器的值,A 自己在调 B 之前压栈"。被调的 B 可以随便改它们,不负责保留。
  • callee-saved(sp, gp, tp, s0-s11):被调方负责保存。意思是"函数 B 如果要用 s0-s11,B 必须在入口压栈、出口恢复,保证 B 返回时这些寄存器和进来时一样"。

嵌入式视角:为什么这么分 这是个分工优化。caller-saved 寄存器"短命",适合放临时变量,调用前要用就自己存;callee-saved 寄存器"长命",适合放跨调用的变量,被调方保证不动它。这样编译器能高效分配寄存器,减少压栈。这条约定直接决定栈回溯算法找哪个寄存器。

1.3 ra 的特殊地位

ra(x1)是返回地址寄存器,但它被归为 caller-saved。这意味着:

  • jal ra, func 执行时,硬件把 PC+4(返回地址)写进 ra,然后跳 func
  • func 内部如果不再调用别的函数(叶子函数),ra 一直保留返回地址,直接 ret 返回
  • func 内部如果要调用别的函数,ra 会被覆盖,所以 func 入口必须先 sw ra, (sp) 把它存起来,出口 lw ra 恢复

这就是为什么非叶子函数的 prologue 一定有 sw ra——你看启动汇编或反汇编,函数开头 addi sp,sp,-16; sw ra,12(sp); sw s0,8(sp) 是标准动作。


二、base 指令集(rv32i):40 条左右

不用背,看懂即可。分六类:

2.1 整数运算(R/I 型)

add rd, rs1, rs2 # rd = rs1 + rs2 (R型,寄存器-寄存器)
addi rd, rs1, imm # rd = rs1 + imm (I型,寄存器-立即数)
sub rd, rs1, rs2 # rd = rs1 – rs2 (没有 subi,用 addi 负立即数)
and/or/xor rd, rs1, rs2
andi/ori/xori rd, rs1, imm
sll/srl/sra rd, rs1, rs2 # 逻辑左移/右移/算术右移
slli/srli/srai rd, rs1, shamt
slt rd, rs1, rs2 # rd = (rs1 < rs2) ? 1 : 0 (有符号)
sltu rd, rs1, rs2 # 无符号版
slti/sltiu rd, rs1, imm
lui rd, imm # rd = imm << 12 (加载立即数高位,U型)
auipc rd, imm # rd = PC + (imm << 12) (PC相对,用于位置无关)

没有 subi、没有 mov RISC-V 极简:subi 没有,用 addi rd, rs, -imm 代替;mov 没有,用 addi rd, rs, 0 代替;nop 就是 addi x0, x0, 0。这种"能用已有指令组合就不加新指令"的思路贯穿整个 base ISA,是它保持精简的原因。

2.2 访存指令(load-store,唯一碰内存的指令)

lw rd, off(rs1) # rd = Mem[rs1 + off] (32位 load)
lh rd, off(rs1) # 16位,符号扩展
lhu rd, off(rs1) # 16位,零扩展
lb rd, off(rs1) # 8位,符号扩展
lbu rd, off(rs1) # 8位,零扩展
sw rs2, off(rs1) # Mem[rs1+off] = rs2 (32位 store)
sh rs2, off(rs1) # 16位
sb rs2, off(rs1) # 8位

# rv64 多了:
ld rd, off(rs1) # 64位 load
sd rs2, off(rs1) # 64位 store

注意寻址形式只有 off(rs1) 一种(基址+偏移),没有 ARM 的 [r1, r2](双寄存器)或 r1!(前/后变址)。要双寄存器寻址,先 add 再 lw。

2.3 控制流(分支 + 跳转)

# 分支(B型,条件跳,不存返回地址)
beq rs1, rs2, label # if (rs1==rs2) goto label
bne rs1, rs2, label
blt rs1, rs2, label # 有符号 <
bge rs1, rs2, label # 有符号 >=
bltu/bgeu # 无符号版

# 跳转(J型/I型,无条件)
jal rd, label # rd = PC+4; PC = label (J型,相对跳转,范围±1MB)
jalr rd, rs1, imm # rd = PC+4; PC = rs1+imm (I型,间接跳转,任意地址)

2.4 系统指令

ecall # 环境调用(触发 trap,用于系统调用)
ebreak # 断点(触发 trap,调试用)
fence # 内存屏障(IO与内存保序)
fence.i # 指令缓存屏障(改代码后同步 icache)
csrrw/csrsc/crrc … # CSR 读写(③专题详讲)

2.5 伪指令(汇编器展开成真指令)

RISC-V 汇编里很多"指令"其实是伪指令,汇编器展开成 1-2 条真指令。常见的:

伪指令真指令含义
li rd, imm lui+addi(大立即数) 加载立即数
mv rd, rs addi rd, rs, 0 寄存器拷贝
nop addi x0, x0, 0 空操作
ret jalr x0, ra, 0 函数返回
j label jal x0, label 无条件跳转(不存 ra)
call func auipc+jalr ra 远调用(超出±1MB)
la rd, symbol auipc+addi 加载符号地址
bnez rs, label bne rs, x0, label 非零则跳

三、指令格式:六种

RISC-V 指令定长 32 位(标准),有六种格式。不用背编码细节,但要知道哪类指令用哪种格式,以及立即数位段分布——这关系到你反汇编时能不能手动解码。

R 型: [funct7(7)|rs2(5)|rs1(5)|funct3(3)|rd(5)|opcode(7)] 寄存器-寄存器运算 add/sub
I 型: [imm(12)|rs1(5)|funct3(3)|rd(5)|opcode(7)] 立即数运算/访存 addi/lw
S 型: [imm[11:5]|rs2(5)|rs1(5)|funct3(3)|imm[4:0]|opcode(7)] store sw/sh/sb
B 型: [imm[12|10:5]|rs2|rs1|funct3|imm[4:1|11]|opcode] 分支 beq/bne
U 型: [imm[31:12]|rd|opcode] 长立即数 lui/auipc
J 型: [imm[20|10:1|11|19:12]|rd|opcode] 跳转 jal

立即数位段是错位拼接的——栈回溯解码的坑 注意 B 型和 J 型的立即数不是连续的:B 型 imm 分散在 [12,10:5] 和 [4:1,11] 两段;J 型分散在 [20,10:1,11,19:12]。这是为了硬件解码优化(让关键位对齐在固定位置),但软件手动解码时要按位段拼回来。


四、RVC 压缩指令:16 位变长

这是 RISC-V 省 Flash 的大杀器。-march 带 c(如 rv32imac)就启用。

4.1 原理

常用指令(addi/lw/sw/jal/mv/li…)在很多场景下立即数很小、寄存器号也集中在常用那几个,完全可以用 16 位编码。RVC 把约 100 多个常用形式压缩成 16 位,省 ~30% 代码体积。硬件解码时自动识别 16/32 位混合(看最低 2 位)。

指令最低 2 位:
00, 01, 10 → 16 位压缩指令(C 扩展)
11 → 32 位标准指令

4.2 C 扩展的常见压缩形式

标准指令C 压缩条件
addi rd, rd, 0 c.mv rd, rs —
addi sp, sp, -imm c.addi16sp rd=sp
lw rd, off(sp) c.lwsp rd, off 基址=sp
sw rs, off(sp) c.swsp rs, off 基址=sp
lw rd, off(rs1) c.lw rd, off(rs1) rd/rs1 在 s0-s1/a0-a5
jal ra, offset c.jal ra, offset 短偏移
jalr ra, rs1, 0 c.jalr ra, rs1 —
j offset c.j offset rd=x0

4.3 编译时控制

-march=rv32imac # 带 c,启用压缩
-march=rv32ima # 不带 c,强制 32 位(代码大但栈回溯简单)
-mno-relax # 关 linker relaxation(⑨专题)

调试死机频繁阶段,可以临时 -march=rv32ima 关掉 C,让栈回溯简单点;量产再开回来省代码。这是个实用权衡。


五、调用约定(ILP32/LP64)详解

5.1 参数传递

  • 整型参数:a0-a7(最多 8 个),超出走栈
  • 返回值:a0/a1(最多 2 个)
  • 浮点参数(有 F/D 扩展):fa0-fa7(这是 -mabi 带 f/d 的事)
  • 结构体返回:小结构体(a0/a1 装下)走寄存器,大的隐式传指针

5.2 栈帧布局

标准函数 prologue:

func:
addi sp, sp, -16 # 开栈帧
sw ra, 12(sp) # 存返回地址(非叶子函数必须)
sw s0, 8(sp) # 存 callee-saved
addi s0, sp, 16 # s0 = 帧指针(可选)
…
lw s0, 8(sp)
lw ra, 12(sp)
addi sp, sp, 16
ret

5.3 ILP32 / LP64 / f / d 后缀

ABI整数位宽浮点参数传递-march 典型
ilp32 32 软浮点(走整数寄存器/栈) rv32imac
ilp32f 32 单精度走 fa0-fa7 rv32imafc
ilp32d 32 双精度走浮点寄存器 rv32imafdc
lp64 64 软浮点 rv64imac
lp64d 64 双精度 rv64imafdc

ABI 必须全工程一致——以ROM 固化为例 ROM 固化的函数和你程序的 -mabi 必须一致,否则浮点参数传递方式不同,调用 sqrtf 拿到错误参数。ABI 不匹配不会编译报错,运行时才崩——因为编译器各自按自己的约定生成代码,链接器看符号匹配就链接上了。这种 bug 极难查。固化前 -march/-mabi 双方锁死。


六、gp 相对寻址:RISC-V 特有

gp(x3)在启动时被设成 .sdata 段的中点,之后访问 ±2KB 内的小全局变量用 gp 相对寻址,2 条指令变 1 条。

# 不用 gp(2条):
lui a0, %hi(var)
lw a0, %lo(var)(a0)

# 用 gp(1条,若 var 在 gp ±2KB 内):
lw a0, %gprel(var)(gp)

  • -msmall-data-limit=8:≤8 字节的全局/静态放 .sdata/.sbss,走 gp。N 别设太大(gp 范围 ±2KB),否则寻址不到。
  • 启动汇编必须设 gp:la gp, __global_pointer$。忘了设 gp,gp 相对寻址全崩——这是启动汇编必做 5 件事之一(⑦专题)。

七、ARM vs RISC-V 寄存器/调用约定对照

ARM Cortex-MRISC-V
通用寄存器数 16(R0-R15) 32(x0-x31)
返回地址 LR(R14) ra(x1)
栈指针 SP(R13),MSP/PSP 两套 sp(x2),单栈(软件切)
帧指针 可选 R7/R11 可选 s0(x8)
参数寄存器 R0-R3(4个) a0-a7(8个)
callee-saved R4-R11 s0-s11
caller-saved R0-R3,R12,LR t0-t6,a0-a7,ra
全局指针 无 gp(x3)——RISC-V 特有
线程指针 无(用 PSP) tp(x4)

八、本篇小结

  • 32 寄存器 + ABI 别名,核心是 caller-saved(t/a/ra) vs callee-saved(s/sp/gp/tp) 的保存责任划分
  • ra 是返回地址寄存器,非叶子函数 prologue 必压 ra
  • base ISA 是 load-store 架构,运算不碰内存,只有 lw/sw 访存
  • 六种指令格式,立即数位段错位拼接(B/J 型),手动解码要拼位段
  • RVC 压缩指令 16/32 混合,省 ~30%
  • 调用约定:a0-a7 传参、a0-a1 返回、ILP32/LP64 + f/d 后缀决定浮点传递
  • ABI 必须全工程一致,ROM 固化尤其要锁死 -march/-mabi

速查表

想干啥用啥
函数调用 jal ra, func(近)/ call func(远)
函数返回 ret(=jalr x0,ra,0)
加载立即数 li(伪指令,展开 lui+addi)
寄存器拷贝 mv(=addi rd,rs,0)
访问内存 lw/sw(唯一碰内存的指令)
常量 0 x0(zero),写它被忽略
存返回地址 ra(x1),非叶子函数要压栈
callee-saved s0-s11,sp(被调方保)
caller-saved t0-t6,a0-a7,ra(调用方保)
参数 a0-a7(8个)
返回值 a0-a1(2个)
关 RVC -march=rv32ima(去 c)
gp 寻址范围 ±2KB,-msmall-data-limit 控

赞(0)
未经允许不得转载:171主机测评 » 【risc-v专栏02】base ISA 与 ABI 寄存器约定
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址