格式和寻址方式

🔥 高优先级
真题练习
组成原理说实话就 两大块:cache 和 虚拟存储器,这两个要放在一起。指令系统 和 CPU,这两个也要放一起。两大块内部的知识点都是相互耦合的,需要综合理解。

指令格式

指令的功能就是 对某些数据 进行 某种操作

所以指令中主要包含两个部分:操作码(opcode)以及 地址(address)。

  • 操作码(opcode)就是决定了指令的类型:
    • 这个指令是干嘛的?进行哪种操作?
  • 地址是一个通用含义,指的是操作的对象:
    • 可以是一个 内存地址<addr>
    • 也可以是 CPU 中的一个寄存器编号<reg>
    • 也可以是一个 立即数<imm>
instruction_formatcluster_address_types地址的三种形式instruction指令 (Instruction)opcode操作码 (Opcode)instruction->opcode包含address地址 (Address)instruction->address包含opcode_func决定指令类型• 这个指令是干嘛的?• 进行哪种操作?opcode->opcode_funcmem_addr内存地址<addr>address->mem_addrregister寄存器编号<reg>address->registerimmediate立即数<imm>address->immediatenote操作对象的通用含义address->note

指令类型

根据操作码分类

指令根据其 操作码(opcode)的不同可以分为以下类别:

  1. 数据传输指令
    • MOV:将数据从一个位置传输到另一个位置,可以是寄存器到寄存器、内存到寄存器、寄存器到内存等。
    • PUSH:将数据(通常是寄存器中的值)推入堆栈。
    • POP:从堆栈中弹出数据并存储到寄存器中。
  2. 算术和逻辑运算指令
    • ADDSUBMULDIV:执行算术运算,如加法、减法、乘法和除法。
    • ANDORXORNOT:执行逻辑运算,如按位与、按位或、按位异或和按位取反。
    • INCDEC:递增和递减操作数的值。
    • CMP:用于比较两个值,并根据结果设置标志寄存器的状态。
  3. 控制转移指令
    • JMP:用于无条件跳转到指定的目标地址。
    • Jxx:条件跳转指令,根据特定的条件(如零标志、进位标志等)来决定是否跳转。
    • CALL:调用子程序或函数。
    • RET:从子程序返回。
  4. 输入/输出指令
    • IN:从外部设备或端口读取数据。
    • OUT:向外部设备或端口发送数据。
  5. 字符串操作指令(String Instructions):
    • MOVSLODSSTOSCMPS:用于在内存中执行字符串操作,如移动、加载、存储、比较。
  6. 陷阱指令(Trap Instructions):
    • INT:用于引发中断,通常用于与操作系统进行通信。
  7. 协处理器指令(Coprocessor Instructions):
    • CLISTI:用于清除和设置 CPU 的中断标志,通常只能在内核模式下执行。
InstructionSetcluster_basic基础指令cluster_control程序控制cluster_advanced高级功能titleCPU指令集分类data_transfer数据传输指令Data Transfertitle->data_transferarithmetic_logic算术和逻辑运算指令Arithmetic & Logictitle->arithmetic_logiccontrol_transfer控制转移指令Control Transfertitle->control_transferio_instructions输入/输出指令I/O Instructionstitle->io_instructionsstring_ops字符串操作指令String Operationstitle->string_opstrap_instr陷阱指令Trap Instructionstitle->trap_instrcoprocessor协处理器指令Coprocessor Instructionstitle->coprocessormovMOV数据移动data_transfer->movpushPUSH入栈data_transfer->pushpopPOP出栈data_transfer->poparithmeticADD, SUBMUL, DIV算术运算arithmetic_logic->arithmeticlogicalAND, ORXOR, NOT逻辑运算arithmetic_logic->logicalinc_decINC, DEC递增递减arithmetic_logic->inc_deccmpCMP比较arithmetic_logic->cmpjmpJMP无条件跳转control_transfer->jmpconditional_jmpJxx条件跳转control_transfer->conditional_jmpcallCALL调用子程序control_transfer->callretRET返回control_transfer->retin_instrIN输入数据io_instructions->in_instrout_instrOUT输出数据io_instructions->out_instrstring_detailedMOVS, LODSSTOS, CMPS字符串处理string_ops->string_detailedint_instrINT中断trap_instr->int_instrinterrupt_flagsCLI, STI中断标志控制coprocessor->interrupt_flagsmov->push堆栈操作push->pop配对使用cmp->conditional_jmp设置标志call->ret配对使用

