← 返回 计组与微机控制

计组与微机控制

7.9 循环控制指令和中断指令

7.9 循环控制指令和中断指令

一、LOOP 循环控制指令

上一页例 4.23 后面补了一句:

NEXT: LOOP CHECK HLT

这句的意思是:

CX ← CX - 1 如果 CX ≠ 0,就跳回 CHECK 如果 CX = 0,就继续向下执行 HLT

所以 LOOP CHECK 可以看成:

CX--; if (CX != 0) goto CHECK;

注意:LOOP 不影响标志位,它只看 CX。

LOOP / LOOPE / LOOPNE

LOOP label

条件:CX ← CX – 1,如果 CX ≠ 0,则跳转

LOOPE label LOOPZ label

条件:CX ← CX - 1 如果 CX ≠ 0 且 ZF = 1,则跳转

也就是:结果相等/为零,并且循环还没结束,就继续。

LOOPNE label LOOPNZ label

条件:CX ← CX - 1 如果 CX ≠ 0 且 ZF = 0,则跳转

也就是:结果不相等/不为零,并且循环还没结束,就继续。

例 4.24:比较两组输入端口数据是否一致

书上例子的大意是:

主端口 MAIN_PORT 有 NUMBER 个端口 冗余端口 REDUNDANT_PORT 也有 NUMBER 个端口 逐个读取两个端口的数据 如果都相等,继续比较下一个 如果有一个不相等,跳到 PORT_DISPUTE

程序里有一段:

MOV DX, MAIN_PORT MOV BX, REDUNDANT_PORT MOV CX, NUMBER

意思是:

DX 保存主端口地址 BX 保存冗余端口地址 CX 保存比较次数

然后:

CHECK: IN AX, DX XCHG AX, BP INC DX

解释:IN AX, DX从 DX 指定的端口读数据到 AX。

XCHG AX, BP把主端口读到的数据暂存到 BP。

INC DX主端口地址加 1,准备下次读下一个端口。

接着:

XCHG BX, DX IN AX, DX INC DX XCHG BX, DX

这一段稍微绕,其实是在做:把 DX 临时换成冗余端口地址 读取冗余端口数据 冗余端口地址加 1 再把 DX 换回主端口地址

所以 BX 里面保存的是冗余端口地址指针,DX 里面保存的是主端口地址指针。

然后:

CMP AX, BP LOOPE CHECK JNZ PORT_DISPUTE

意思是:

比较冗余端口数据 AX 和主端口数据 BP 如果相等,并且 CX 减 1 后不为 0,就继续 CHECK 如果不相等,就跳到 PORT_DISPUTE

这里用 LOOPE 很妙:它一边让 CX 减 1,一边检查 ZF 是否为 1。

二、过程调用指令 CALL / RET

这一节很重要。过程调用就像 C 语言里的函数调用。

比如:func();

汇编里大概就是:CALL func

被调用的过程执行完后,用:RET 返回。

CALL 的核心动作

CALL 不只是跳转,它还要保存返回地址。

因为过程执行完后要知道回哪里。

所以 CALL 做两件事:

1. 把返回地址压入堆栈 2. 跳到子程序入口

返回地址就是:CALL 指令下一条指令的地址

三、近过程调用 NEAR CALL

如果被调用过程和当前代码在同一个代码段里,叫近调用。

只需要保存 IP。CALL near_proc

执行过程:

SP ← SP - 2 把当前 IP 压入堆栈 IP ← 目标过程偏移地址

这里 CS 不变。所以近调用的返回地址只有一个字:IP

段内直接调用

CALL near_proc 目标过程地址直接写在指令中。

机器码里存的不是目标绝对地址,而是相对位移:目标地址 - 当前 IP

类似之前讲的段内直接 JMP。

段内间接调用

CALL BX CALL WORD PTR [BX]

区别是:CALL BX

表示:IP ← BX

而:CALL WORD PTR [BX]

表示:IP ← DS:[BX] 中存放的那个字

注意:WORD PTR [BX] 里面放的是过程入口偏移地址,不是指令本身。

四、远过程调用 FAR CALL

如果被调用过程在另一个代码段里,就叫远调用。

远调用要同时修改:CS 和 IP

所以返回的时候也必须恢复 CS 和 IP。

段间直接调用

CALL far_proc

执行过程:

先压入当前 CS 再压入当前 IP 然后 CS ← far_proc 的段地址 IP ← far_proc 的偏移地址

注意栈里保存的是返回用的 CS:IP。

段间间接调用

CALL DWORD PTR [BX]

它从内存里取 4 个字节作为目标地址:

[BX] 和 [BX+1] → IP [BX+2] 和 [BX+3] → CS

所以它调用的是一个远过程。这也是为什么书上强调:

CALL WORD PTR [BX]只能调用近过程。

CALL DWORD PTR [BX]才能调用远过程。

五、RET 返回指令

过程最后一般写:RET

RET 的任务是从堆栈中弹出返回地址。

近过程返回

如果是近过程:

从栈顶弹出 IP SP ← SP + 2

也就是回到原来 CALL 后面的那条指令。

远过程返回

如果是远过程:

先弹出 IP 再弹出 CS

然后程序回到原来的调用处。

所以:近调用 CALL 配近返回 RET,远调用 CALL 配远返回 RET

这两个必须匹配,否则返回地址会乱。

RET imm16

书上还提到:RET 4

意思是:先正常返回 再让 SP 额外加 4

这通常用于丢弃调用前压入栈中的参数。

可以理解成:返回时顺便清理参数

例如过程调用前压了两个字参数,共 4 字节,RET 4 就可以把它们从栈中丢掉。

六、中断指令 INT / IRET

中断可以理解成:CPU 当前程序先暂停,去执行一个中断服务程序,执行完后再回来。

有两种:

外部中断:外设请求 CPU 处理 内部中断:CPU 执行指令或发生异常时产生

INT n

INT n

意思是产生一个类型号为 n 的中断。

执行时,CPU 会自动做这些事:

1. 把 FLAGS 压入堆栈 2. 把 CS 压入堆栈 3. 把 IP 压入堆栈 4. 清除 TF 和 IF 5. 根据中断类型号 n 找到中断服务程序入口 6. 转去执行中断服务程序

为什么要保存 FLAGS、CS、IP?

因为中断服务程序执行完后要恢复原来的程序现场。

中断向量表

每个中断向量占 4 个字节 最多 256 个中断

每个中断向量保存的是:中断服务程序的 IP 和 CS

因为:256 × 4 = 1024 字节 = 1KB

8086 的中断向量表就在内存最低的 1KB 区域。

INTO

INTO这是溢出中断指令。

它等价于:

如果 OF = 1,则执行 INT 4 如果 OF = 0,则不产生中断

所以 INTO 专门用来检查溢出。

IRET

中断服务程序最后用:

IRET它和 RET 很像,但恢复的东西更多。

IRET 会从栈中弹出:

IP CS FLAGS

所以 IRET 不只是返回地址,还恢复原来的标志寄存器状态。

对比一下:

RET:恢复 IP,或者恢复 CS:IP IRET:恢复 IP、CS、FLAGS

七、处理器控制类指令

这一节主要是一些控制 CPU 状态的指令。

1. 标志位操作指令

这些指令直接修改标志位。

CF 进位标志

STC ;CF ← 1 CLC ;CF ← 0 CMC ;CF ← NOT CF

其中:CMC

是把 CF 取反。

如果原来 CF = 1,执行后 CF = 0。

如果原来 CF = 0,执行后 CF = 1。

DF 方向标志

STD ;DF ← 1 CLD ;DF ← 0

这个和串操作关系很大。

CLD:串操作地址递增 STD:串操作地址递减

所以我们前面经常看到:

CLD REP MOV SB

就是让字符串从低地址往高地址复制。

IF 中断允许标志

STI ;IF ← 1 CLI ;IF ← 0

含义:

IF = 1:允许可屏蔽中断 IF = 0:禁止可屏蔽中断

注意:NMI 非屏蔽中断不受 IF 控制。

2. 外同步类指令

HLT让 CPU 进入暂停状态。

CPU 会停止执行,直到出现:

RESET NMI 或者 IF=1 时的 INTR才会继续。

注意:HLT 后如果被中断唤醒,中断返回后会回到 HLT 的下一条指令。

WAIT

它用于等待外部硬件,特别是以前和 8087 协处理器配合时用。

大意是:

如果 TEST 引脚无效,CPU 等待 如果 TEST 引脚有效,CPU 继续执行

它不影响标志位。

ESC

这是给协处理器用的“逃逸指令”。8086 自己不真正执行协处理器运算,而是把相关操作数放到总线上,让 8087 这类协处理器去执行。

ESC 是早期 CPU 和协处理器配合用的指令

LOCK

这是一个前缀,不是单独普通指令。

它的作用是:

在当前指令执行期间锁住总线 防止其他总线主设备访问总线

用于多处理器或 DMA 相关场景,保证某些内存操作的原子性。

3. NOP 空操作

NOP

