Appearance
指令格式与寻址方式
概念
指令(instruction)是 CPU 能直接识别并执行的最小命令单位;一条指令由操作码(做什么)和地址码(对谁做)两部分拼成。
为什么要把"地址码"和"操作码"分开设计:CPU 只有一套硬件,却要执行几百条不同的指令。硬件在取指之后第一件事就是拆开这两部分——操作码送控制器译码(决定发哪些控制信号),地址码送地址形成部件(决定操作数在哪)。这个"拆"的动作叫指令译码。
本章要解决两个问题:一条指令能带几个地址(指令格式),以及地址码怎么变成真正要访问的位置(寻址方式)。两者共同决定了指令的长度、访存次数与程序的灵活性。
原理
一、指令格式:按地址码个数分类
| 类型 | 形式 | 访存次数(取操作数) | 典型例子 | 说明 |
|---|---|---|---|---|
| 零地址 | OP | 0 | NOP、RET、PUSH(隐含栈顶) | 操作数隐含(栈顶 / 累加器),或根本不需要操作数 |
| 一地址 | OP A | 1 | INC R、MUL X | 单操作数指令;若是双操作数,第二个操作数隐含在累加器 ACC |
| 二地址 | OP A1, A2 | 2 | ADD R1, R2 | 最常见;A1 通常既是源又是目的,结果写回 A1 |
| 三地址 | OP A1, A2, A3 | 3 | ADD R1, R2, R3 | A1 是结果存放处,不破坏源操作数 |
| 四地址 | OP A1,A2,A3,A4 | 3 | 教学目标 | 多出的 A4 是下条指令地址,因 PC 出现而被淘汰 |
规律:地址码越多,一条指令能表达的操作越完整,但指令越长、访存越多。现代机器普遍用「二地址 + 寄存器」的组合来平衡。
指令字长与机器字长不是一回事:
| 概念 | 含义 |
|---|---|
| 机器字长 | CPU 一次能处理的二进制位数(通用寄存器宽度、ALU 宽度) |
| 指令字长 | 一条指令占的二进制位数 |
| 存储字长 | 一个存储单元(编址单位)的位数,也等于 MDR 的位数 |
三者关系:指令字长 = 存储字长的整数倍(半字长、单字长、双字长指令),这样一条指令可以整齐地放在若干个存储单元里,取指时不用访问半个单元。
二、操作码设计:定长与扩展
定长操作码:操作码字段位数固定,
- 译码最简单(一条译码电路)、译码最快。
- 缺点:指令数少时浪费编码空间,指令数多时字段太长。
扩展操作码(variable-length opcode):让不同地址码个数的指令共享同一段比特——地址码少的指令(本来地址字段就短)可以占用更长的操作码。
三地址: OP(4) A1(4) A2(4) A3(4) <-- OP 只用 4 位
二地址: OP(8)=1111+xxxx A1(4) A2(4) <-- 借 1 个三地址编码当"扩展前缀"
一地址: OP(12)=1111 1111+xxxx A1(4) <-- 再借 1 个二地址编码
零地址: OP(16)=1111 1111 1111 xxxx <-- 16 种编码全给零地址核心思想:用「短指令让出编码空间」的方式,在有限的指令字长里塞下更多指令种类。这与哈夫曼编码的思路一致——让最常出现的指令用最短的编码。
必须记住的两句话:
- 扩展操作码的"扩展位"是用上一级"少编一条指令"换来的。 三地址想留 1 个编码当扩展前缀,就只能编 15 条三地址指令。
- 解码规则是"看前缀":从高位起,连续 1 的个数决定格式,直到出现第 1 个 0 或字段边界。
三、寻址方式:地址码怎么变成有效地址
有效地址 EA(effective address)指操作数的真实主存地址。寻址方式就是"由指令里的地址码字段 A 算出 EA 的规则"。
十种寻址方式(408 考纲范围)
| # | 名称 | 英文 | 有效地址 EA | 取操作数访存次数 | 用途 / 特点 |
|---|---|---|---|---|---|
| 1 | 隐含寻址 | implied | 无(由操作码隐含) | 0 | 操作数在 ACC 或栈顶 |
| 2 | 立即寻址 | immediate | 无 EA,A 就是操作数本身 | 0 | 常量、立即数 |
| 3 | 寄存器寻址 | register | 无 EA,A 是寄存器号 | 0 | 最快,不访存 |
| 4 | 直接寻址 | direct | 1 | 简单;A 的位数限制了寻址范围 | |
| 5 | 间接寻址 | indirect | 2(一次取地址,一次取数) | 可扩大寻址范围;多次间接可再扩 | |
| 6 | 寄存器间接 | register indirect | 1 | 结合寄存器的快与间接的灵活 | |
| 7 | 基址寻址 | base | 1 | 基址寄存器内容由 OS 决定,用于程序重定位/浮动 | |
| 8 | 变址寻址 | indexed | 1 | A 是数组首址(不变),IX 是下标(变) | |
| 9 | 相对寻址 | relative | 1 | 用于转移指令,A 是"相对偏移量" | |
| 10 | 堆栈寻址 | stack | 隐含 SP,栈顶 | 1 | 硬堆栈(寄存器组)或软堆栈(主存) |
三种"偏移寻址"的区别(必考对比)
| 对比项 | 基址寻址 | 变址寻址 | 相对寻址 |
|---|---|---|---|
| 公式 | |||
| 谁在变 | A 变,基址寄存器不变 | 变址寄存器 IX 变,A 不变 | A 变,PC 变 |
| 谁的值是"基准" | 程序(段)的起始地址 | 数组的首地址 | 当前指令地址 |
| 主要用途 | 程序浮动 / 重定位 / 扩大寻址范围 | 数组、循环、字符串处理 | 程序转移(分支) |
| 由谁设定 | 操作系统(用户不可见) | 用户程序(循环里自增) | 编译器计算 |
| 典型指令 | LDR R1, [BR, #20] | LDR R1, [IX, #4] | BEQ offset |
记忆口诀:「基址管浮动、变址管数组、相对管跳转」。
相对寻址的一个细节:执行时 PC 已经指向下一条指令,所以
这个"+指令长度"是最高频的送分/送命点。若指令长 2 字节、当前地址 2000、A = 100,则
前索引与后索引:变址寄存器的自增/自减时机。
| 名称 | 规则 | 说明 |
|---|---|---|
| 前索引(pre-index) | 先 | 等价于 *++p |
| 后索引(post-index) | 先取数,再 | 等价于 *p++ |
堆栈寻址:入栈/出栈时 SP 自动增减,所以不需要在指令里写地址码——这是"零地址指令"的重要来源。硬堆栈用寄存器组(快、贵、容量小),软堆栈在主存里(慢、便宜、容量大)。
四、寻址方式对"寻址范围"的影响
| 情形 | 寻址范围 |
|---|---|
| 直接寻址,地址码 | |
| 间接寻址,地址码 | |
| 寄存器间接 | |
| 变址/基址 |
这就是"间接寻址能扩大寻址范围"的确切含义:它把"地址码的位数"换成了"存储单元的位数"。
五、数据的存放顺序(与本篇相邻的小考点)
多字节数据在主存里有个"先放哪个字节"的约定:
| 方式 | 规则 | 0x12345678 存于地址 0x1000~0x1003 |
|---|---|---|
| 大端(big-endian) | 高位字节放低地址 | 12 34 56 78 |
| 小端(little-endian) | 低位字节放低地址 | 78 56 34 12 |
关键结论:字/半字访问(lw、sw)与字节序无关(整块搬,硬件自动还原),只有字节访问(lb、sb)才暴露字节序。这与 MIPS32 的具体落地放在《语言 · 汇编与 C》第 2 篇里讲。
示例
例 1:扩展操作码的编码空间(经典必考)
指令字长 16 位,采用扩展操作码,每个地址码占 4 位。 (1)若三地址指令 15 条,则二地址、一地址、零地址各最多多少条? (2)若三地址指令改为 12 条,二地址最多多少条?
完整计算过程:
(1) 先看操作码字段的"所有权":
第一步,三地址指令的格式是 OP(4) A1(4) A2(4) A3(4),操作码只有 4 位 → 编码空间 16 种。
现在要编 15 条,必须留 1 种编码(1111)当作"更短格式"的扩展前缀:
第二步,二地址指令的操作码变成 8 位(1111 + 4 位),编码空间:
同样留 1 种(1111 1111)继续扩展:
第三步,一地址指令的操作码 12 位,编码空间
第四步,零地址指令的操作码 16 位(1111 1111 1111 + 4 位),不再需要扩展,16 种全给零地址:
结论(1):15 / 15 / 15 / 16。
词空间校验(务必会算,是自检手段):每条指令占用的"16 位字的组合数":
恰好铺满
(2) 三地址编 12 条,则剩下 4 种 4 位编码(1100~1111)都可以作前缀:
若还要再给一地址留 1 种扩展口,则为
规律总结:若上一级指令减少
例 2:定长操作码下的上限
某机指令字长 16 位,采用定长操作码,有二地址和三地址两种指令,每个地址码 4 位。问操作码最长可设几位、最多容纳多少条指令。
完整计算过程:
第一步,三地址指令占用的位数最大:
第二步,操作码只能取"两种格式都能装下"的位数,即 4 位:
结论:16 条。对比例 1 的扩展方案(15 + 15 + 15 + 16 = 61 条),可见扩展操作码用"译码复杂"换来了四倍的指令空间。
例 3:各种寻址方式的有效地址计算
已知:指令里地址码字段
(十进制表示,实际以 4 位计);变址寄存器 ;基址寄存器 ;PC 指向下一条指令、地址为 2000;主存单元 100 里存着 500。
完整计算过程:
| 寻址方式 | 计算式 | EA | 访问主存取出操作数需要的访存次数 |
|---|---|---|---|
| 立即寻址 | 无 EA,操作数 = 100 | — | 0 |
| 寄存器寻址 | 无 EA,操作数在寄存器 | — | 0 |
| 直接寻址 | 1 | ||
| 间接寻址 | 2 | ||
| 寄存器间接 | $EA = (R) $ | 由 | 1 |
| 变址寻址 | 1 | ||
| 基址寻址 | 1 | ||
| 相对寻址 | 1 |
注意相对寻址的坑:上表若把 PC 当作"当前指令的地址",且指令长 2 字节,则
间接寻址为什么会 2 次访存:
第 1 次访存:读地址 100,得到内容 500 —— 这才是"操作数的地址"
第 2 次访存:读地址 500,得到操作数本身例 4:访存次数的统计
一段程序有 5 条指令:2 条寄存器寻址的加法、1 条直接寻址的取数、1 条间接寻址的取数、1 条立即寻址的赋值。每条指令都要取指(1 次访存)。求总访存次数。
完整计算过程:
| 指令 | 取指 | 取操作数 | 小计 |
|---|---|---|---|
| 加法 1(寄存器) | 1 | 0 | 1 |
| 加法 2(寄存器) | 1 | 0 | 1 |
| 取数(直接) | 1 | 1 | 2 |
| 取数(间接) | 1 | 2 | 3 |
| 赋值(立即) | 1 | 0 | 1 |
| 合计 | 5 | 3 | 8 |
结论:总访存 8 次。
这里藏着一个高频陷阱:题目若说"设取指阶段不需要访存(指令已在指令队列 / Cache 中)",或"采用指令预取",那么取指那 5 次要扣掉,答案是 3 次。做题第一步永远是确认"取指算不算访存"。
例 5:C 代码——扩展操作码解码器
本机不提供代码运行能力,runnable 标记仅为将来接入运行件预留;请自行在本地编译验证。
#include <stdio.h>
/* 16 位指令字 / 每地址码 4 位 / 扩展操作码(第 12 篇例 1 的方案):
* 三地址: bits[15:12] != 1111 -> OP = 高 4 位
* 二地址: bits[15:12] == 1111, bits[11:8] != 1111 -> OP = 高 8 位
* 一地址: bits[15:8] == 1111_1111, bits[7:4] != 1111 -> OP = 高 12 位
* 零地址: bits[15:4] == 1111_1111_1111 -> OP = 全部 16 位
* 判定必须"按 4 位一段"逐段查, 不能只数连续 1 的个数:
* 0xF834 的 bits[11:8] = 8, 应判二地址; 若数连续 1 得 k=5, 会误判成一地址。
*/
static void decode(unsigned w) {
const char *fmt;
unsigned op;
int opbits;
if (((w >> 12) & 0xFu) != 0xFu) { fmt = "三地址"; op = w >> 12; opbits = 4; }
else if (((w >> 8) & 0xFu) != 0xFu) { fmt = "二地址"; op = w >> 8; opbits = 8; }
else if (((w >> 4) & 0xFu) != 0xFu) { fmt = "一地址"; op = w >> 4; opbits = 12; }
else { fmt = "零地址"; op = w; opbits = 16; }
printf(" 0x%04X -> %s, 操作码 = 0x%X (高 %2d 位)\n", w, fmt, op, opbits);
}
int main(void) {
unsigned cases[] = {0x1234, 0x0ABC, 0xF234, 0xF834, 0xFF34, 0xFFF4, 0xFFFF};
int n = (int)(sizeof cases / sizeof cases[0]);
printf("扩展操作码解码:\n");
for (int i = 0; i < n; i++) decode(cases[i]);
/* 编码空间核算: 15 条三地址 + 15 条二地址 + 15 条一地址 + 16 条零地址 */
long words = 15L * (1 << 12) + 15L * (1 << 8) + 15L * (1 << 4) + 16L;
printf("\n词空间核算: 15*4096 + 15*256 + 15*16 + 16*1 = %ld\n", words);
printf("2^16 = %d -> %s\n", 1 << 16, words == (1 << 16) ? "恰好铺满, 编码紧致" : "不匹配");
return 0;
}
c 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
预期输出:
扩展操作码解码:
0x1234 -> 三地址, 操作码 = 0x1 (高 4 位)
0x0ABC -> 三地址, 操作码 = 0x0 (高 4 位)
0xF234 -> 二地址, 操作码 = 0xF2 (高 8 位)
0xF834 -> 二地址, 操作码 = 0xF8 (高 8 位)
0xFF34 -> 一地址, 操作码 = 0xFF3 (高 12 位)
0xFFF4 -> 零地址, 操作码 = 0xFFF4 (高 16 位)
0xFFFF -> 零地址, 操作码 = 0xFFFF (高 16 位)
词空间核算: 15*4096 + 15*256 + 15*16 + 16*1 = 65536
2^16 = 65536 -> 恰好铺满, 编码紧致两个要注意的地方:
0xFFFF归零地址——它是"零地址 16 条"里的最后一条(0xFFF0~0xFFFF),编码空间刚好用完。0xF834必须判成二地址——bits[11:8] = 8 ≠ 1111。判决规则是"按 4 位一段逐段查",不是"数连续 1 的个数":数连续 1 会得到 ,落进"一地址"区间而误判。这是写解码器时最常踩的坑。
例 6:Python 对照——寻址方式与访存次数核算
def ea_of(mode, A, IX=1000, BR=0x10040000, PC_next=2000, mem=None):
mem = mem or {100: 500}
if mode == '立即':
return None, 0, A # 操作数就是 A 本身
if mode == '寄存器':
return None, 0, 'R%d' % A
if mode == '直接':
return A, 1, None
if mode == '间接':
return mem[A], 2, None
if mode == '变址':
return IX + A, 1, None
if mode == '基址':
return BR + A, 1, None
if mode == '相对':
return PC_next + A, 1, None
raise ValueError(mode)
def show(ea):
if ea is None:
return '—'
return '0x%X' % ea if ea > 0xFFFF else str(ea)
print('%-6s %-12s %-6s %s' % ('寻址', 'EA', '访存', '说明'))
for m in ['立即', '寄存器', '直接', '间接', '变址', '基址', '相对']:
ea, n, extra = ea_of(m, 100)
note = '' if extra is None else ('操作数 = %s' % extra)
print('%-6s %-12s %-6d %s' % (m, show(ea), n, note))
print('\n相对寻址的两种口径:')
print(' PC 指向下一条指令 -> EA = 2000 + 100 = %d' % (2000 + 100))
print(' 当前指令地址 2000、指令长 2 -> EA = 2000 + 2 + 100 = %d' % (2000 + 2 + 100))
print('\n5 条指令访存统计:')
prog = [('加法1', '寄存器'), ('加法2', '寄存器'), ('取数1', '直接'),
('取数2', '间接'), ('赋值', '立即')]
tot_f = tot_o = 0
for name, m in prog:
_, n, _ = ea_of(m, 100)
tot_f += 1; tot_o += n
print(' %-6s 取指 1 + 取数 %d = %d' % (name, n, 1 + n))
print(' 合计: 取指 %d + 取数 %d = %d 次' % (tot_f, tot_o, tot_f + tot_o))
python 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
输出对照:寻址方式表 EA 与访存次数与例 3 完全一致(间接寻址 EA = 500、2 次访存);相对寻址两种口径给出 2100 / 2102;5 条指令总访存 8 次(取指 5 + 取数 3),与例 4 一致。
考点
考点
1. 必背结论
- 指令 = 操作码 + 地址码;地址码个数决定访存次数(零 0、一 1、二 2、三 3)
- 定长操作码:
位 → 条指令 - 扩展操作码:上一级少编 1 条,下一级多
个编码;解码靠"看前缀"地 址 码 位 数 - 三种偏移寻址:
/ / ,基址管浮动、变址管数组、相对管跳转 - 相对寻址的 PC 已经指向下一条指令:
下 一 条 指 令 地 址 - 间接寻址访存 2 次,且能把寻址范围从
扩到存 储 字 长 - 指令字长
存储字长的整数倍(半字、单字、双字)
2. 高频陷阱
- 相对寻址忘了加指令长度。这是本考点最经典的错误,且一年考多次。看到相对寻址先问:"PC 指向的是本条还是下一条?"
- 一地址指令的双操作数问题:若是一地址的"双操作数"指令,另一个操作数隐含在累加器 ACC——这是"一地址为什么能算加法"的答案。
- 二地址指令的 A1 被覆盖:结果写回 A1,源操作数 A1 的旧值丢失。若不能破坏源,就要用三地址指令。
- "取指算不算访存":题目不给前提时按"算"处理,但要在答案里写明假设;给了"指令预取/指令在 Cache"就扣掉。
- "寻址范围"与"寻址方式"要分开看:直接寻址的范围由地址码位数决定,变址/基址由寄存器宽度决定,间接由存储字长决定。
- 基址 vs 变址的"谁不变"要答对:基址是"基址寄存器不变、形式地址 A 变";变址是"形式地址 A 不变、变址寄存器变"。答反了整题丢分。
- 立即寻址没有 EA。写成
是错的——A 那不是地址,是数据本身。 - 扩展操作码的"最多条数"要不要减 1:要看题目是否要求"给更短格式留扩展位"。留就减 1,不留就不减。例 1(2) 两种答案(64 / 63)都写了,答题时把假设说清楚。
- 大端小端只在字节访问时暴露。
lw/sw与字节序无关,lb/sb有关。
3. 解题模板("扩展操作码求最多条数")
① 数清地址码个数与每字段位数 -> 得到每级操作码字段的位数
② 第一级: 2^OP位, 若需扩展则 -1
③ 第二级: (上一级让出的编码数) x 2^(该级新增的 OP 位), 若需扩展再 -1
④ 逐级下推; 最后一级不再需要扩展, 全部编码可用
⑤ 自检: 各级消耗的词空间之和 == 2^指令字长4. 与相邻章节的接口
- 第 22 篇会用 MIPS32 的真实编码走一遍"机器码 ↔ 汇编"的双向翻译,是本章的落地实例。
- 第 21 篇讲 CISC/RISC:RISC 的特点是定长指令、寻址方式少、Load-Store 结构,正好对应本章"指令格式与寻址方式"的两个极端选择。
- **第 30 篇(数据通路)**要执行本章的指令:地址形成部件对应"寻址方式",取指周期对应"读取指令字"。
- 《语言 · 汇编与 C》第 2 篇 /lang/02-addressing.md 给出 MIPS32 五种寻址方式的具体形式(基址+偏移、寄存器间接、直接、PC 相对、伪直接),可与本章表格对照。
小结
- 一条指令 = 操作码 + 地址码;地址码越少,指令越短但能表达的越少。零/一/二/三/四地址的分界,本质是访存次数的分界。
- 扩展操作码用"上一级让出的编码"换取"下一级更大的编码空间",解码靠看前缀连续 1 的个数。词空间之和必须等于
,这是最好的自检。指 令 字 长 - 十种寻址方式里,只有立即和寄存器是 0 次访存,间接是 2 次,其余取操作数都是 1 次。
- 基址管浮动、变址管数组、相对管跳转 —— 三者的公式都是"寄存器 + A",区别在谁不变、谁设定、干什么用。
- 相对寻址的 PC 已经加了指令长度;这是全章最容易丢分的一个细节。
- 指令字长是存储字长的整数倍;这一点决定了取指令要用几个访存周期,第 30 篇会接着算。
下一篇:CISC 与 RISC
评论(0)
当前浏览器不允许本地存储,评论无法保存。
还没有评论,来说两句。