06. 立即数操作与移位
上一章介绍的 R-type 指令操作数全部来自寄存器。但在实际程序中,大量操作数是编译期已知的常量——循环步长、数组偏移、位掩码、地址对齐值。为每一个常量都先加载到寄存器再运算显然低效。RISC-V 用 I-type 指令格式将"立即数"直接编码在指令中,一条指令完成常量运算。
I-type 指令格式
I-type(Immediate type)将 R-type 的 funct7 + rs2 字段(共 12 位)替换为一个 12 位立即数。其他字段位置完全相同。
位域布局:
┌────────────────────┬───────┬─────┬───────┬─────────┐
│ imm[11:0] │ rs1 │funct3│ rd │ opcode │
└─────────12─────────┴───5───┴──3───┴───5───┴────7────┘12 位立即数能表示的范围是有限的。但对于函数内部的局部常量(计数器、小偏移、常见掩码),12 位覆盖了绝大多数情况。更大的立即数用 LUI+ADDI 组合构造(第 9 章详述)。
立即数的符号扩展
12 位立即数被取出后,符号扩展至 64 位才参与运算:即用 imm[11](符号位)填充高 52 位。
imm[11] = 0 → 高位填 0(正数或小正数)
imm[11] = 1 → 高位填 1(负数)可表示的范围(经符号扩展后):
- 有符号视角:[-2048, 2047](即 -2^11 到 2^11 - 1)
- 无符号视角(如果指令将其视为无符号):[0, 4095]
注意:是指令语义决定将符号扩展后的值当有符号还是无符号。例如 addi 将其当作有符号立即数与 rs1 相加,而 sltiu 虽然也做符号扩展,但比较语义将其视为无符号。对于 64 位运算结果,符号扩展确保了负值立即数的数学正确性。
立即数范围的硬件约束
为什么只给 12 位?指令总长只有 32 位——opcode(7b)+ rd(5b)+ funct3(3b)+ rs1(5b)占用 20 位,留给立即数只剩 12 位。I-type 已是在保留字段一致性的前提下最大化立即数宽度。更大的格式(U-type)给 20 位立即数,但牺牲了 rs1/rs2 字段。
实际影响:addi t0, t1, 5000 在汇编时报错——5000 超过 12 位有符号表示范围。此时需要用 li 伪指令或 lui+addi 手动构造:
# addi t0, t1, 5000 → 非法!立即数超出范围
li t2, 5000 # 汇编器自动展开为 lui + addi(或更优序列)
add t0, t1, t2 # t0 = t1 + 5000算术立即数指令
| 指令 | opcode | funct3 | 操作描述 |
|---|---|---|---|
addi rd, rs1, imm | 0010011 | 000 | rd = rs1 + sext(imm) |
slti rd, rs1, imm | 0010011 | 010 | rd = (rs1 < sext(imm)) ? 1 : 0(有符号) |
sltiu rd, rs1, imm | 0010011 | 011 | rd = (rs1 < sext(imm)) ? 1 : 0(无符号) |
注意:I-type 算术指令的 opcode 是 0010011,与 R-type 的 0110011 不同——opcode 的第 5 位区分了寄存器操作和立即数操作。
addi 的三种典型用法
# 1. 寄存器 + 常量偏移
addi t0, t1, 8 # t0 = t1 + 8
# 2. 加载小立即数(li 伪指令的基础)
addi t0, x0, 42 # t0 = 0 + 42 = 42 → 等效于 li t0, 42
# 3. 栈指针偏移(函数内局部变量访问)
addi sp, sp, -16 # 栈向下增长 16 字节第 3 种用法是函数序言(prologue)的核心——addi sp, sp, -N 分配栈帧空间,addi sp, sp, N 在函数尾声(epilogue)回收。
为什么没有 subi
RISC-V 不设 subi 指令——减法等价于加负数。subi rd, rs1, imm 写为 addi rd, rs1, -imm:
# 需要 t0 = t1 - 10 时:
addi t0, t1, -10 # 汇编器对 -10 做编码: 12 位补码 0xFF6汇编器在汇编阶段计算 -10 的 12 位补码,编码为立即数。如果 imm=10 的 12 位编码是 0x00A,imm=-10 的 12 位编码是 0xFF6——符号扩展后恰好是 -10。无需单独的 subi 指令,指令条数就此减少。
slti / sltiu 的陷阱
sltiu 的语义容易让人困惑——立即数先做了符号扩展,再用无符号比较 rs1 < sext(imm)。这就是为什么 sltiu 在比较中把 sltiu t0, t1, -1 的 -1(符号扩展后是 0xFFFFFFFFFFFFFFFF)作为极大的无符号数来比较:
li t1, 100
slti t2, t1, -1 # t2 = 0(有符号 100 < -1 不成立)
sltiu t3, t1, -1 # t3 = 1(无符号 100 < 0xFFFFFFFFFFFFFFFF 成立)
# 再看一个例子:最小负数
li t1, 0xFFFFFFFFFFFFFFFF # -1 有符号,最大值无符号
slti t2, t1, 0x7FF # t2 = 1(有符号 -1 < 2047 成立)
sltiu t3, t1, 0x7FF # t3 = 0(无符号 2^64-1 < 2047 不成立)逻辑立即数指令
| 指令 | opcode | funct3 | 操作描述 |
|---|---|---|---|
andi rd, rs1, imm | 0010011 | 111 | rd = rs1 & sext(imm) |
ori rd, rs1, imm | 0010011 | 110 | rd = rs1 | sext(imm) |
xori rd, rs1, imm | 0010011 | 100 | rd = rs1 ^ sext(imm) |
逻辑立即数指令的立即数同样做了符号扩展——但对于 AND/OR/XOR 操作,高位是 0 还是 1 对逻辑结果会有直接影响。因此使用 12 位以上的逻辑掩码时,高位由符号扩展决定:
# 低 12 位全 1 → imm = 0x7FF(符号位 = 0,符号扩展后高位全 0)
andi t0, t1, 0x7FF # t0 = t1 & 0x0000_0000_0000_07FF(正确:只保留低 11 位)
# 低 12 位设置 bit 11 = 1 → imm = 0xFFF(符号位 = 1,符号扩展后高位全 1)
andi t0, t1, 0xFFF # t0 = t1 & 0xFFFF_FFFF_FFFF_FFFF(所有位都保留,等价于 NOP!)第二个例子是一个微妙的 bug 来源——andi 的立即数经符号扩展后变成全 64 位掩码,导致操作失去实际效果。需要真 12 位以上掩码时,用 li + R-type and 的方式。
xori 实现 NOT
xori rd, rs, -1 等价于 NOT rd, rs(伪指令)。-1 的 12 位编码是 0xFFF,符号扩展后是 64 位全 1——异或全 1 = 按位取反:
xori t0, t1, -1 # t0 = ~t1(每一位翻转)→ 汇编器将其展开为 not t0, t1ori 加载无符号小立即数
ori t0, x0, 0xFF # t0 = 0 | 0x0000_0000_0000_00FF = 0xFF对 0-4095 范围内的无符号立即数,ori rd, x0, imm 是一条便捷的加载方式。不过汇编器的 li 伪指令通常会统一优化为最优序列——不需要手工选择 addi vs ori。
移位指令
移位指令在 RISC-V 中有两种形式:R-type(移位量在寄存器中)和 I-type(移位量是立即数)。I-type 变体用 slli/srli/srai 命名(末尾的 i 表示 immediate)。
移位量编码
对于 RV64I,移位量最多 63(2^6 - 1),需要 6 位。R-type 中取自 rs2 的低 6 位(rs2[5:0]),高 58 位被忽略。I-type 中,shamt(shift amount)取自 imm[5:0](6 位),I-type 的高 6 位 imm[11:6] 用作额外的 sub-opcode 编码:
| 指令 | opcode | funct3 | imm[11:6] | 操作描述 |
|---|---|---|---|---|
slli rd, rs1, shamt | 0010011 | 001 | 000000 | rd = rs1 << shamt |
srli rd, rs1, shamt | 0010011 | 101 | 000000 | rd = rs1 >> shamt(逻辑右移,补 0) |
srai rd, rs1, shamt | 0010011 | 101 | 010000 | rd = rs1 >> shamt(算术右移,补符号位) |
注意 slli 的 shamt 只有 6 位有效,I-type 立即数的高 6 位(imm[11:6])必须是全 0——规范要求,非零值保留给未来扩展。
三种移位的语义对比
li t0, -16 # t0 = 0xFFFF_FFFF_FFFF_FFF0
slli t1, t0, 2 # 逻辑左移 2: t1 = 0xFFFF_FFFF_FFFF_FFC0
srli t2, t0, 2 # 逻辑右移 2: t2 = 0x3FFF_FFFF_FFFF_FFFC(高位补 0)
srai t3, t0, 2 # 算术右移 2: t3 = 0xFFFF_FFFF_FFFF_FFFC(高位补符号位 1)→ -4移位 = 快速乘除
编译器将乘/除常数优化为移位指令是常规操作。理解这一点就能读懂很多优化后的汇编:
| 操作 | 移位 | 等价算术 |
|---|---|---|
slli rd, rs, n | 左移 n 位 | rs * 2^n(有符号和无符号通用) |
srli rd, rs, n | 逻辑右移 n 位 | rs / 2^n(无符号) |
srai rd, rs, n | 算术右移 n 位 | rs / 2^n(有符号,向下取整) |
有符号除法的重要差异:srai 的结果向负无穷取整(向下取整),而 C 语言的整数除法(/)向零取整。这意味着负数的移位除法结果可能与 C 代码直觉不符:
li t0, -5 # t0 = -5
srai t1, t0, 1 # t1 = -3(-5 >> 1 → -3,向下取整)
# C 的 -5 / 2 = -2(向零取整),所以编译器不能简单用 srai 替代有符号除法编译器对于 x / 2^n(有符号)会生成类似以下序列来纠正取整方向:
# 有符号除法 x / 8(向零取整)
srai t0, x, 63 # 提取符号位(0 或 -1)
srli t0, t0, 61 # t0 = 符号位 ? 7 : 0 (2^3 - 1 = 7)
add t0, x, t0 # 负数先加上 (2^n - 1)
srai t0, t0, 3 # 算术右移 3 位移位组合:数组索引
移位最常见的实战场景是数组索引计算——将 C 语言的 array[i] 翻译为 base + i * sizeof(element):
# 64 位整数数组: array[i]
# 设 base 在 t0(数组基地址),index 在 t1(数组下标)
slli t2, t1, 3 # t2 = index * 8(因为 sizeof(int64_t) = 8)
add t3, t0, t2 # t3 = base + index * 8 → 目标元素的地址
ld t4, 0(t3) # t4 = array[index]sizeof(element) = 2^n 是常见情况(int=4 在 RV64 中、指针=8、double=8),因此 slli 移位量是 2 或 3 出现最多。对于非 2 的幂大小的元素(如 24 字节结构体),编译器会用乘法指令(M 扩展)或乘常数展开:
# 结构体 size = 24 → 拆为 16 + 8
slli t2, t1, 3 # t2 = index * 8
slli t3, t1, 4 # t3 = index * 16
add t2, t2, t3 # t2 = index * 24
add t3, t0, t2 # t3 = base + index * 24
ld t4, 0(t3)移位实现位域提取
许多数据结构用位域(bitfield)打包多个小字段进一个整数。移位 + 掩码是提取和插入位域的标准手法:
# 从 t0 中提取 bit [15:8](第二个字节)
srli t1, t0, 8 # 右移 8 位,让目标字段到低位
andi t1, t1, 0xFF # 掩码保留低 8 位
# 组合多个位域: result = (field1 << 8) | field2
slli t0, a0, 8 # field1 左移 8 位腾出空间
or t0, t0, a1 # 低位放入 field2RV32 vs RV64 的移位宽度差异
在 RV32I 中,slli/srli/srai 的 shamt 只有 5 位(最大移位 31),I-type imm[11:5] = 7 位编码空间用于 sub-opcode。RV64I 需要 6 位 shamt(最大移位 63),因此 I-type imm[11:6] 被赋予新的含义区分 srli/srai。
对于 R-type 移位(sll/srl/sra),rs2 的低位位数随 XLEN 变化:
- RV32:rs2[4:0](5 位,最大移位 31)
- RV64:rs2[5:0](6 位,最大移位 63)
这意味着同一个 R-type 移位指令在 RV32 和 RV64 上执行,rs2 的有效位数不同——二进制兼容的代码需要注意移位量上限。
本章要点
- I-type 的 12 位立即数经符号扩展至 64 位参与运算——范围 [-2048, 2047]
- RISC-V 不设
subi和NOT专用指令——addi加负数立即数和xori异或 -1 完成等价功能,减少指令总数 - 移位 = 快速乘除 2^n:
slli(乘)、srli(无符号除)、srai(有符号除,向下取整) - RV64 中 I-type 移位指令的 shamt 为 6 位(imm[5:0]),imm[11:6] 区分
srli(000000)和srai(010000) - 逻辑立即数指令(andi/ori/xori)的立即数同样符号扩展——12 位以上掩码需注意符号位影响