根据地址个数分类

根据指令中的 地址 个数,可以将指令划分为以下类型。
这些地址可以是 寄存器内存地址,也可以是 立即数

OPOPA1OPA1A2OPA3A1A2零地址一地址二地址三地址
指令格式指令格式含义
零地址指令op执行操作 ,操作数隐含在栈中
一地址指令op, A1 :对 操作并将结果存回
二地址指令op, A1, A2 :将 运算,结果存入
三地址指令op, A3, A1, A2 :对 运算,结果存入

定长和变长指令

变长指令集
定长指令集

指令长度的设计可以分为两类:

  • 定长指令集:所有指令的长度完全相同
    → 典型架构:ARM(RISC
    → 优点:解码简单、高效
    → 缺点:指令中可能出现浪费空间的无效字段

  • 变长指令集:不同指令具有不同长度
    → 典型架构:x86(CISC
    → 优点:编码更紧凑,能支持更复杂操作
    → 缺点:解码过程复杂,需要准确识别指令边界

注意

这两种设计思路分别带来了两个常见问题,在试题中也经常以变形方式考查:

  1. 定长指令集 中,所有指令长度固定,而不同指令所需的 地址字段 个数不一样,如何统一表示?
  2. 变长指令集 中,指令长度不固定,CPU 又如何判断指令边界(即每条指令的起始和结束位置)?

🧩 解决思路

问题 1:定长指令中地址字段数量不统一怎么办?

解决方法是使用 无效字段填充

即:当某条指令所需的 地址字段 不足以填满整个固定长度时,剩余部分使用 填充值(无效字段)占位。这些填充值不会影响指令执行,仅用于保证每条指令长度一致,简化硬件解码逻辑。

指令长度设计的两大挑战及解决方案问题 1:地址字段数量不统一定长指令 · 不同指令需要的地址字段数不同三地址指令操作码地址1地址2地址3← 刚好填满二地址指令操作码地址1地址2无效字段← 需要填充!✓ 解决方案:无效字段填充指令长度保持一致(如32位)地址字段不足时用填充值占位填充值不影响执行,仅用于对齐简化硬件解码逻辑💡 定长指令的优势• 解码简单:固定位置提取字段• 流水线高效:指令长度可预测• 取指快速:按固定步长即可问题 2:如何识别指令边界变长指令 · 不同指令长度不同指令流示例前缀8位指令16位前缀16位指令24位前缀32位指令40位✓ 解决方案:指令前缀标识在指令开头设置前缀字段前缀标识该指令的长度/类型CPU先读前缀,再提取完整指令增加解码复杂度,换取编码灵活性💡 变长指令的优势• 编码紧凑:指令密度更高• 功能丰富:可支持复杂操作

问题 2:变长指令中如何识别指令边界?

一种常用方案是引入 指令前缀

即:在每条指令的开头,设置一个前缀字段,或在 操作码 的高位嵌入标志,用来标识该指令的长度或类型。CPU 在解码时先读取前缀位,就能判断指令长度,从而准确提取整条指令。

虽然这种方式增加了解码复杂度,但换来了更大的编码灵活性与指令集的扩展能力。

操作码扩展编码

上面的指令前缀是为了让 CPU 判断整条指令的长度;而在 定长指令系统 中,还有一种与之思想类似、但用途不同的设计——操作码扩展编码

其核心思想是:让操作码本身具有层次结构。当 CPU 解码时,如果发现当前操作码属于"扩展前缀",就继续读取后续位,直到得到完整的操作码。

因此,指令系统通常采用:

可变长度操作码 + 定长指令字 的方式, 并要求这些 操作码 遵循 前缀编码 设计原则。


🌟 先举个简单的例子:

假设我们设计如下 操作码长度规则

  • 三地址指令使用 4 位 操作码
  • 二地址指令使用 6 位 操作码
  • 一地址指令使用 8 位 操作码

为了防止解析冲突,必须满足 前缀码 的要求:

  • 任意一个 4 位 操作码不能是任何 6 位8 位 操作码的前缀
  • 任意一个 6 位 操作码不能是任何 8 位 操作码的前缀

这样,每条指令的 操作码 就能唯一识别其类别与长度,避免歧义,并保持系统的可扩展性和解码自同步。

🌟 再举个复杂的例子:

假设某指令系统指令长 16 位操作码字段4 位地址码字段4 位,采用扩展 操作码 技术,形成 三地址指令 15 条、二地址指令 12 条、一地址指令 63 条、零地址指令 16 条。

那么 三地址指令 格式如下:

op
A1
A2
A3
4 位
4 位
4 位
4 位

二地址指令 复用 三地址指令 的 A1 字段,一地址指令 复用 三地址指令 的 A1 和 A2 字段,零地址指令 复用 三地址指令 的 A1、A2 和 A3 字段。

可以通过树形扩展得到不同指令的 op 前缀

【三地址】OP = 0000 ~ 1110
(15条)
OP = 1111
【二地址】A1 = 0000 ~ 1011
(12条)
A1 = 1111
A1= 1100
A1 = 1101
A1 = 1110
【一地址】A2 = 0000 ~ 1111
(16条)
【一地址】A2 = 0000 ~ 1110
(15条)
A2 = 1111
【零地址】A2 = 0000 ~ 1111
(16条)

沿着树的边一直走到叶子结点,可以得到如下格式的指令:

指令类型操作码地址码1地址码2地址码3
三地址指令(15 条)0000 ~ 1110A1A2A3
二地址指令(12 条)1111 0000 ~ 1111 1011A2A3
一地址指令(63 条)1111 1100 0000 ~ 1111 1111 1110A3
零地址指令(16 条)1111 1111 1111 0000 ~ 1111 1111 1111 1111
注意

操作码扩展编码虽然属于计算机组成原理,但它所采用的设计思想,在计算机网络 中同样十分常见。

例如,变长子网划分 也是按照前缀不断扩展的方式划分地址空间:每向下一层扩展 1 位,地址空间便减半;最终,每个子网都对应二叉树上的一个叶子节点。

从二叉树的角度来看,两者都是不断将一个大的编码空间递归划分为更小的子空间:

          根节点
             │
      扩展一位(二选一)
          /       \
        0           1
      /  \        /  \
     …   …      …    …

因此,无论是 CPU 的操作码设计,还是 IP 地址的 VLSM 划分,本质上都是利用**前缀编码(Prefix Code)**来对有限的编码空间进行高效划分。只不过,一个划分的是 指令编码空间,另一个划分的是 IP 地址空间

寻址方式

计算机中的 寻址方式(Addressing Modes)是指在 指令中如何指定操作数的位置或地址寻址方式可以被归为以下种类:

立即数寻址

立即数寻址(Immediate Addressing)是一种将 常量值直接嵌入指令中 的寻址方式,常用于赋值、初始化、比较等基本操作。

立即数寻址 之中,操作数本身就是指令的一部分,而不是从寄存器或内存中取得。这种寻址方式不涉及额外的地址计算,执行效率较高。

举个实际例子,下图是指令 MOV AX, 4567H 存储结构和执行示意图,指令直接将 立即数 4567H 存储到寄存器 R1 中:

• • • • • 
OP
76H
45H
• • • • • 
76H
45H
AX
MOV AX, 4576H
立即数寻址
指令存储在 text 段在内存中
AX

📌 示例应用

应用示例说明
加载常量MOV AX, 5 —— 将常数 5 加入 AX
比较固定值CMP AL, 0 —— 判断 AL 是否为零
注意

寻址方式是针对指令还是操作数的?

寻址方式针对的是每一个操作数,而不是整条指令。

因此:

MOV AX,1234H

最严谨的表述是:

  • AX —— 寄存器寻址
  • 1234H —— 立即寻址
  • 整条指令同时包含寄存器寻址和立即寻址

不过在考题或教材中,如果说"该指令采用立即寻址",通常是约定俗成地指源操作数采用立即寻址。因此考试时可以按这个习惯理解,不会与标准定义冲突。

寄存器寻址

寄存器寻址(Register Addressing)是一种将操作数存储在寄存器中的寻址方式。在这种模式下,指令通过指定寄存器来访问操作数,寄存器本身就是操作数的存储位置。

举个实际例子,指令 MOV AX, BX 表示将寄存器 BX 中的值复制到寄存器 AX 中:

• • • • • 
OP
BX
• • • • • 
76H
45H
BX
MOV AX, BX
寄存器寻址
指令存储在 text 段在内存中
AX
76H
45H
AX

📌 示例应用

应用示例说明
拷贝寄存器内容MOV AX, BX —— 将 BX 内容拷贝到 AX
比较寄存器CMP AX, BX —— 判断 AL 是否为零

直接寻址

直接寻址(Direct Addressing)是一种通过 在指令中显式给出操作数的内存地址 来访问数据的方式,适用于访问固定位置的数据。

直接寻址 中,指令中包含了操作数在内存中的确切地址。CPU 在执行指令时,会直接从该地址读取或写入数据,不依赖寄存器辅助寻址。

举个实际例子,下图是指令 MOV R1, [1000] 的执行示意图,以 立即数 1000 作为访存地址,指令从内存地址 1000 的单元读取数据并加载到寄存器 R1 中:

3456H
R1
56H
34H
10
OP
00
内存
从相应内存地址中读取数据
存储到 R1 中
高地址
低地址
MOV R1, [1000]
译码执行
0
1000H
FFFFH
01
reg
addr

📌 示例应用

应用示例说明
访问固定内存MOV AX, [0x1234] —— 读取内存地址 0x1234 的内容
读取硬件端口IN AL, [0x60] —— 从端口地址读取键盘输入
设置显存颜色值MOV [0xB8000], AL —— 设置文本模式字符颜色
注意

上图中使用的例子是基于 8086 的,8086 的计算机直接使用物理地址,没有虚拟内存。

但是在现代计算机操作系统中,由于使用了虚拟存储器,所以编译后的程序中的地址都是虚拟地址,所以在访问实际物理内存之前需要经过一次 地址翻译

间接寻址

间接寻址(Indirect Addressing)是一种通过 寄存器或内存中的地址来访问实际数据地址 的方式,适用于访问指针、链表等动态结构。

间接寻址 中,指令中提供的是一个地址的“指针”,实际的数据地址存储在寄存器或内存单元中。CPU 先访问该中间地址,再通过它获取最终的操作数地址。

举个实际例子,下图是指令 MOV R1, [R2] 的执行示意图,访存地址间接地存储在寄存器 R2 中,指令首先从 R2 中读取目标地址,然后在相应的地址中读取数据加载进入 R1 中:

7890H
1234H
90H
78H
01
OP
10
存储到 R1 中
从相应内存地址中读取数据
译码执行
MOV R1, [R2]
内存
低地址
高地址
FFFFH
1234H
0
R1
R2
reg
reg
R2 中存储有
需读取的
内存地址

间接寻址 包含多种类型,其中最常见的是 寄存器间接寻址

操作数的地址保存在寄存器中,CPU 通过这个寄存器中存储的地址访问内存中的操作数。

当我们提到 间接寻址 时,大多数时候都是 寄存器间接寻址

📌 示例应用

应用示例说明
通过指针访问数据MOV AX, [BX] —— BX 存储了目标地址

基址寻址

基址寻址(Base Addressing)是一种通过 基址寄存器与偏移值相加 来访问结构体字段或局部变量的方式,常见于函数调用过程中的栈帧操作。

OPRbA基址寄存器(Rb) + A = E操作数E:指令基址(寻址标志)逻辑地址加法器有效地址地址主存

8086 可以用于基址寻址的寄存器实际上有两个:

  • BX(Base Register):用于访问数组或结构体的基础地址
  • BP(Base Pointer):用于访问函数栈帧的基础地址

📌 示例应用

应用示例说明
栈帧内访问局部变量或参数MOV AX, [BP - 2]MOV AX, [BP + 6]
访问数组中的元素MOV AX, [BX + 4]
函数栈帧访问

程序中最常见的是函数调用,参考 函数调用时内存结构,BP 用于指定 8086 中的函数栈底地址。

例如

高地址
+-----------------+
| 参数2           |  BP+6
+-----------------+
| 参数1           |  BP+4
+-----------------+
| 返回地址        |  BP+2
+-----------------+
| 旧BP            |  BP
+-----------------+
| 局部变量1 | BP-2
+-----------------+
| 局部变量2       |  BP-4
+-----------------+
低地址

于是:

MOV AX, [BP+4]    ; 第一个参数
MOV BX, [BP-2]    ; 局部变量
结构体和数组访问

在结构体和数组访问中,8086 中用 BX 指定数据结构在内存中的起始地址。

BX
 
 
1000: 10
1001: 20
1002: 30
1003: 40

于是

; 访问数组首地址 + 2
MOV AL,[BX+2]

变址寻址

变址寻址(Indexed Addressing)是一种通过 变址寄存器的值加上偏移量 来获取操作数地址的寻址方式,通常用于数组或表格中元素的访问。

OP......基准地址 A变址值操作数E:指令主存ALU有效地址 EA变址寄存器 Rx

📌 示例应用

应用示例说明
多维数组访问MOV AX, [BX + SI] —— 行列下标组合
结构体数组成员访问MOV AX, [DI + SI*4] —— 每个结构体占 4 字节
动态偏移数据结构遍历MOV AL, [BX + CX] —— 使用索引偏移访问
注意

基址寻址和变址寻址的区别

基址寻址变址寻址
[Base + Offset][Base + Index]
Base 基本不变Base 基本不变
Offset 通常是立即数Index 是寄存器,运行时变化
适合结构体、栈帧适合数组、循环

相对寻址

相对寻址(Relative Addressing)是一种根据 当前指令地址(PC)与偏移量 来确定跳转或访问目标位置的方式,广泛应用于控制流指令。

相对寻址 中,以当前程序计数器(PC)作为基准,通过加上一个有符号的偏移量来计算跳转目标地址。这种寻址方式便于编写可重定位代码。

OP
......
A
PC
指令
E:
指令
主存
ALU
有效地址 EA
.text

📌 示例应用

应用示例说明
条件跳转(分支)JZ LABEL —— 如果为零,跳转到相对偏移处
循环控制LOOP LOOP_START —— PC 相对跳转
实现函数局部跳转表JMP [PC + offset](某些架构中)

对于现代主流 ISA(尤其是 x86-64、AArch64、RISC-V)来说,可以近似认为:

绝大多数跳转(jmp、call、branch)默认都是 PC 相对寻址(Relative Addressing)。

举个例子:

例如 C 代码:

if (a == 0)
    foo();

编译出来(x86-64)通常类似:

cmp eax, 0
je  .L1        ; 跳转到标签

...

.L1:
call foo

实际上机器码并不是存储 .L1 的绝对地址,而是:

je +0x18

CPU 实际计算 目标地址 = 下一条指令地址(RIP) + 偏移,所以 JZ LABEL 实际上就是 JZ +offset。其中 offset = LABEL - 下一条指令地址

堆栈寻址

堆栈寻址(Stack Addressing)是一种通过 栈指针或基址指针 来访问 栈中数据 的方式,广泛应用于函数调用过程中的参数传递和返回值保存。

堆栈寻址 中,利用 SP(栈指针)或 BP(基址指针)定位栈中元素,通过栈顶向下或向上偏移来读取或写入局部变量、返回地址等。通常与 PUSHPOPCALLRET 等指令结合使用。

caller BP
BP
SP
caller BP
BP
SP
PUSH A
A
caller BP
BP
SP
PUSH B
A
B
caller BP
BP
SP
POP
A
High Address
Low Address

📌 示例应用

应用示例说明
函数调用和返回CALL FUNCRET —— 使用栈存储返回地址
保存和恢复寄存器值PUSH AXPOP AX

寻址方式对比

下表给出了各个 寻址方式 的核心区别:

寻址方式描述示例
立即寻址操作数直接包含在指令中。MOV R1, #5 (将 5 加载到 R1 寄存器)
寄存器寻址操作数在寄存器中。ADD R1, R2 (R2 加到 R1 中)
直接寻址操作数的内存地址直接包含在指令中。MOV R1, [1000] (从内存地址 1000 取数据)
间接寻址操作数的地址存储在寄存器中,指令通过寄存器访问内存中的数据。MOV R1, [R2] (R2 寄存器中是内存地址)
基址寻址使用基址寄存器和偏移量计算操作数的实际地址。MOV R1, [R2 + 4] (基址 R2 加偏移 4)
变址寻址通过基址寄存器和索引寄存器的和来确定操作数地址,常用于数组操作。MOV R1, [R2 + R3] (R2 与 R3 相加)
相对寻址操作数地址通过程序计数器(PC)当前值加上指令中的偏移量计算,常用于跳转指令。JMP LABEL (跳转到相对地址)
堆栈寻址通过堆栈顶指针(SP)来访问操作数,常用于函数调用和返回。PUSH R1 (将 R1 压入堆栈)