意思是 CPU 执行一次空操作。

它不改变寄存器、不改变内存、不改变标志位。

但是它会占用时间,并且 IP 会前进。

所以 NOP 常用来:

短延时 占位 对齐代码 调试时临时替换指令

注意和 HLT 区别很大:

NOP:什么也不做,然后继续执行下一条 HLT:CPU 暂停,等待外部事件唤醒

八、80x86/Pentium 指令格式

从 4.4 开始,书进入 80x86/Pentium 指令格式和寻址方式。

这部分是在 8086 基础上扩展。

一条 80x86/Pentium 指令的一般格式

Prefix | OP code | mod r/m | s-i-b | disp | data

也就是:

前缀字段 操作码字段 mod r/m 寻址字段 SIB 字段 位移量字段 立即数字段

每条指令不一定都有全部字段。

1. Prefix 前缀字段

前缀用于修改指令属性,常见有 5 类:

段超越前缀 操作数宽度前缀 地址宽度前缀 重复前缀 LOCK 总线锁定前缀

比如:ES:就是段超越前缀。

REP就是重复前缀。

LOCK就是总线锁定前缀。

2. OP code 操作码字段

操作码说明 CPU 要做什么。

比如:

MOV ADD SUB JMP CALL

机器码里都要靠 OP code 来区分。

3. mod r/m 字段

这个字段用来说明:

操作数在寄存器中 还是在内存中 如果在内存中,有效地址怎么计算

8086 里也有 mod、reg、r/m。

80x86/Pentium 仍然保留这种思想。

4. SIB 字段

SIB 是 32 位寻址新增的重要字段。

SIB 可以理解成:Scale + Index + Base 比例因子 + 变址寄存器 + 基址寄存器

它用于更灵活地计算地址,比如:

MOV EAX, [EBX + ESI*4 + 8]

这里:

EBX 是基址 ESI 是变址 4 是比例因子 8 是位移量

这种寻址方式在数组访问里特别常见。

5. disp 位移量字段

disp 是地址中的偏移补充量。

比如:MOV AX, [BX + SI + 20H]

其中:20H 就是 disp

在 80x86/Pentium 中,disp 可以是:

0、1、2、4 字节

6. data 立即数字段

data 是指令中直接给出的常数。

例如:

MOV AX, 1234H ADD AL, 05H

其中:

1234H,05H 就是立即数。

九、80x86/Pentium 寻址方式

书上说,实际地址仍然由两部分组成:

段基地址 + 段内偏移地址

段内偏移地址也叫:有效地址 EA

有效地址 EA 的一般公式

EA = 基址寄存器 + 变址寄存器 × 比例因子 + 位移量

这个是 32 位寻址的核心。

比如:MOV EAX, [EBX + ESI*4 + 20H]

那么:EA = EBX + ESI × 4 + 20H

32 位寻址中的 4 个分量

基址寄存器:任意 32 位通用寄存器 变址寄存器:除 ESP 外的任意 32 位通用寄存器 比例因子:1、2、4、8 位移量:0、8、32 位

这里特别注意:ESP 不能作为变址寄存器

也就是说:[EAX + ESP*4]这种形式不合法。

但 ESP 可以作为基址寄存器,比如:[ESP + 4]这是合法的。

十、32 位寻址相比 8086 增加了什么?

8086 常见地址形式是:

[BX + SI] [BX + DI] [BP + SI] [BP + DI] [SI] [DI] [BX] [BP]

到了 32 位 80x86/Pentium,地址形式更灵活,可以写:

[EAX] [EBX + 8] [EBX + ESI] [EBX + ESI*4] [EBX + ESI*4 + 20H]

所以它特别适合数组。比如 C 语言里:a[i]

如果一个元素是 4 字节,那么地址可以写成:[EBX + ESI*4]

其中:EBX = 数组首地址,ESI = 下标 i,4 = 每个元素 4 字节

这就是比例变址寻址的意义。

十一、这几页最重要的脉络

LOOP:CX 减 1,不为 0 就跳 CALL:保存返回地址,然后跳到过程 RET:从栈中取返回地址回来 INT:保存 FLAGS、CS、IP,然后进入中断服务程序 IRET:恢复 IP、CS、FLAGS HLT:停机等待 NOP:空操作继续 LOCK:锁总线

然后 80x86/Pentium 指令格式重点是:Prefix | OP code | mod r/m | SIB | disp | data

32 位寻址重点是:EA = Base + Index × Scale + Displacement

过程调用靠堆栈保存返回地址 32 位寻址靠 基址 + 变址×比例 + 位移 计算有效地址