4.1 指令格式
指令
计算机执行某种操作的命令,是软件与硬件的接口。指令格式 = 操作码 OP + 地址码 A。指令字长 = 操作码长度 + 地址码长度。按地址码字段个数,指令可分为:
| 类型 | 格式 | 含义 | 访存次数 |
|---|---|---|---|
| 零地址指令 | OP | 无操作数(如 NOP、停机)或隐含寻址(如栈操作) | 0 |
| 一地址指令 | OP A | ACC ← OP(A),需另一个操作数隐含在 ACC | 2(读A、写ACC) |
| 二地址指令 | OP A1 A2 | A1 ← (A1) OP (A2) | 3~4 |
| 三地址指令 | OP A1 A2 A3 | A1 ← (A2) OP (A3) | 4 |
| 四地址指令 | OP A1 A2 A3 A4 | A1 ← (A2) OP (A3),A4 为下条指令地址 | 4 |
操作码扩展:固定长度操作码简单但浪费空间;可变长度操作码通过扩展技术,在地址数少的指令中用较长操作码,提高编码效率。
4.2 寻址方式
寻址方式
确定本条指令的数据地址及下一条待执行指令地址的方法。寻址方式的目标是在有限指令字长内灵活表示更大的地址空间。有效地址 EA:操作数的真实地址(按字节或字编址)。形式地址 A 是指令中给出的地址字段。
各寻址方式对比
| 寻址方式 | 有效地址 EA | 访存次数 | 特点 |
|---|---|---|---|
| 立即寻址 | —(操作数在指令中) | 0 | 快,但操作数长度受限 |
| 直接寻址 | EA = A | 1 | 简单,地址范围受 A 长度限制 |
| 间接寻址 | EA = (A) | 2(或多重) | 扩大寻址范围,但慢 |
| 寄存器寻址 | EA = Ri(操作数在 Ri) | 0 | 快,无需访存 |
| 寄存器间接寻址 | EA = (Ri) | 1 | 比间接快,常用于循环 |
| 基址寻址 | EA = (BR) + A | 1 | BR 由 OS 管理,利于多道程序重定位 |
| 变址寻址 | EA = (IX) + A | 1 | IX 由用户管理,适合数组/循环 |
| 相对寻址 | EA = (PC) + A | 1 | A 为偏移量,用于转移指令,利于浮动 |
基址 vs 变址:基址寄存器内容通常不变(由操作系统控制,面向多道程序),形式地址 A 为位移量;变址寄存器内容可变(由用户控制,面向循环/数组),形式地址 A 给出基准。两者公式相同但用途不同。
4.2.1 数据对齐与大/小端存储 [22新增]
按边界对齐
n 字节数据的存放起始地址必须能被 n 整除。例如:4 字节 int 必须存放在地址为 4 的倍数的位置。不对齐的数据访问需要多次访存,降低性能。大端模式(Big Endian):高位字节存放在低地址,低位字节存放在高地址。符合人类阅读习惯(从左到右),网络字节序通常采用大端。
小端模式(Little Endian):低位字节存放在低地址,高位字节存放在高地址。x86、ARM(默认)等主流 CPU 使用小端模式。
示例:0x12345678 的存储
| 地址 | 大端模式 | 小端模式 |
|---|---|---|
| 低地址(如 0x100) | 12(最高字节) | 78(最低字节) |
| 0x101 | 34 | 56 |
| 0x102 | 56 | 34 |
| 高地址(0x103) | 78(最低字节) | 12(最高字节) |
记忆口诀:大端 — "高对低"(高位字节在低地址);小端 — "低对低"(低位字节在低地址)。
4.2.2 编译器、汇编器与链接器
程序转换流程
源程序(.c) → 编译器 → 汇编代码(.s) → 汇编器 → 目标文件(.o) → 链接器 → 可执行文件(.exe/ELF)。- 编译器(Compiler):将高级语言(C/C++/Java)翻译为汇编代码。负责词法分析、语法分析、语义分析、中间代码生成、代码优化、目标代码生成。
- 汇编器(Assembler):将汇编代码翻译为机器码,生成目标文件(.o 或 .obj)。目标文件中指令地址是相对地址,尚未最终确定。
- 链接器(Linker):将多个目标文件和库文件合并为一个可执行文件。完成符号解析(将外部符号引用与定义关联)和重定位(修正地址为最终地址)。链接分为静态链接和动态链接。
符号解析与重定位:符号解析解决"函数/变量在哪里"的问题;重定位将目标文件中的相对地址修改为可执行文件中的绝对地址。链接器需要修改指令中的地址字段,使跳转、访存指令指向正确的目标。
4.2.3 高级语言与机器级代码对应
高级语言程序最终被翻译为机器指令序列执行,下面以 C 语言为例说明常见结构的对应关系。
选择结构(if-else)→ 条件分支
C 语言中的 if-else 语句编译为比较指令(CMP) + 条件跳转指令(Bcond):
算法: if-else 的机器指令映射
输入: 变量 a, b, max 均为寄存器
输出: max = max(a, b)
C 代码: 对应机器指令:
if (a > b) CMP Ra, Rb // 比较 a 和 b
max = a; BLE ELSE // 若 a≤b 跳转到 ELSE
else MOV Rmax, Ra // max = a (then 分支)
max = b; JMP ENDIF // 跳过 else 分支
ELSE: MOV Rmax, Rb // max = b
ENDIF: ... // 后续代码
循环结构(for/while)→ 循环分支
算法: for 循环的机器指令映射
输入: 初始 sum=0, 循环变量 i
输出: 计算 sum = 0+1+2+...+N-1
C 代码: 对应机器指令:
sum = 0; MOV Rsum, #0 // sum = 0
i = 0; MOV Ri, #0 // i = 0
for (i=0; i<N; i++) LOOP: CMP Ri, #N // 比较 i 和 N
sum += i; BGE EXIT // 若 i≥N 退出循环
ADD Rsum, Rsum, Ri // sum += i
ADD Ri, Ri, #1 // i++
JMP LOOP // 跳回循环头
EXIT: ... // 循环结束
函数调用 → 栈帧与 call/ret
栈帧(Stack Frame)
每次函数调用在栈上分配的一块内存区域,用于保存返回地址、调用者保存的寄存器、局部变量等。由调用者(Caller)和被调用者(Callee)共同管理。函数调用的关键步骤:
- 参数传递:前几个参数通过寄存器传递(超出部分压栈)。
- call 指令:将返回地址(下一条指令地址)压栈,然后跳转到被调用函数的入口地址。
- 栈帧建立:被调用函数压入旧帧指针、分配局部变量空间。
- ret 指令:弹出返回地址并跳转回去,恢复调用者的执行流。
调用约定:x86 常见约定包括 cdecl(调用者清理栈)、stdcall(被调用者清理栈)。参数压栈顺序通常为从右向左(cdecl)。408 中需理解 call/ret 对栈指针 SP 和程序计数器 PC 的影响。
4.3 CISC 与 RISC
| 对比项 | CISC(复杂指令集) | RISC(精简指令集) |
|---|---|---|
| 指令系统 | 复杂、庞大(100+ 条) | 简单、精简(<100 条) |
| 指令长度 | 可变 | 固定 |
| 寻址方式 | 多(>4 种) | 少(≤4 种) |
| 执行时间 | 差异大 | 大多一个周期完成 |
| 通用寄存器 | 较少 | 较多(>32 个) |
| 控制方式 | 微程序控制为主 | 硬布线控制为主 |
| 流水线 | 难实现 | 易实现,必须实现 |
| 代表 | x86 | MIPS、ARM、RISC-V |
RISC 核心思想
选取使用频率最高的简单指令,固定指令长度、Load/Store 结构(只有访存指令访问内存)、硬布线控制、大量寄存器、指令流水线,以提高主频和 CPI。