跳转至

指令格式与寻址方式

动机:有限位宽里装下足够信息

处理器拿到的是一串位。要执行操作,它必须从中知道“做什么、读哪里、写哪里、常数是多少”。指令格式就是这些字段的编码协议。

以典型 32 位 RISC 指令为例,一条寄存器加法可能包含:

| funct7 | rs2 | rs1 | funct3 | rd | opcode |

opcode 给出大类,funct 细分操作,rs1/rs2 指定源寄存器,rd 指定目的寄存器。若有 32 个通用寄存器,一个寄存器编号需要:

\[ \log_2 32=5 \]

位。增加寄存器数量会减少访存,却要占用更多编码位并增大寄存器堆。

常见指令格式

RISC-V 用若干规则格式复用字段位置:

  • R 型:两个寄存器源和一个寄存器目的;
  • I 型:一个寄存器源、一个目的和立即数;
  • S 型:两个寄存器源与拆分的位移,用于 store;
  • B 型:两个比较源与 PC 相对分支位移;
  • U/J 型:容纳较大的高位立即数或跳转位移。

rs1rs2rd 尽量位于固定位置,可以让译码和读寄存器并行。立即数被拆开会让汇编者看起来别扭,但硬件连线更规则。

定长与变长

定长指令边界容易定位,取指、并行译码和流水线更简单;变长指令能提高代码密度,减少指令 cache 与内存带宽压力,却增加边界识别和译码复杂度。现代 ISA 也会折中,例如基础 32 位指令加 16 位压缩扩展。

寻址方式

寻址方式回答“有效地址或操作数怎样得到”。

立即寻址

常数直接在指令中:

addi x5, x6, 12

无需额外读内存,但常数范围受字段位数限制。

寄存器寻址

操作数在寄存器中:

add x5, x6, x7

速度快、编码规则,但寄存器数量有限。

基址加位移

load/store 常用:

lw x5, 16(x10)

有效地址为:

\[ EA=R[x10]+\operatorname{signext}(16). \]

它自然支持数组元素、结构体字段和栈帧。若要访问远地址,编译器需先构造更大的基址。

PC 相对寻址

分支或位置无关代码以当前 PC 为基准:

\[ target=PC+\operatorname{signext}(offset). \]

代码装载到不同地址时,近距离跳转仍然有效。位移通常按 2 字节或 4 字节对齐编码,以牺牲最低恒为零的位换更远范围。

间接寻址

寄存器保存目标地址,跳转或访存再使用它。函数指针、虚函数和动态链接依赖这类机制。灵活性的代价是目标更难预测,也可能需要多次访存。

对齐与字节序

若一个 4 B 数据放在 4 的倍数地址上,就称自然对齐。对齐访问通常只落在一个存储块或总线事务内;非对齐访问可能被拆成两次,甚至触发异常。

字节序规定多字节对象中最低有效字节位于低地址还是高地址。小端把最低有效字节放低地址,大端相反。字节序改变内存中的字节排列,不改变寄存器内数值本身。

最小推理例子

假设 x10 = 0x1000,内存地址 0x100C 保存 32 位整数 7。执行:

lw x5, 12(x10)

处理器先符号扩展立即数 12,用 ALU 得到 0x100C,再读 4 B 并写入 x5。这条指令的“加法”和“访存”是两个不同阶段,正是后续流水线中 load-use 冒险的来源。

自测

  1. 为什么增加立即数字段位数会压缩其他字段的空间?
  2. PC 相对寻址为何利于位置无关代码?
  3. 非对齐访问慢的根本原因是什么?