计组与微机控制
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 位寻址靠 基址 + 变址×比例 + 位移 计算有效地址
