04 指令系统
存储程序思想(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 位,问还能留几条零地址指令?
- 三地址:操作码 4 位,16 种组合用 15 条 → 剩下
1111做前缀 - 二地址:用
1111开头 + 4 位扩展 = 8 位操作码 → 16 种,用 15 条 → 又剩1111 1111 - 一地址:
1111 1111+ 4 位 = 12 位操作码 → 16 种,用 15 条 → 剩1111 1111 1111 - 零地址: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,三步走通任何架构
六、盲点自测
- 一地址指令的操作数另一个在哪?(隐含在累加器 ACC)
- 扩展操作码:16 位指令、4 位地址码,三地址 15 条后还能扩几条二地址?(15 条,前缀 1111)
- 哪些寻址方式取数不用访存?(立即、寄存器)
- 基址和变址谁是系统的、谁面向数组循环?(基址系统/重定位,变址用户/数组)
- 相对寻址的 EA 用取指前还是取指后的 PC?(取指后已自增的 PC)
- 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 思想的正源
💬 评论