第四讲 · 指令系统、寻址与 MIPS 机器码
对应材料:2025 计组复习提纲“程序执行的基本原理”“指令系统”“MIPS 指令系统”“指令编码”。
CPU 不理解 C 语言,也不理解汇编助记符。它只会从存储器取出一串比特,按照指令系统规定解释字段,再驱动硬件完成动作。
C 源程序 → 编译器 → 汇编程序 → 汇编器 → 机器码
↓
硬盘中的可执行文件 → 加载到主存 → CPU 取指、译码、执行
这一讲站在软硬件边界上:向上说明一条 C 语句如何变成指令,向下说明 32 位机器码怎样选择寄存器、立即数和下一条 PC。
1. 先把整台计算机看成一个系统
课程概述先用四类功能认识计算机:
- 数据处理:ALU 与寄存器完成最基本的算术、逻辑运算;
- 数据存储:存储器保存程序和数据,每个可寻址单元有地址;
- 数据移动:输入输出部件在机器内部与外部设备之间交换数据;
- 控制:控制器解释指令,产生控制信号,让各部件按顺序协作。
对应到经典冯·诺依曼机器,就是运算器、控制器、存储器、输入与输出五个部分。它最重要的“存储程序”思想是:
- 指令和数据都用二进制表示并存入存储器;
- PC 给出下一条指令地址;
- CPU 反复取指、译码、取操作数、执行、保存结果;
- 顺序执行时 PC 前进,分支或跳转时由指令改写。
从高级语言、编译器、操作系统、ISA,到处理器、存储器与 I/O,是一层层抽象:上一层只依赖下一层承诺的接口,不必知道所有实现细节。课程用大型机、小型机、PC、云与仓库级计算机的演进,以及摩尔定律、类脑芯片和量子计算等例子说明:实现技术会持续变化,抽象接口让软件和硬件仍能分层演进。
2. ISA 是软硬件之间的契约
指令集体系结构(Instruction Set Architecture,ISA)规定程序员能看到的机器:
- 有哪些指令以及每条指令的语义;
- 有哪些可见寄存器;
- 操作数放在哪里、怎样寻址;
- 指令如何编码;
- 数据类型、字节序、异常等软件可见行为。
同一 ISA 可以由不同内部结构实现。例如单周期、流水线处理器的内部完全不同,只要执行每条 MIPS 指令得到相同的软件可见结果,就都能运行同一份机器码。
这也解释两个常见问题:
- 不同计算机可以使用相同 ISA,因为它们实现了同一份契约;
- 同一计算机不能直接执行另一 ISA 的机器码,因为相同比特会被解释成不同动作。
3. 一条指令包含什么
机器指令至少要表达:
- 做什么:操作码;
- 从哪里取操作数:寄存器编号、立即数或存储器地址信息;
- 把结果放哪里:目的寄存器或存储单元;
- 下一条指令在哪里:通常顺序执行,分支和跳转会改 PC。
指令类型可分为数据传送、算术逻辑、控制转移、系统控制等。MIPS 的设计原则是格式少而规则、寄存器运算、访存由专门的 load/store 指令完成。
4. 一般寻址方式
有效地址(Effective Address,EA)是真正访问的地址;指令里的地址字段可能只是形式地址。
| 方式 | 计算 | 直观理解 |
|---|---|---|
| 立即寻址 | 操作数就是指令中的常量 | 数写在指令里 |
| 寄存器寻址 | 操作数为 | 编号指向寄存器 |
| 基址寻址 | 基地址加位移 | |
| 相对寻址 | 相对当前位置跳转 |
MIPS 把寻址方式隐含在操作码中,不再单独留一个“寻址方式字段”。例如 lw 天生使用基址加偏移,beq 天生使用 PC 相对寻址。
5. MIPS 的三个核心约束
5.1 固定 32 位指令
所有指令等长,取指和译码边界固定,利于流水线。代价是字段空间有限:寄存器编号、立即数和操作码要在 32 位中权衡。
5.2 32 个 32 位通用寄存器
寄存器编号占 5 位,因为 。寄存器本身没有“有符号/无符号类型”;同一串比特按什么方式解释,由当前指令决定。
$0 或 $zero 永远读出 0,写入会被丢弃。常量 0 太常用,专门保留一个零寄存器能减少指令种类。
5.3 Load/Store 体系结构
算术逻辑指令的操作数来自寄存器,不能让主存单元直接参加运算:
lw $t0, 0($s0) # 主存 → 寄存器
add $t1, $t0, $s1 # 寄存器之间运算
sw $t1, 4($s0) # 寄存器 → 主存
这让 ALU 数据通路更规则,也让访存延迟与普通运算更容易分开处理。
6. R 型指令
R 型用于两个寄存器参与的运算:
31 26 25 21 20 16 15 11 10 6 5 0
+-----------+--------+--------+--------+---------+---------+
| opcode(6) | rs(5) | rt(5) | rd(5) |shamt(5) |funct(6) |
+-----------+--------+--------+--------+---------+---------+
opcode:R 型常为 0;rs、rt:两个源寄存器;rd:目的寄存器;shamt:立即移位量;funct:与opcode一起确定具体运算。
例如 add $8, $9, $10:
| 字段 | 十进制值 |
|---|---|
opcode | 0 |
rs | 9 |
rt | 10 |
rd | 8 |
shamt | 0 |
funct | 32 |
拼接后为
000000 01001 01010 01000 00000 100000
即 0x012A4020。
0x00000000 也能按 R 型解释:它是 sll $0,$0,0。因为读写零寄存器且移 0 位,没有可见效果,正好用作 nop。
7. I 型指令
I 型给 16 位立即数留下空间:
31 26 25 21 20 16 15 0
+-----------+--------+--------+----------------------------+
| opcode(6) | rs(5) | rt(5) | immediate(16) |
+-----------+--------+--------+----------------------------+
同一个字段在不同指令中含义不同:
addi/ori:rt是目的寄存器,rs是源寄存器;lw/sw:rs是基址,rt是加载目标或存储来源;beq/bne:rs、rt都是比较对象,立即数是分支位移。
7.1 符号扩展与零扩展
- 地址偏移、
addi、分支位移要保留正负,使用符号扩展; andi、ori把立即数当位模式,使用零扩展。
例如 0xFFFF:符号扩展为 0xFFFFFFFF,零扩展为 0x0000FFFF。扩展方式不是由比特自己决定,而由指令语义决定。
8. 分支指令为何用 PC 相对寻址
beq 的目标地址为
注意三件事:
- 基准是分支指令的下一条,即 ;
- 立即数按补码解释,可向前或向后跳;
- 指令按 4 字节对齐,所以位移按“指令字”计数,硬件左移 2 位变成字节位移。
如果目标标号地址为 ,汇编器填写
8.1 手算示例
0x00400020: beq $t0, $t1, Target
0x00400024: ...
0x00400028: ...
0x0040002C: Target: ...
因此
不要从分支指令自身开始数,也不要把字节差 8 直接填进立即数字段。
9. J 型指令
J 型只有两个字段:
31 26 25 0
+-----------+----------------------------------------------+
| opcode(6) | instr_index(26) |
+-----------+----------------------------------------------+
目标地址按课程中的 32 位 MIPS 模型拼接:
低两位补 0 是因为指令 4 字节对齐;高四位沿用当前区域,所以一次 J 型跳转只能在同一个 256 MB 区域内。跨区域可先构造完整地址,再通过寄存器跳转。
10. 一条汇编语句怎样变成机器码
课件把手工汇编整理为五步:
- 判断 R、I、J 类型;
- 按格式写出字段;
- 查指令表与寄存器表,填各字段十进制值;
- 每个字段按固定宽度转二进制,负立即数写补码;
- 拼成 32 位,再按 4 位一组转十六进制。
最容易错的不是进制转换,而是字段语义:
add rd,rs,rt的书写顺序与编码字段顺序不同;lw rt,offset(rs)中rs是基址;sll rd,rt,shamt的rs固定为 0;- 分支立即数由标号地址计算,跳转索引则取目标地址的中间 26 位。
11. 汇编器还做了什么
汇编器不只逐行翻译:
- 建立符号表,把标号解析为地址;
- 展开伪指令;
- 计算 PC 相对位移与跳转索引;
- 生成机器码和重定位信息。
所以源程序中的一行不一定对应一条机器指令。后面看到 li、la、move 等伪指令时,要区分方便人写的 MAL 与硬件真正执行的 TAL。
12. 本讲的统一视角
一条机器码同时连接三层:
- 语义层:程序员想做加法、访存或跳转;
- 编码层:操作码、寄存器编号和立即数字段如何排列;
- 硬件层:这些字段怎样接到寄存器堆、扩展器、ALU 和下一 PC 逻辑。
下一讲先站在程序员视角写 MIPS;再下一讲把这些字段真正接进单周期 CPU。