指令与汇编.
从一行 C 代码出发,理解指令格式、寄存器、数据寻址与程序跳转。
这一章沿着一个问题展开:x = a + b 到了 CPU 里,究竟会发生什么? 先认识指令,再理解编码和地址,最后读懂一段有条件分支的程序。处理器型号年表略去,发展史只保留能解释设计选择的部分。
指令、汇编与 CPU
指令集规定软件可以要求什么;CPU 用电路实现这些约定。
机器指令用二进制表示加法、读写数据、比较和跳转等操作。汇编语言用助记符和寄存器名称表达这些操作,汇编器再把真实指令编码成机器码。汇编还可以提供伪指令,一条伪指令可能展开成多条机器指令。
这是便于理解的典型主线,实际工具链还包括链接、装载等步骤,编译器也可以直接生成目标代码。
ISA 与 CPU 实现
ISA(指令集架构)规定指令的可见行为,还包括寄存器、数据类型、编码、寻址和异常等规则。流水线、执行单元和缓存如何组织,属于实现层面。同一套 ISA 可以有不同的 CPU 实现。
CPU 内部的小型快速存储
保存正在使用的数据、中间结果,也可以保存地址。$t0、$s1 是寄存器的名称。
通过地址访问的存储空间
存放程序与数据。本课 MIPS 按字节编址,一个 32 位字占 4 个连续字节。
从一个加法开始
add $t0, $t1, $t2 # $t0 ← $t1 + $t2
MIPS 的这种写法是目的在前、来源在后。计算时读取两个来源,结果覆盖目的寄存器。一个 C 变量在某段代码中使用哪个寄存器,由编译器安排。
MIPS 采用 load/store 结构:普通整数加法使用寄存器数据;内存数据通过专门的加载、存储指令读取和写入。这里说的是数据访问,CPU 还需要读取指令本身。
看数据一步步流动
| 名称 | 值 |
|---|
加法结果先保存在寄存器中,执行 sw 后才写回内存。
必要的发展史:CISC 与 RISC
CISC 的一个方向是增加指令功能,让单条指令承担更多工作,并兼顾已有程序的兼容性。对实际程序的观察推动了 RISC 思路:优先处理常用操作,强调规整格式、寄存器计算和 load/store 分工,以便高效实现。
它们是设计取向。现代实现相互吸收技术;实际性能还受 CPU 内部结构、内存访问和程序行为影响。本课采用的经典 MIPS 指令固定为 32 位,不能把这一条件推广到所有 RISC 架构与扩展。
指令格式与数据
一条指令需要说明操作、输入、结果位置,以及怎样继续执行。
操作码说明做什么,地址码描述操作对象。地址码可以指定寄存器、涉及内存地址,也可以直接携带常数。顺序执行时,下一条指令地址通常由 CPU 隐含推算;转移指令则提供改变执行位置的信息。
地址个数:统计显式指定的操作数字段
| 形式 | 示意写法 | 含义 |
|---|---|---|
| 三地址 | add R0, R1, R2 | R0 ← R1 + R2 |
| 二地址 | ADD R1, R2 | R1 ← R1 + R2,其中一个来源兼作目的 |
| 一地址 | ADD A | AC ← AC + 内存[A],累加器 AC 是约定的位置 |
| 零地址 | ADD | 默认取栈顶两个数,再把和压回栈顶 |
上表比较不同机器风格的教学语法;本课 MIPS 的普通寄存器加法采用三地址形式。零地址统计显式字段,参与运算的数据仍可以隐含存在。
显式字段少可以节省编码空间;字段多能更灵活地指定来源和去向。程序的总指令数与执行时间还取决于具体操作和实现。
字节、字与大小端
一个从地址 0x1000 开始存放的字占用 0x1000 至 0x1003。大端把最高有效字节放在低地址,小端把最低有效字节放在低地址;变化的是字节排列。
一个数,四个字节
按相同规则读回整个字,数值保持不变;这里没有反转单个字节内部的位。
MIPS 指令编码
32 位编码空间有限。不同格式,把空间分配给不同的信息。
MIPS 有 32 个通用寄存器,指定一个需要 5 位,因为 2⁵ = 32。机器码存的是编号:$t0 = 8、$t3 = 11、$s1 = 17。
op 6 │ rs 5 │ rt 5 │ rd 5 │ shamt 5 │ funct 6以寄存器运算为例:两个来源、一个目的,以及具体操作。op 6 │ rs 5 │ rt 5 │ immediate 16给立即数、数据地址偏移量或分支偏移量留出空间。op 6 │ target 26把更多编码空间用于构造跳转目标。R 型:先认清角色,再放进字段
add $t0, $t3, $s1
# 汇编顺序:目的 rd、来源 rs、来源 rt
# 字段顺序:op、rs、rt、rd、shamt、funct
本例字段为 0 | 11 | 17 | 8 | 0 | 32。op 和 funct 共同确定运算:add 的 funct 是 32,sub 是 34,两者 op 都是 0。其他 R 型指令会按自己的语义使用这些字段。
I 型:同一个字段,作用随指令变化
| 指令 | rs | rt | 16 位字段 |
|---|---|---|---|
addi | 提供加数 | 接收结果 | 参与加法的常数 |
lw | 提供基址 | 接收读取的数据 | 字节偏移量 |
sw | 提供基址 | 提供要写入的数据 | 字节偏移量 |
beq | 比较来源 | 比较来源 | 以 4 字节为单位的分支偏移量 |
本课的 addi、lw、sw 使用有符号 16 位立即数,范围是 −32768~32767,使用前按符号扩展。I 型字段怎样解释由具体指令规定,例如逻辑立即数指令有不同规则。
组装一条机器指令
数据寻址与数组
寄存器里的数与这个数所指向的内存内容,是两层不同的信息。
假设 R1 = 1000,从内存地址 1000 开始的字为 42,从地址 1004 开始的字为 7。下面用 内存[地址] 表示读取该地址处的一个字。
| 寻址方式 | 怎样取得操作数 | 本例的值 |
|---|---|---|
| 立即数 | 直接使用指令里的常数 1000 | 1000 |
| 寄存器 | 使用 R1 的值 | 1000 |
| 直接 | 内存[1000] | 42 |
| 寄存器间接 | 内存[R1] | 42 |
| 基址加偏移 | 内存[R1 + 4] | 7 |
存储器间接寻址多一层取地址:若内存[800] = 1000、内存[1000] = 42,就先访问 800 取得地址 1000,再到 1000 取出数据 42。这种计数指的是取数据过程中的内存访问,未计取指令与缓存实现细节。
有效地址 EA:先算位置,再按指令读写
addi $t0, $s1, 4 # 计算一个数:$s1 + 4
lw $t0, 4($s1) # 计算地址后,读取该处的一个字
sw $t0, 4($s1) # 计算地址后,把 $t0 写入该处
加载和存储可以使用相同的地址算法,数据流向由操作码决定。lw 读取后保留内存原值,sw 写入后保留源寄存器的值。
数组下标,要换成字节偏移
本课每个字占 4 字节,所以 A[12] 相对数组起点的偏移量为 12 × 4 = 48 字节。变址寻址常用于改变索引、遍历数组;复合寻址可以组合“基址 + 索引 × 比例 + 固定偏移”。哪些项能放进一条指令由 ISA 决定,MIPS 的 lw/sw 直接提供“寄存器基址 + 常数偏移”。
从下标走到内存地址
把一行 C 代码拆开
# $s0 保存 x;$s1 保存数组 A 的基地址
# C:A[100] = x - A[12];
lw $t0, 48($s1) # 取 A[12]
sub $t0, $s0, $t0 # 计算差值
sw $t0, 400($s1) # 写回 A[100]
分支与跳转
数据寻址决定去哪里取数;指令寻址决定程序到哪里继续。
以下统一把 PC 定义为当前分支或跳转指令的地址。本课指令长 4 字节,顺序位置是 PC + 4。例如 0x1000 后面的指令依次位于 0x1004、0x1008、0x100C。
beq:比较寄存器,按相对距离分支
beq $t0, $t1, TARGET # 相等时转移到 TARGET
TARGET 是指令位置的标签,汇编器会计算它对应的偏移字段。beq 属于 I 型,两个寄存器都提供比较数据。
例如当前地址 0x1000、目标 0x1010,字段应为 (0x1010 − 0x1004) ÷ 4 = 3。偏移量为负时能回到前面的指令,这为循环提供了基础。16 位有符号字段对应相对 PC+4 约 ±128 KiB 的范围,精确字节位移为 −131072~131068。
本讲的经典 beq 和 j 在完成转移前,先执行紧随其后的一条指令。初学示例把空操作 nop 放在这里。它影响执行顺序,目标地址计算公式保持不变。具体架构版本与教学模拟器设置需分别确认。
j:拼接出跳转目标
J 型的 26 位目标字段用于构造地址。指令地址按 4 字节对齐,因此最低 2 位为 00,再从 PC+4 取最高 4 位,拼成 32 位地址。
这种方式称为伪直接寻址,可以在 PC+4 所在的 256 MiB 对齐区域内选择目标。这里的运算是拼接;分支公式中的运算则是相加。
亲手算一个跳转目标
| 指令 | 地址计算 | 偏移量解释 |
|---|---|---|
lw / sw | 基址寄存器值 + 偏移量 | 字节,直接相加 |
beq | PC + 4 + 偏移量 × 4 | 以 4 字节为单位 |
j | 高 4 位 + 目标字段 + 00 的位拼接 | 按拼接规则构造地址 |
用一段程序,检查理解
先独立推演,再检查答案。做错的地方,就是最值得回看的那个步骤。
综合例子中,$s1 = 1000(十进制数组基址),$s0 = 20(x 的值),A[2] = 7,A[5] = 99。每个元素占 4 字节,左侧指令地址为十六进制。
0x1000: lw $t0, 8($s1)
0x1004: beq $s0, $t0, END
0x1008: nop
0x100C: sub $t1, $s0, $t0
0x1010: sw $t1, 20($s1)
0x1014: END: nop
也可以回到第一个演示,选择“带条件的数组写入”逐条核对。该演示支持延迟槽,并可修改 x 与 A[2] 的值。
查看综合程序对应的 C 代码
if (x != A[2]) {
A[5] = x - A[2];
}C 在“不相等”时进入代码块;这段汇编在“相等”时跳过代码块。读分支时,要同时看比较条件和目标位置。
来源与适用范围
依据课件《3指令与汇编-1.pdf》整理,实际内容覆盖 3.1 指令系统的发展、3.2 指令格式、3.3 寻址方式。目录中的 3.4、3.5 尚未在该文件展开。讲解采用经典 32 位定长 MIPS;具体架构扩展和模拟器设置可能有不同约定。
对课件中的简写作了补全:分支以当前指令地址加 4 为基准;J 型高 4 位来自 PC+4;立即数字段的解释由具体指令决定。RISC 的规整格式是设计取向,不代表所有 RISC 扩展都采用同一种指令长度。
MIPS32 指令手册 · BEQ(印刷页 55) ↗MIPS32 指令手册 · J(印刷页 115) ↗