前置复习:常量构造、函数调用与栈
前置复习目录 · 上一章 · 下一章:数据通路与性能
1. 为什么低 12 位会影响高 20 位
lui 填充高 20 位,addi 加的是 signed 12 位数。若低 12 位 bit 11=1,addi 使用负值,必须先给高位加一补偿。
将 32 位常量 C 按位串分成两部分:
将 H 的低 20 位放入 lui,\(\ell\) 放入 addi,RV32 下得到 C。例:
# 低 12 位 0x678 < 0x800,直接相加
lui x5, 0x12345
addi x5, x5, 0x678 # 0x12345678
# 低 12 位 0xABC 按 signed 12 位解释为 -1348 = -0x544
lui x5, 0x87655 # 高位 0x87654 加一
addi x5, x5, -1348 # 低 32 位为 0x87654ABC
addi ...,0xABC 把立即数写成正数 2748,会超出 signed 12 位范围,不能靠汇编器自动理解成 −1348。
RV64 的边界:lui 会把形成的 32 位结果符号扩展至 64 位。要通用地构造“任意 32 位常量再符号扩展”,可用 lui + addiw,保证加法后先截成 32 位再扩展。例如 0x7FFFFFFF 应用 lui rd,0x80000; addiw rd,rd,-1;若在 RV64 只用 addi,会错误地得到 0xFFFFFFFF7FFFFFFF。下节手稿给定常量不跨这一边界,其 addi 序列仍然正确。RV64 的 LUI / ADDIW 规则。
2. 手稿例题:构造 0x1234567887654ABC
目标是将任意位串 0x12345678_87654ABC 放入 x5。先构造高低两个 32 位部分,再合并:
lui x5, 0x12345
addi x5, x5, 0x678 # x5 = 0x0000000012345678
slli x5, x5, 32 # x5 = 0x1234567800000000
lui x6, 0x87655
addi x6, x6, -1348 # x6 = 0xFFFFFFFF87654ABC
slli x6, x6, 32
srli x6, x6, 32 # x6 = 0x0000000087654ABC
add x5, x5, x6 # x5 = 0x1234567887654ABC
slli + srli 保留低 32 位并清除高 32 位,即零扩展(zero extension);这里必须使用逻辑右移 srli,不能用 srai。若未清除 x6 的符号扩展位,就会干扰目标的高 32 位。
最后两部分的有效位互不重叠,所以用 or x5,x5,x6 也能合并;加法在这里不会产生跨半部进位。工程中可直接用 li x5,0x1234567887654ABC,考试要求展开时再给出基础指令。
3. 寄存器保存约定
ISA 规定 jal/jalr 怎样改变寄存器和 PC;调用约定(calling convention)规定软件如何传参、返回和保存状态。
| ABI 名称 | 编号 | 用途 | 跨调用是否保留 |
|---|---|---|---|
| zero | x0 | 常量零 | 恒为 0 |
| ra | x1 | return address,返回地址 | 否;仍需使用旧返回地址时自行保存 |
| sp | x2 | stack pointer,栈指针 | 是,返回时恢复 |
| gp / tp | x3 / x4 | 全局指针 / 线程指针 | 普通过程不要任意改作临时量 |
| t0–t2 | x5–x7 | temporary,临时值 | 否,caller-saved |
| s0–s1 | x8–x9 | saved registers;s0 也可作 fp | 是,callee-saved |
| a0–a7 | x10–x17 | arguments;a0/a1 也用于常见返回值 | 否,caller-saved |
| s2–s11 | x18–x27 | saved registers | 是,callee-saved |
| t3–t6 | x28–x31 | temporary,临时值 | 否,caller-saved |
调用者保存(caller-saved)意味着被调用函数可改写:调用结束后仍需使用的值才要由 caller 保护。被调用者保存(callee-saved)意味着 callee 若使用并修改该寄存器,就必须恢复原值。无需每次机械地保存全部寄存器。
手稿中临时寄存器范围应完整记为 t0–t6;不是全部 a 寄存器都用作普通返回值。标准整数规则见 RISC-V psABI。
4. 栈、序言与尾声
栈(stack)向低地址增长,分配空间用 addi sp,sp,-N,释放用 addi sp,sp,N。标准 RV32I/RV64I 整数 ABI 要求 sp 在过程执行期间维持 16 字节对齐,N 通常取 16 的倍数。以下采用 RV64:
# Prologue:分配 32 字节,保存将被改写的状态
addi sp, sp, -32
sd ra, 24(sp)
sd s0, 16(sp)
# Body:函数体,可修改 s0、调用其他函数
# ...
# Epilogue:恢复状态、释放栈帧、返回
ld s0, 16(sp)
ld ra, 24(sp)
addi sp, sp, 32
ret
保存与恢复地址必须配对;不同函数可采用不同帧布局。修改 sp 只分配地址范围,不会自动保存寄存器。RV64 的完整寄存器用 sd/ld 保存;sw/lw 只能保存/恢复 32 位。
复用课程笔记的自绘栈帧图,实际偏移以函数的分配为准。
叶子函数(leaf function)不再调用其他函数,不一定需要保存 ra;非叶子函数(non-leaf function)若要在调用后返回,需在下一次 jal 覆盖 ra 前保留旧值。尾调用(tail call)等特殊形式另行分析,不必机械套用“所有函数都保存 ra”。
5. 手稿例题:嵌套调用 g(g(a,b),c+d)
设 C 函数为:
采用 RV64 LP64、C 的 int 为 32 位,假定 c+d 不发生 C 的有符号溢出。参数 a/b/c/d 在 a0/a1/a2/a3,返回值在 a0:
f:
addi sp, sp, -16
sd ra, 8(sp) # 返回 f 的调用者所需的位置
addw t0, a2, a3 # 32 位 c+d,结果符号扩展到 64 位
sd t0, 0(sp) # t0 是 caller-saved,不能直接跨 g 保留
jal ra, g # a0 = g(a,b)
ld a1, 0(sp) # 第二次调用参数:(a0, c+d)
jal ra, g # a0 = g(g(a,b), c+d)
ld ra, 8(sp)
addi sp, sp, 16
ret
第一个 g 可以破坏 t0 和 a2/a3,所以先把 c+d 存入栈;两次 jal 都会改写 ra,所以在整个函数外层保存一次旧 ra。第二次 g 的返回值已在 a0,无需额外搬移。
若处理的是 64 位 long,c+d 可改用 add;RV64 并不意味着 C 的 int 加法应一律写成 64 位 add。更详细的叶子函数、callee-saved 与帧指针讨论见过程调用章节。