04 指令系统

📚 本文是 计算机组成原理 的第 4 篇,相关系列见 基础与理论

存储程序思想(01-系统概述)说"程序以二进制存在存储器里"——那个二进制就是指令。指令系统(指令集架构 ISA)是软件和硬件的分界线:往上是编译器看得见的抽象,往下是 CPU 实打实的电路。本章三件事:指令长什么样(格式与扩展操作码)、操作数从哪来(十种寻址方式)、设计取向(CISC vs RISC)。

一、指令格式

一条指令 = 操作码(干什么)+ 地址码(对谁干)

按地址码个数分类:

形式 特点
零地址 NOP、栈顶运算(push/pop 架构) 无需操作数地址,或隐含在栈顶
一地址 ACC ← ACC OP M(累加器隐含) 只给一个操作数
二地址 ADD R1, R2 → R1 ← R1 + R2 最常见
三地址 ADD R1, R2, R3 结果单独存放(RISC 风格)

⚠️ 定长指令字 vs 变长指令字:RISC 用定长(好流水线),x86 用变长(1~15 字节,省空间但解码噩梦——05-CPU 里"x86 解码复杂"的伏笔在这)。

二、扩展操作码:不定长操作码的设计

操作码不必等长:短指令给常用操作,长指令留足地址空间——哈夫曼编码思想在指令集里的应用。

手算例题(408 必考):指令字长 16 位,要求三地址指令 15 条、二地址指令 15 条、一地址指令 15 条,每个地址码 4 位,问还能留几条零地址指令?

  1. 三地址:操作码 4 位,16 种组合用 15 条 → 剩下 1111前缀
  2. 二地址:用 1111 开头 + 4 位扩展 = 8 位操作码 → 16 种,用 15 条 → 又剩 1111 1111
  3. 一地址:1111 1111 + 4 位 = 12 位操作码 → 16 种,用 15 条 → 剩 1111 1111 1111
  4. 零地址:12 个 1 做前缀 + 4 位 = 16 条

规律口诀:每往下一个地址级,可用组合 = 16 − 已用 15 = 1 条前缀继续扩。考试常反过来问"还剩多少条",就是顺着这条链乘下来。

三、寻址方式:操作数到底在哪

指令里写的不是数据本身,是"数据的地址的地址的……"——寻址方式决定这一串 dereference 要做几次。

十种寻址方式全表

(EA = 有效地址,最终算出来的操作数真实地址;A = 指令中给的地址码)

寻址方式 有效地址 EA 访存次数(取数) 特点/用途
立即寻址 操作数=A 本身 0 最快,但位数受限;装常数
直接寻址 EA = A 1 简单;寻址范围受 A 位数限制
间接寻址 EA = M[A] 2(先取 A 的内容再取数) 扩大范围;慢
寄存器寻址 操作数在寄存器 Ri 0 快、指令短
寄存器间接 EA = (Ri) 1 常用于指针
相对寻址 EA = (PC) + A 1 程序浮动/转移(A 是偏移量,补码可正可负)
基址寻址 EA = (基址寄存器 BR) + A 1 操作系统用:重定位/多道程序,BR 由 OS 管,用户改不了
变址寻址 EA = (变址寄存器 IX) + A 1 用户用:数组循环遍历,A 是数组首址、IX 是下标
堆栈寻址 EA = (SP)(隐含) 1 push/pop,函数调用
页面寻址 EA 高位 = PC 高位,低位 = A 1 分页思想的简化版

基址 vs 变址——最容易混的一对

维度 基址寻址 变址寻址
谁的内容是"基准" 基址寄存器(系统给的大基准) 指令里的 A(程序内的小基准)
谁变 A 变(用户改位移) IX 变(用户改下标)
面向 系统:程序重定位、进程隔离 用户:数组、字符串循环

💡 一句话记忆:基址是"搬家不改代码"(OS 管),变址是"循环遍历数组"(用户管)。x86 的 mov eax, [ebx+esi*4] 就是基址+变址+比例因子的组合。

⚠️ 访存次数是 408 高频陷阱:立即 0 次、寄存器 0 次、直接/相对/基址/变址 1 次、间接 2 次。若问"执行一条指令共访存几次",还要加上取指令本身的 1 次(变长指令集里可能更多)。

相对寻址的转移距离

A 是补码偏移,所以一条 jmp -4 能向上跳,jmp +100 向下跳。取指后 PC 已指向下一条指令,所以 EA = (PC)已更新 + A——408 计算题最容易踩的点:用更新后的 PC,别用取指前的

四、CISC vs RISC

维度 CISC(复杂指令集) RISC(精简指令集)
代表 x86 / x86-64 ARM、RISC-V、MIPS
指令 多(数百条)、变长 少(<200 条)、定长
寻址方式 丰富(十几种) 精简(几种)
访存 多种指令都可访存 只有 Load/Store 指令访存
执行 各指令周期差异大 绝大多数单周期,利于流水线
控制 微程序控制为主 **组合逻辑(硬布线)**为主
寄存器 少(x86 经典 8 个通用) 多(ARM/RISC-V 32 个)
优化方向 用指令丰富度换程序短 用编译器 + 流水线换速度

💡 现代融合:x86 外部是 CISC 指令集,**内部把它翻译成类 RISC 微操作(μop)**再进流水线——表面 CISC、内心 RISC。ARM 赢在移动端功耗,x86 赢在兼容性生态。

五、x86 与 RISC-V 一瞥

  • x86-64:变长指令、两种端序传统(实际小端)、8→16 个通用寄存器;mov rax, [rbx] 一条指令横跨内存访问
  • RISC-V:开源 ISA,定长 32 位,只有 add/sub 等基础指令 + load/store 访存;指令少到"不设除法溢出标志"也能活
  • 读汇编的通用套路:看操作码 → 找寻址方式 → 心里算 EA,三步走通任何架构

六、盲点自测

  1. 一地址指令的操作数另一个在哪?(隐含在累加器 ACC)
  2. 扩展操作码:16 位指令、4 位地址码,三地址 15 条后还能扩几条二地址?(15 条,前缀 1111)
  3. 哪些寻址方式取数不用访存?(立即、寄存器)
  4. 基址和变址谁是系统的、谁面向数组循环?(基址系统/重定位,变址用户/数组)
  5. 相对寻址的 EA 用取指前还是取指后的 PC?(取指后已自增的 PC
  6. RISC"只有哪两类指令能访存"?(Load / Store)

七、动手玩

# x86-64 反汇编看看"变长指令"长什么样(macOS 自带)
echo -n 'int main(){return 1+2;}' > /tmp/t.c && clang /tmp/t.c -o /tmp/t
objdump -d /tmp/t | head -30     # 观察每条指令的字节数都不一样

有 Linux 机器的话,对比 gcc -S 同一段代码的 x86 输出和 RISC-V 输出(riscv64-unknown-elf-gcc 或 Compiler Explorer 网页版),直观感受定长 vs 变长。

参考资料

  • 王道《计算机组成原理考研复习指导》第 4 章
  • CSAPP 第 3 章(x86-64 汇编,把寻址方式用到真实代码里)
  • 《计算机组成与设计:硬件/软件接口》Patterson & Hennessy——RISC 思想的正源

⬅️ 存储系统 🏠 00-基础与理论 ➡️ CPU