Skip to content
Published at:

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 手动构造:

asm
# addi t0, t1, 5000  → 非法!立即数超出范围
li   t2, 5000          # 汇编器自动展开为 lui + addi(或更优序列)
add  t0, t1, t2        # t0 = t1 + 5000

算术立即数指令

指令opcodefunct3操作描述
addi rd, rs1, imm0010011000rd = rs1 + sext(imm)
slti rd, rs1, imm0010011010rd = (rs1 < sext(imm)) ? 1 : 0(有符号)
sltiu rd, rs1, imm0010011011rd = (rs1 < sext(imm)) ? 1 : 0(无符号)

注意:I-type 算术指令的 opcode 是 0010011,与 R-type 的 0110011 不同——opcode 的第 5 位区分了寄存器操作和立即数操作。

addi 的三种典型用法

asm
# 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

asm
# 需要 t0 = t1 - 10 时:
addi t0, t1, -10      # 汇编器对 -10 做编码: 12 位补码 0xFF6

汇编器在汇编阶段计算 -10 的 12 位补码,编码为立即数。如果 imm=10 的 12 位编码是 0x00Aimm=-10 的 12 位编码是 0xFF6——符号扩展后恰好是 -10。无需单独的 subi 指令,指令条数就此减少。

slti / sltiu 的陷阱

sltiu 的语义容易让人困惑——立即数先做了符号扩展,再用无符号比较 rs1 < sext(imm)。这就是为什么 sltiu 在比较中把 sltiu t0, t1, -1-1(符号扩展后是 0xFFFFFFFFFFFFFFFF)作为极大的无符号数来比较:

asm
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 不成立)

逻辑立即数指令

指令opcodefunct3操作描述
andi rd, rs1, imm0010011111rd = rs1 & sext(imm)
ori rd, rs1, imm0010011110rd = rs1 | sext(imm)
xori rd, rs1, imm0010011100rd = rs1 ^ sext(imm)

逻辑立即数指令的立即数同样做了符号扩展——但对于 AND/OR/XOR 操作,高位是 0 还是 1 对逻辑结果会有直接影响。因此使用 12 位以上的逻辑掩码时,高位由符号扩展决定:

asm
# 低 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 = 按位取反:

asm
xori t0, t1, -1     # t0 = ~t1(每一位翻转)→ 汇编器将其展开为 not t0, t1

ori 加载无符号小立即数

asm
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 编码:

指令opcodefunct3imm[11:6]操作描述
slli rd, rs1, shamt0010011001000000rd = rs1 << shamt
srli rd, rs1, shamt0010011101000000rd = rs1 >> shamt(逻辑右移,补 0)
srai rd, rs1, shamt0010011101010000rd = rs1 >> shamt(算术右移,补符号位)

注意 slli 的 shamt 只有 6 位有效,I-type 立即数的高 6 位(imm[11:6])必须是全 0——规范要求,非零值保留给未来扩展。

三种移位的语义对比

asm
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 代码直觉不符:

asm
li   t0, -5           # t0 = -5
srai t1, t0, 1        # t1 = -3(-5 >> 1 → -3,向下取整)
# C 的 -5 / 2 = -2(向零取整),所以编译器不能简单用 srai 替代有符号除法

编译器对于 x / 2^n(有符号)会生成类似以下序列来纠正取整方向:

asm
# 有符号除法 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)

asm
# 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 扩展)或乘常数展开:

asm
# 结构体 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)打包多个小字段进一个整数。移位 + 掩码是提取和插入位域的标准手法:

asm
# 从 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       # 低位放入 field2

RV32 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 不设 subiNOT 专用指令——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 位以上掩码需注意符号位影响