第四讲 · 指令系统、寻址与 MIPS 机器码

对应材料:2025 计组复习提纲“程序执行的基本原理”“指令系统”“MIPS 指令系统”“指令编码”。

CPU 不理解 C 语言,也不理解汇编助记符。它只会从存储器取出一串比特,按照指令系统规定解释字段,再驱动硬件完成动作。

C 源程序 → 编译器 → 汇编程序 → 汇编器 → 机器码
                                    ↓
硬盘中的可执行文件 → 加载到主存 → CPU 取指、译码、执行

这一讲站在软硬件边界上:向上说明一条 C 语句如何变成指令,向下说明 32 位机器码怎样选择寄存器、立即数和下一条 PC。

1. 先把整台计算机看成一个系统

课程概述先用四类功能认识计算机:

  • 数据处理:ALU 与寄存器完成最基本的算术、逻辑运算;
  • 数据存储:存储器保存程序和数据,每个可寻址单元有地址;
  • 数据移动:输入输出部件在机器内部与外部设备之间交换数据;
  • 控制:控制器解释指令,产生控制信号,让各部件按顺序协作。

对应到经典冯·诺依曼机器,就是运算器、控制器、存储器、输入与输出五个部分。它最重要的“存储程序”思想是:

  1. 指令和数据都用二进制表示并存入存储器;
  2. PC 给出下一条指令地址;
  3. CPU 反复取指、译码、取操作数、执行、保存结果;
  4. 顺序执行时 PC 前进,分支或跳转时由指令改写。

从高级语言、编译器、操作系统、ISA,到处理器、存储器与 I/O,是一层层抽象:上一层只依赖下一层承诺的接口,不必知道所有实现细节。课程用大型机、小型机、PC、云与仓库级计算机的演进,以及摩尔定律、类脑芯片和量子计算等例子说明:实现技术会持续变化,抽象接口让软件和硬件仍能分层演进。

2. ISA 是软硬件之间的契约

指令集体系结构(Instruction Set Architecture,ISA)规定程序员能看到的机器:

  • 有哪些指令以及每条指令的语义;
  • 有哪些可见寄存器;
  • 操作数放在哪里、怎样寻址;
  • 指令如何编码;
  • 数据类型、字节序、异常等软件可见行为。

同一 ISA 可以由不同内部结构实现。例如单周期、流水线处理器的内部完全不同,只要执行每条 MIPS 指令得到相同的软件可见结果,就都能运行同一份机器码。

这也解释两个常见问题:

  • 不同计算机可以使用相同 ISA,因为它们实现了同一份契约;
  • 同一计算机不能直接执行另一 ISA 的机器码,因为相同比特会被解释成不同动作。

3. 一条指令包含什么

机器指令至少要表达:

  1. 做什么:操作码;
  2. 从哪里取操作数:寄存器编号、立即数或存储器地址信息;
  3. 把结果放哪里:目的寄存器或存储单元;
  4. 下一条指令在哪里:通常顺序执行,分支和跳转会改 PC。

指令类型可分为数据传送、算术逻辑、控制转移、系统控制等。MIPS 的设计原则是格式少而规则、寄存器运算、访存由专门的 load/store 指令完成。

4. 一般寻址方式

有效地址(Effective Address,EA)是真正访问的地址;指令里的地址字段可能只是形式地址。

方式计算直观理解
立即寻址操作数就是指令中的常量数写在指令里
寄存器寻址操作数为 R[r]R[r]编号指向寄存器
基址寻址EA=R[base]+offsetEA=R[base]+offset基地址加位移
相对寻址EA=PC+offsetEA=PC+offset相对当前位置跳转

MIPS 把寻址方式隐含在操作码中,不再单独留一个“寻址方式字段”。例如 lw 天生使用基址加偏移,beq 天生使用 PC 相对寻址。

5. MIPS 的三个核心约束

5.1 固定 32 位指令

所有指令等长,取指和译码边界固定,利于流水线。代价是字段空间有限:寄存器编号、立即数和操作码要在 32 位中权衡。

5.2 32 个 32 位通用寄存器

寄存器编号占 5 位,因为 25=322^5=32。寄存器本身没有“有符号/无符号类型”;同一串比特按什么方式解释,由当前指令决定。

$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:

字段十进制值
opcode0
rs9
rt10
rd8
shamt0
funct32

拼接后为

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 的目标地址为

PCtarget=(PC+4)+(signext⁡(imm16)≪2).PC_{target}=(PC+4)+(\operatorname{signext}(imm16)\ll2).

注意三件事:

  1. 基准是分支指令的下一条,即 PC+4PC+4;
  2. 立即数按补码解释,可向前或向后跳;
  3. 指令按 4 字节对齐,所以位移按“指令字”计数,硬件左移 2 位变成字节位移。

如果目标标号地址为 TT,汇编器填写

imm16=T−(PC+4)4.imm16=\frac{T-(PC+4)}{4}.

8.1 手算示例

0x00400020: beq  $t0, $t1, Target
0x00400024: ...
0x00400028: ...
0x0040002C: Target: ...

因此

imm16=0x0040002C−0x004000244=2.imm16=\frac{0x0040002C-0x00400024}{4}=2.

不要从分支指令自身开始数,也不要把字节差 8 直接填进立即数字段。

9. J 型指令

J 型只有两个字段:

31        26 25                                           0
+-----------+----------------------------------------------+
| opcode(6) | instr_index(26)                              |
+-----------+----------------------------------------------+

目标地址按课程中的 32 位 MIPS 模型拼接:

PCtarget=(PC+4)31:28  ∥  instr_index  ∥  00.PC_{target}=(PC+4)_{31:28}\;\Vert\;instr\_index\;\Vert\;00.

低两位补 0 是因为指令 4 字节对齐;高四位沿用当前区域,所以一次 J 型跳转只能在同一个 256 MB 区域内。跨区域可先构造完整地址,再通过寄存器跳转。

10. 一条汇编语句怎样变成机器码

课件把手工汇编整理为五步:

  1. 判断 R、I、J 类型;
  2. 按格式写出字段;
  3. 查指令表与寄存器表,填各字段十进制值;
  4. 每个字段按固定宽度转二进制,负立即数写补码;
  5. 拼成 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。

评论