格式和寻址方式

指令格式是计算机组成原理中的重点考察内容,需熟练掌握,熟练张常常在大题中与 CPU 的知识交叉考察。

指令格式

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

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

  • 操作码(opcode)就是决定了指令的类型:这个指令是干嘛的?进行哪种操作?
  • 地址是一个通用含义,指的是操作的对象:
    • 可以是一个内存地址(<addr>
    • 也可以是 CPU 中的一个寄存器编号(<reg>
    • 也可以是一个立即数(<imm>

指令类型

根据操作码分类

指令根据其操作码(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 的中断标志,通常只能在内核模式下执行。

根据地址个数分类

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

指令格式指令格式含义
零地址指令op执行操作 $op$,操作数隐含在栈中
一地址指令op, A1$op(A_1) \rightarrow A_1$:对 $A_1$ 操作并将结果存回 $A_1$
二地址指令op, A1, A2$(A_1) op (A_2) \rightarrow A_1$:将 $A_1$ 和 $A_2$ 运算,结果存入 $A_1$
三地址指令op, A3, A1, A2$(A_1) op (A_2) \rightarrow A_3$:对 $A_1$ 和 $A_2$ 运算,结果存入 $A_3$

注意同一个指令系统中 根据地址个数区分的 N 地址地址 不能使用相同的前缀,否则会发生混淆。

定长和变长指令

变长指令集
定长指令集

定长指令集中所有指令长度相同,变长指令集中会包含不同长度的指令。

使用定长指令集的典型 cpu 架构是 arm(RISC),优点是解码简单、高效,但可能导致指令浪费空间。

使用变长指令集的典型 cpu 架构是 x86(CISC),优点是可以更加紧凑地编码复杂操作,但解码更加复杂。

注意

下面就要抛出两个问题了,这两个点也经常在题目中间接考察。

  1. 在定长指令集中,所有指令长度都相同,但有些指令需要的地址数量不同,如何在固定长度中容纳这些差异?
  2. 在变长指令集中,每条指令长度不同,,那么 cpu 如何判断不同指令的边界(即指令的起始和结束位置)呢?

上面两个问题有多种解决方案,这里只介绍最简单的方法。

对于第一个问题,最简单的解决方法是使用 无效字段填充:当某条指令所需的地址字段不足以填满固定的指令长度时,可以在空余位置填入无效字段或填充位。这些填充值不会参与指令的执行,仅用于占位,从而保持所有指令长度一致,简化解码过程。

针对第二个问题,一种常用的方案是采用 指令前缀 :不同类型的指令前缀不同,操作码的高位可以用来标识当前指令的长度。CPU 通过解析这些前缀或操作码位模式,就能判断一条指令的长度,从而确定下一条指令的起始位置。这种方式虽然增加了解码复杂度,但提高了指令的编码灵活性和紧凑性。

寻址方式

计算机中的寻址方式(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 是否为零

寄存器寻址

寄存器寻址(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 —— 设置文本模式字符颜色

间接寻址

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

📌 示例应用

应用示例说明
栈帧内访问局部变量或参数MOV AX, [BP - 2]MOV AX, [BP + 6]
基址寻址和变址寻址的区别

首先,两者具有共同点:基址寻址与变址寻址都类似于相对寻址,它们的有效地址 EA = 基址 + 指令字中形式地址 A。

1、基址寻址

计算公式:EA = (BR) + A

有效地址是将 CPU 中基址寄存器 BR 的内容加上指令字中形式地址 A。BR 的内容由操作系统决定,在程序执行过程中 BR 的内容不可变,而形式地址是可变的。基址寻址方式适合解决动态定位的问题。在多道程序的环境当中,操作系统根据内存空间的情况赋值给 BR,一旦赋值成功就不可更改,直至用户程序结束,使得用户不必关心实际的地址而只需要关心自己的地址空间即可。

2、变址寻址

计算公式:EA = (IX) + A

有效地址是将 CPU 中变址寄存器 IX 的内容加上指令字中有效地址 A。其指令字的形式地址作为一个基准地址,内容不可变,而 CPU 中变址寄存器 IX 在程序执行过程中根据使用情况发生改变。这样的寻址方式非常适合于循环问题,原因在于指令的“基址”(形式地址)保持不变,使得执行循环时,只需要改变 IX 的内容即可(比如迭代时,不断加 4)。假若使用基址寻址的方式,意味着循环过程中不断需要新的“基址”,也就是需要更多的指令字加以控制。而变址寻址只需要一条指令即可完成相关操作,可以大量缩短指令编码的长度,提高指令字的可用性。

两种寻址方式都是解决特定应用场景的问题,它们本质上是一样的,只是表现形式的不同而已。

变址寻址

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

📌 示例应用

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

相对寻址

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

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

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

📌 示例应用

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

堆栈寻址

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

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

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 压入堆栈)