Program Control Instructions⚓︎
约 10594 个字 245 行代码 预计阅读时间 56 分钟
The Jump Group⚓︎
- 通过无条件或条件跳转(unconditional / conditional jump) 指令,能跳过程序段,并跳转到内存的任何部分以执行下一条指令
- 条件跳转指令允许基于数值测试做出决策,结果存储在标志位中,然后由条件跳转指令测试
LOOP和条件LOOP也是跳转指令的一种形式
Unconditional Jump (JMP)⚓︎
无条件跳转指令分为三类:
- 短跳(short jump):2 字节指令,允许跳转到以跳转指令地址为中心的 -128B ~ 127B 范围内的内存位置
- 近跳(near jump):3 字节指令,允许跳转到在当前代码段中的指令 ±32 KB 范围内的地方
- 远跳(far jump):5 字节指令,允许跳转到在真实内存系统上的任意位置
其中前面两种指令称为段内跳转(infrasegment jump),最后一种称为段间跳转(intersegment jump)。
Short Jump⚓︎
-
短跳又称相对跳转(relative jump),因为可以通过相关软件移动到当前代码段中的任何位置,而不会发生变化
- 跳转地址并不存储在操作码中
- 操作码后会跟一个(跳转)距离(distance) 或偏移量 (displacement)
-
短跳偏移量是一个用 1 字节符号数表示的距离,范围在 -128 ~ 127 之间
-
右图展示了短跳操作的细节:
- 当微处理器执行短跳时,对偏移量进行符号扩展,并将其加到指令指针(
IP/EIP)中,以生成在当前代码段内的跳转地址 - 指令跳转到程序中下一条指令的新地址
- 当微处理器执行短跳时,对偏移量进行符号扩展,并将其加到指令指针(
-
当跳转指令引用一个地址时,通常用标签标识该地址,比如
JMP NEXT- 跳转到标签
NEXT上以执行下一条指令 - 很少会在跳转指令中使用十六进制地址
- 标签
NEXT后必须跟一个冒号(NEXT:) ,从而让指令能够引用它,否则该标签无法被引用 - 唯一使用冒号的情况是当标签与跳转或调用指令一起使用时
- 跳转到标签
Near Jump⚓︎
- 实模式下,近跳是一个 3 字节指令,包含操作码以及一个 16 位的符号偏移量
- 近跳将控制权传递给当前代码段内的一个指令,该指令位于近跳转指令的 ±32KB 范围内
- 因此可以跳转到当前实模式代码段内的任何内存位置
- 80386 - Pertium 4 中偏移量是 32 位的,因此近跳指令长为 5 字节
- 在保护模式下,80386 以上的处理器的跳转范围为 ±2GB
- 将符号偏移量加在指令指针上(
IP) ,生成跳转地址 -
下图展示了近跳操作的细节:
-
近跳是可重定位的(relocatable),因为它也是一种相对跳转
- 该功能连同可重定位的数据段,使得 Intel 微处理器是通用计算机系统的理想选择
- 软件可以写入和加载到内存的任何位置,并且由于相对跳转和可重定位的数据段,无需修改即可运行
例子
下面是一段用到近跳的程序:
0000 33DB XOR BX,BX
0002 B8 0001 START: MOV AX,1
0005 03 C3 ADD AX,BX
0007 E9 0200 JMP NEXT
000A
... <skipped memory locations>
0200 8B D8 NEXT: MOV BX,AX
0202 E9 0002 R JMP START
- 第一个跳转指令(
JUMP NEXT)将控制权传递给代码段内内存地址偏移量为0200H的指令 - 注意到指令汇编为
E9 0200 R。字母R表示0200H的可重定位跳转地址 - 链接后,跳转指令以
E9 F6 01形式呈现(真实的偏移量为01F6H(0200H - 000AH = 01F6H) )
Far Jump⚓︎
- 获取新的段和偏移地址来实现跳转
- 在实模式在是 5 字节指令
- 字节 2 和 3 包含新的偏移地址
- 字节 4 和 5 包含新的段地址
-
在保护模式下
- 段地址访问具有远跳段的基址的描述符
- 16 或 32 位的偏移地址包含新代码段的偏移地址
-
下图展示了远跳操作的细节:
-
两种实现远跳的方式:
- 使用
FAR PTR指示符,比如JMP FAR PTR START - 定义一个远标签(far label),比如
EXTRN START:FAR
- 使用
-
仅当标签在当前代码段或过程的外面时,该标签才是远标签;外部标签出现在包含多个程序文件的程序中
- 两种定义可被来自过程块外访问的远标签的方式:
- 使用
EXTRN指示符,比如EXTRN START:FAR - 使用双冒号,比如
START::
- 使用
例子
EXTRN UP:FAR
0000 33 DB XOR BX, BX ; far label
0002 B8 0001 START: ADD AX, 1
0005 E9 0200 R JMP NEXT
;<skipped memory locations>
0200 8B D8 NEXT: MOV BX, AX
; segment address
0202 EA 0002 -- R JMP FAR PTR START ; far jump
0207 EA 0000 -- E JMP UP ; far jump
- 当标签
UP通过EXTRN UP:FAR指令定义为远标签时,JMP UP指令引用了一个远标签 - 当程序文件被连接时,链接器将
UP标签的地址插入到JMP UP指令中,还将段地址插入到JMP START指令中
Jump with Register Operands⚓︎
-
跳转指令可以使用 16 或 32 位寄存器作为操作数
- 自动设置为绝对间接跳转(absolute indirect jump)
- 跳转地址位于跳转指令指定的寄存器中
-
与近跳转相关的偏移不同,寄存器内容直接被传输给指令指针上,而不会和指令指针做加法
- 例子:
JMP AX将AX寄存器的内容复制到IP上- 而在 80386 以上的处理器中,
JMP EAX能跳转到当前代码段的任意位置上- 因为在保护模式下,代码段长度为 4GB,因此需要 32 位的偏移地址
Indirect Jump Using Indexes⚓︎
- 跳转指令也可能使用 [] 形式的寻址(比如
JMP TABLE [SI])来直接访问跳转表(jump table) -
跳转表可以包含近间接跳转的偏移地址,或者远间接跳转的段和偏移地址
- 若寄存器跳转被称为间接跳转,那么这种跳转也可被称为双重间接跳转(double-indirect jump)
-
编译器假定跳转是近的,除非
FAR PTR指令指示为远跳转指令 -
访问跳转表的机制与正常内存引用相同
- 例子:
JMP TABLE [SI]指令指向由SI指定的数据段偏移位置存储的跳转地址
- 例子:
-
寄存器和间接索引跳转指令通常指向 16 位偏移,因此都属于近跳转
- 如果
JMP FAR PTR [SI]或JMP TABLE [SI],并且TABLE数据由DD指令定义,那么微处理器假定跳转表包含双字,32 位地址(IP和CS)
Conditional Jumps⚓︎
- 在 8086-80286 中,条件跳转指令始终是短跳转,范围限制在条件跳转后位置 -128 ~ 127 字节内
-
在 80386 及以上版本中,条件跳转可以是短跳转或近跳转(±32K)
- 在 Pertium 4 的 64 位模式下,条件跳转的近跳转距离为 ±2G
- 允许条件跳转到当前代码段内的任何位置
-
条件跳转指令测试标志位:
- 符号(
S) ,零(Z) ,进位(C) - 奇偶(
P) ,溢出(O)
- 符号(
-
如果测试的条件为真,则跳转到指定标签上
- 否则按顺序执行程序中的下一条指令
- 例子:如果进位标志位置 1,则
JC将执行跳转操作 - 大多数条件跳转指令都很直接,因为它们通常只测试一个标志位(不过还是有测试多个标志位的指令)
思考
如何比较一个有符号值和一个无符号值?比如当比较 -1(0xFF)和 1(0x01)时,-1 看起来更大
有符号和无符号的比较是相同的,关键在于如何解释标志位。比如关于 -1 和 1 的无符号数和符号数比较:
-
因为编程中既可能使用有符号数,也可能使用无符号数,而这些数的顺序不同,所以存在两组用于大小比较的条件跳转指令
- 16 位和 32 位数字的顺序与 8 位数字相同,只是它们更大
-
下图展示了有符号和无符号 8 位数字的顺序
-
当比较无符号数时,使用 JA、JB、JAE、JBE、JE 和 JNE 指令
- above 和 below 均指代无符号数
-
当比较符号数时,使用 JG、JL、JGE、JLE、JE 和 JNE 指令
- greater than 和 less than 均指代符号数
-
下表列出了完整的条件跳转指令列表:
例子:加热器控制
下面的简短程序展示了如何使用温度计和加热器保持恒定温度(在 15℃ 至 25℃ 之间
- 假设空气温度低于 15℃
- 空气温度可以从 I/O 端口 125 读取
- 加热器可以通过 I/O 端口 127 打开(
1)和关闭(0)
-
剩余的条件跳转测试单个标志位,例如溢出和奇偶校验
- 注意,
JE有一个替代操作码JZ
- 注意,
-
所有指令都有替代指令,但许多在编程中未使用,因为它们通常不符合测试的条件
-
条件跳转指令都测试标志位,除了
-
JCXZ指令直接测试CX寄存器的内容- 如果
CX = 0,则发生跳转 - 如果
CX != 0,则不发生跳转
- 如果
-
JECXZ则测试ECX寄存器的内容- 如果
ECX = 0,则发生跳转 - 如果
ECX != 0,则不发生跳转
- 如果
-
例子
- 以下程序使用
SCASB指令在表中搜索0AH - 搜索之后使用
JCXZ指令测试CX以查看计数是否达到零
Conditional Sets⚓︎
- 80386 - Core 2 处理器还包含条件设置指令(conditional set instructions)
-
条件设置字节(
SETcc)指令检查EFLAGS寄存器中的状态标志- 如果标志符合助记符(cc)中指定的条件,将指定 8 位内存位置或寄存器中的值设置为 1
- 如果标志不符合指定的条件,
SETcc将内存位置或寄存器清 0
-
例子:
SETC EAX- 如果进位被设置,
EAX = 01H;如果进位被清除,EAX = 00H -
EAX的内容可以在程序稍后的某个点进行测试,以确定SETC EAX指令执行点是否清除进位 -
条件设置指令在必须测试程序中较晚的点上的条件时很有用
- 如果进位被设置,
-
下表罗列了各种条件设置指令:
-
软件通常使用
SETcc指令来设置逻辑指示器 -
与
CMOVcc指令类似,SETcc指令可以替换两个指令:一个条件跳转和一个移动,比如:
LOOP⚓︎
LOOP指令使用RCX/ECX/CX作为计数器执行循环操作-
LOOP指令等价于SUB RCX/ECX/CX, 1JNZ label
-
每次执行
LOOP指令时,计数器会递减,然后检查是否为 0- 如果
CX != 0,则执行到标签的近跳转 - 如果
CX == 0,则执行下一条指令
- 如果
-
LOOP指令不影响任何标志 - 在 16 位指令模式下,
LOOP使用CX - 在 32 位模式下,
LOOP使用ECX - 在 64 位模式下,循环计数器在
RCX中 - 使用的计数寄存器的大小取决于
LOOP指令的地址大小属性
例子
以下程序计算 BLOCK1 和 BLOCK2 的内容之和,并将结果存储在 BLOCK2 的数据上方。
.MODEL SMALL ; select SMALL model
.DATA ; start data segment
BLOCK1 DW 100 DUP (?) ; 100 words for BLOCK1
BLOCK2 DW 100 DUP (?) ; 100 words for BLOCK2
.CODE ; start code segment
.STARTUP ; start program
MOV AX, DS ; overlap DS and ES
MOV ES, AX
CLD ; select auto-increment
MOV CX, 100 ; load counter
MOV SI, OFFSET BLOCK1 ; address BLOCK1
MOV DI, OFFSET BLOCK2 ; address BLOCK2
L1: LODSW ; load AX with BLOCK1
ADD AX, ES: [DI] ; add BLOCK2
STOSW ; save answer
LOOP L1 ; repeat 100 times
.EXIT
END
Conditional LOOPs⚓︎
-
LOOP指令也有条件形式,语法为:LOOPE destLOOPNE dest
-
LOOPE(循环直到相等 (loop while equal))指令接受ZF标志作为在计数达到零之前终止循环的条件- 如果条件不相等或
CX寄存器递减到 0,将退出循环
- 如果条件不相等或
-
LOOPNE(当不相等时循环 (loop while not equal))在CX不等于 0 且存在不相等条件时跳转- 如果条件相等或
CX寄存器递减到 0,将退出循环
- 如果条件相等或
-
LOOPE在扫描数组以查找第一个不匹配给定值的元素时很有用 LOOPNE在扫描数组以查找第一个匹配给定值的元素时很有用LOOPE和LOOPNE指令不影响任何标志-
在 80386 - Core 2 处理器中,条件循环可以使用
CX或ECX作为计数器- 如有需要,
LOOPEW/LOOPED或LOOPNEW/LOOPNED可覆盖指令模式
- 如有需要,
-
在 64 位的操作下,循环计数器使用
RCX,宽度为 64 位 -
LOOPE和LOOPNE存在替代方案LOOPE == LOOPZLOOPNE == LOOPNZ
-
在大多数程序中,只有
LOOPE和LOOPNE适用
例子
以下代码寻找数组中的第一个正整数:
.data
array DW -3,-6,-1,-10,10,30,40,4
.code
MOV ESI, OFFSET array
MOV ECX, LENGTHOF array ; get the number of array
next:
TEST WORD PTR [ESI],8000h ; sign bit=1 (negative), Z=0, else Z=1 (positive)
PUSHF ; protect flags on stack
ADD ESI, 2 ; inc ESI to next value
POPF ; restore flags from stack
LOOPNZ next ; if Z=0 and ECX != 0 continue
JNZ quit ; if Z=0, not found
SUB ESI, 2 ; ESI points to value
quit:
For, Do-while, While Loops in Assembly Language⚓︎
-
do-while循环 -
while循环 -
for循环
Controlling the Flow of the Program⚓︎
- 使用汇编语言指示符
.IF、.ELSE、.ELSEIF和.ENDIF来控制程序流程比使用正确的条件跳转语句要容易得多,这些语句始终指示一个对于 MASM 特殊的汇编语言命令 - 从点号开始的控制流程汇编语言语句在 MASM 6.xx 版本中可用,而不适用于更早的版本
-
其他指示符包括
.REPEAT–.UNTIL和.WHILE–.ENDW- 点命令(dot commands) 在 Visual C++ 内联汇编器中不起作用
-
在内联汇编器中,绝不要使用大写字母表示汇编语言指示符,因为其中一些被 C++ 保留,使用会导致问题
-
条件控制流指示符:
-
条件控制流运算符:
.IF, .ELSE, .ELSEIF, and .ENDIF⚓︎
WHILE Loops⚓︎
-
使用
.WHILE语句与条件开启循环.ENDW语句结束循环
-
.BREAK和.CONTINUE语句可用于while循环.BREAK通常跟随着.IF来选择中断条件,例如.BREAK .IF AL == 0DH.CONTINUE可用于在满足特定条件时允许DO-.WHILE循环继续
例子
以下示例展示了如何使用 .WHILE 语句从键盘读取数据并将其存储到数组(BUF)中,直到回车键(0DH
.CODE ; start code segment
.STARTUP ; start program
MOV AX, DX ; overlap DS with ES
MOV ES, AX
CLD ; select auto-increment
MOV DI, OFFSET BUF ; address buffer
.WHILE AL != 0DH ; loop while not enter
jmp @C0001
@C0002:
MOV AH, 1 ; read key
INT 21H
STOSB ; store key code
.ENDW
* @C0001:
cmp al, 0dh
jne @C0002
MOV BYTE PTR[DI-1], '$' ; '$' is the terminated string for DOS INT 21H function 9
MOV DX, OFFSET BUF
MOV AH, 9
INT 21H ; display BUF
.EXIT
END
REPEAT-UNTIL Loops⚓︎
REPEAT-UNTIL循环表示一系列指令会重复执行,直到某个条件发生.REPEAT语句定义了循环的开始- 循环的结束由包含条件的
.UNTIL语句定义 .UNTILCXZ指令使用了LOOP指令检查作为计数器的寄存器CX以进行重复循环
例子
以下示例展示了如何使用 REPEAT-UNTIL 从键盘读取数据并将其存储到数组(BUF)中,直到回车键(0DH
.CODE ; start code segment
.STARTUP ; start program
MOV AX, DX ; overlap DS with ES
MOV ES, AX
CLD ; select auto-increment
MOV DI, OFFSET BUF ; address buffer
.REPEAT ; repeat until enter
@C0001:
MOV AH, 1 ; read key
INT 21H
STOSB ; store key code
.UNTIL AL == ODH
cmp al, odh
jne @C0001
MOV BYTE PTR [DI-1], 'S
MOV DX, OFFSET BUF
MOV AH, 9
INT 21H ; display BUF
.EXIT
END
Procedures⚓︎
-
过程(procedure) 是一组指令的集合,通常用于执行一个任务
- 子例程 (subroutine)、方法或函数是任何系统架构的重要组成部分
-
过程是软件中存储在内存中的可重用部分,根据需要频繁使用(节省内存空间,使软件开发更容易)
- 一个过程以
PROC指令开始,并以ENDP指令结束;每个指令都显示过程名称 PROC后跟过程的类型,包括NEAR或FAR- 在 MASM 版本 6.x 中,
NEAR或FAR类型可以跟USES语句,实现在过程中自动将任意数量的寄存器压入栈并在栈中弹出 RET用于结束或退出过程
- 被所有软件(全局)使用的程序应编写为远过程
- 由特定任务使用的程序(局部)通常定义为近过程
- 大多数程序是近过程
CALL将紧随其后的指令地址(返回地址)推送到栈上RET指令从栈中移除一个地址,以便程序返回到紧随CALL之后的指令- 过程的缺点是计算机链接到过程(
CALL)并从中返回(RET)需要一定时间
CALL⚓︎
CALL将程序流转移到过程上CALL指令与跳转指令不同,因为CALL会将返回地址保存到栈上- 当执行
RET指令时,返回地址将控制权返回到CALL指令之后的指令 -
有四种不同类型的调用:
- 近调用(near call)(段内调用
) :对当前代码段内过程的调用 - 远调用(far call)(段间调用
) :对位于与当前代码段不同的段中的过程的调用 - 跨特权级远调用(inter-previlige-level far call):对位于与当前执行程序特权级不同的段中的过程的远程调用
- 任务切换(task switch):对位于不同任务中的过程的调用
- 近调用(near call)(段内调用
-
后两种调用类型只能在保护模式下执行
Near CALL⚓︎
-
在执行近调用时
- 在执行
CALL指令之前将参数压入栈中(由程序员或编译器实现) - 将
EIP寄存器的值压入栈中(通过调用)
- 在执行
-
然后跳转到当前代码段中由目标操作数指定的地址
- 近调用不会改变
CS寄存器的值
-
目标操作数指定两种类型
- 相对偏移(相对于当前指令指针指向的
CALL指令后的一条指令的符号偏移) - 代码段的绝对偏移(从代码段基址开始的偏移)
- 相对偏移(相对于当前指令指针指向的
-
对于“近绝对间接(near absolute indirect)”调用,绝对偏移通过寄存器或内存位置间接指定,例如:
-
对于“近相对直接(near relative direct)”调用,指令长为 3 字节
- 第一个字节是操作码
- 第二个和第三个字节包含位移量
-
当执行近相对
CALL时- 首先将下一条指令的偏移地址压入栈,下一条指令的偏移地址出现在指令指针(
IP/EIP)中 - 然后将位移量加到
IP上,以将控制权转移到过程
- 首先将下一条指令的偏移地址压入栈,下一条指令的偏移地址出现在指令指针(
-
之所以要在栈上保存
IP或EIP,是因为指令指针始终指向程序中的下一个指令 - 对于
CALL指令,IP/EIP的内容会被压入栈中;在过程结束后,程序控制传递到CALL指令后的指令 -
下图展示了存储在栈上的返回地址(
IP)和调用过程
Far CALL⚓︎
-
远调用是一条 5 字节指令,包含一个操作码,其后是
IP和CS寄存器的下一个值- 字节 2 和 3 包含
IP的新内容 - 字节 4 和 5 包含
CS的新内容
- 字节 2 和 3 包含
-
远调用在跳转到由字节 2-5 指示的地址之前,将
IP和CS的内容压入栈
- 远调用可调用内存中任何位置的程序,并从该程序返回
-
在 64 位模式下,远调用可以指向任何内存位置,放置到栈上的信息是一个 8 字节数字
- 远返回指令从栈中检索一个 8 字节返回地址并将其放入
RIP
- 远返回指令从栈中检索一个 8 字节返回地址并将其放入
-
下图展示了远调用是如何调用远过程的:
CALLs with Register Operands⚓︎
以下示例使用 CALL 寄存器指令调用一个从当前代码段的偏移地址 DISP 开始的过程,以显示“OK”。
MOV BX,OFFSET DISP ; load BX with offset DISP
MOV DL,'O' ; display O
CALL BX ; call DISP (in BX)
MOV DL,'K' ; display K
CALL BX ; call DISP (in BX)
.EXIT
;
; Procedure that displays the ASCII character in DL
;
DISP PROC NEAR
MOV AH,2 ; select function 2
INT 21H ; execute DOS function 2
RET
DISP ENDP
CALLs with Indirect Memory Addresses⚓︎
- 这种调用方式在程序中需要选择不同的子例程时特别有用
-
本质上与使用查找表进行跳转地址的间接跳转相同
-
CALL指令也可以引用远指针:指令以CALL FAR PTR [4*EBX]或CALL TABLE [4*EBX]的形式出现,并且表中的数据被定义为双字
RET⚓︎
RET 指令从栈中移除一个 16 位数字(近返回IP 中;或者移除一个 32 位数字(远返回)并将其放入 IP 和 CS 中。
- 程序的
PROC指示符中包括近返回和远返回指令 - 自动选择适当的返回指令
下图展示了 CALL 指令如何链接到过程,以及如何在 8086–Core2 的实模式下通过 RET 返回:
在 80386 及以上版本的处理器以保护模式运行时,远返回会从栈中移除 6 个字节
- 前 4 个字节包含
EIP的新值 - 后 2 个字节包含
CS的新值
而同样在保护模式中,近返回从栈中移除 4 个字节,并将其放入 EIP 中。
另一种返回形式(RET n)在从栈中移除返回地址后,将一个数字添加到栈指针(SP)的内容中;栈指针将根据所指示的字节数进行调整。
例子
以下实例展示了上述形式的返回如何清除通过几个推送放置在栈上的数据:
RET 4在从栈中移除返回地址后,将 4 加到SP上- 参数通过使用
BP寄存器在栈上进行寻址
MOV AX,30
MOV BX,40
PUSH AX ; stack parameter 1
PUSH BX ; stack parameter 2
CALL ADDM ; add stack parameters
ADDM PROC NEAR
PUSH BP ; save BP
MOV BP,SP ; address stack with BP
MOV AX,[BP+4] ; get parameter 2
ADD AX,[BP+6] ; add parameter 1
POP BP ; restore BP
RET 4 ; return, dump parameters
ADDM ENDP
Transfer Control Between Privilege Levels⚓︎
- 分支(branches) 还可以用于将控制转移到在不同特权级别下运行的其他代码
- 在这种情况下,处理器会自动检查源程序和目标程序的特权,以确保在执行控制转移操作之前允许该转移
-
三种实现跨特权级远调用(inter-previlige-level far call) 的方法:
- 定义符合规范的代码段(conforming code segment) 以共享不同特权级别的库(比如数学库)
-
通过称为门(Gates) 的特殊段描述符
-
利用快速系统调用指令(fast system call instructions)(
SYSCALL / SYSRET或SYSENTER / SYSEXIT)从环 3 访问环 0
Interrupts and Exceptions⚓︎
- 中断(interrupts) 和异常(exceptions) 强制控制从当前执行的程序转移到处理该中断事件的系统软件服务例程
- 这些例程被称为异常处理程序和中断处理程序,或中断服务过程(interrupt service procedure, ISP)
- 在转移控制到 ISP 期间,处理器停止执行被中断的程序并保存其返回指针
- 处理异常或中断的系统软件服务例程负责保存被中断程序的状态,这使得处理器在系统处理完事件后重新启动被中断的程序
- 处理器使用分配给异常或中断的向量编号(中断向量)作为进入中断向量表(interrupt vector table, IVT) 或中断描述符表(interrupt descriptor table, IDT) 的索引,而这两张表提供了异常或中断处理程序的入口点
- IVT 在实模式下使用
- IDT 在保护模式和长模式下使用
比较中断和异常
- 是由外部硬件或软件生成的信号,用于请求处理器或操作系统内核提供服务
- 是程序自愿发出的请求,希望获取帮助
- 大多数中断是异步的(如硬件中断
) ,使用INT n指令的软件中断是同步的
- 是在执行指令期间直接由 CPU 产生的事件,通常由于错误(errors) 或异常(unusual) 情况
- 说明程序偏离了正常流程,CPU 将其识别为需要特殊处理的情况
- 每个异常都有一个助记符,由一个井号(
#)后跟两个字母和一个可选的错误码(括号内)组成。例如,#GP(0)表示具有错误码 0 的一般保护异常
Sources of Interrupts⚓︎
- 外部(硬件生成)中断:是一个异步事件,通常由 I/O 设备通过处理器上的引脚或通过本地 PIC(可编程中断控制器)触发
- 软件生成的中断:执行
INT n指令的结果- 例如,在 x86 系统上使用
INT 80h来调用 Linux 中的系统调用并请求操作系统提供服务
- 例如,在 x86 系统上使用
Masking External Interrupts⚓︎
- 软件可以屏蔽某些异常和中断的发生;屏蔽(masking) 可能会延迟甚至阻止异常处理或中断处理机制的触发
- 外部中断分为可屏蔽和不可屏蔽:
- 可屏蔽中断(maskable interrupts) 通过
INTR引脚由中断处理机制触发,仅在FLAGS.IF=1时有效。否则,它们将保持待处理状态,直到FLAGS.IF位被清除为0 - 不可屏蔽中断(nonmaskable interrupts, NMI) 不受
FLAGS.IF位值的影响
- 可屏蔽中断(maskable interrupts) 通过
Interrupt Control⚓︎
有两条可以控制可屏蔽中断的指令:
- 设置中断标志指令(set interrupt flag instruction)(
STI)将 1 放入I标志位,从而启用INTR引脚 - 清除中断标志指令(clear interrupt flag instruction)(CLI)将 0 放入
I标志位,从而禁用INTR引脚
在软件中断服务程序中,硬件中断作为首要的步骤之一,通过 STI 指令启用。之所以这样做,是因为几乎所有个人计算机中的 I/O 设备都是通过中断处理的。如果中断禁用时间过长,会导致严重的系统问题。
Sources of Exceptions⚓︎
- 程序错误异常(program-error exceptions):当处理器在执行过程中检测到程序错误时,会生成异常
- 例子:除法错误(
#DE)异常
- 例子:除法错误(
- 软件生成的异常(software-generated exceptions):
INTO、INT1、INT3和BOUND指令允许在软件中生成异常- 例子:
INT3会导致生成断点异常
- 例子:
- 机器检查异常(machine-check exceptions):Pertium 处理器提供了机器检查机制,用于检查内部芯片硬件和总线事务的操作
Precise and Imprecise Exceptions⚓︎
-
精确异常在指令边界上报告
- 有些在导致异常的指令之前报告边界,而有些则在导致异常的指令之后报告边界
- 当事件处理程序返回到被中断的程序时,可以从被中断的指令边界重新启动
-
不精确异常不保证在可预测的指令边界上报告
- 不精确事件可以视为是异步的
- 被中断的程序不可重启
Three Types of Exceptions⚓︎
-
故障(faults):在故障指令之前的边界上报告的精确异常
- 可以纠正并重新启动,且不会丢失连续性
- 返回地址指向故障指令
-
陷阱(traps):在陷阱指令之后的边界上报告的精确异常
- 可以继续执行而不丢失程序连续性
- 返回地址指向紧随其后的指令
-
中止(aborts) 是非精确异常,不允许可靠的程序重启
Interrupt Vectors⚓︎
- 特定的中断和异常源被分配一个固定的向量识别号(称为“中断向量”或简单地称为“向量”)
- 中断向量(interrupt vectors) 为中断处理机制所用,用于定位分配给异常或中断的服务例程
- 最多可用 256 个唯一的中断向量,每个向量包含一个
ISP的地址 - Intel 保留了前 32 个向量用于预定义的异常和中断条件
- 中断向量(32–255)对用户可用。
-
在实模式下
- 一个 4 字节的数字存储在内存的前 1024 字节中(00000H–003FFH)
- 每个向量包含一个
IP和CS的值,形成ISP的地址 - 前 2 个字节包含
IP,最后 2 个字节为CS
-
在保护模式下,中断向量表被中断描述符表替代,该表使用 8 字节描述符来描述每个中断
Double Fault Exception⚓︎
- 当在处理先前(第一个)异常或中断处理程序时发生第二个异常时,可能会出现双重故障异常(double fault exception)(参阅 AMD64 手册(第 8.2.9 节
) ) - 例如,当触发页错误但在中断描述符表中没有页错误处理程序时,会发生双重故障
- 通常,第一次和第二次异常可以顺序处理,从而避免双重故障异常
- 然而,在某些情况下,第一个异常会对处理器处理第二个异常的能力产生不利影响,因此它会发出双重故障异常信号
- 这些异常促成了双重故障异常的发生,被称为贡献性异常(contributory exceptions)
-
只有一些非常特定的异常组合才会导致双重故障,这些组合包括:
-
例子:
- 除零错误 -> 页错误:不发生双重错误
- 除零错误 -> 一般保护错误:发生了双重错误
-
如果在尝试调用双重故障处理程序时发生另一个贡献性或页面故障异常,处理器将进入关闭模式 (shutdown mode)
- 在 x86 架构中,三重故障(triple fault) 是一种特殊的异常,即当 CPU 尝试调用双重故障异常处理程序时发生异常
- 提供双重故障处理程序非常重要,因为如果未处理双重故障,将会发生致命的三重故障,这会促使启动 CPU 重置
Error Codes⚓︎
- 某些类型的异常提供错误码(error codes),它会报告有关错误的附加信息(比如
#PF(故障码) ) - 在控制转移到异常处理程序期间,异常机制将错误码推送到栈上
-
错误码的两种格式:
- 用于错误报告异常的选择器格式(selector format)
- 用于页错误的页错误格式(page-fault format)
Priority among Simultaneous Exceptions and Interrupts⚓︎
- 当同时发生中断时,处理器将控制权转移到优先级最高的中断处理程序
- 来自外部源的低优先级中断被处理器暂时挂起,并在高优先级中断之后进行处理
- x86 架构定义了不同组之间的中断优先级,而组内的中断优先级则依赖于具体实现
同时中断的优先级如下:
Real-Mode Interrupt Control Transfers⚓︎
- 在实模式下,IDT 是一个 4 字节条目的表,每个条目对应系统实现的 256 个可能的中断之一。实模式 IDT 通常被称为中断向量表(IVT)
- IVT 表条目包含指向异常或中断处理程序的远指针(
CS:IP对) - IDT 的基址存储在
IDTR寄存器中,在处理器复位时加载值为00h
当在实模式下发生异常或中断时,处理器执行以下操作:
- 将
FLAGS压入栈 - 将
FLAGS.IF清零 - 将
CS和IP压入栈 - (如果适用)将错误代码压入栈
- 通过将中断向量乘以 4 来定位 IDT 中的
ISP - 将控制权转移到 IDT 中由
CS:IP引用的ISP
IRET 指令用于返回到被中断的程序。当执行 IRET 时,处理器执行以下操作:
- 从栈中弹出保存的
CS:IP - 从栈中弹出
FLAGS值 - 执行从保存的
CS.IP位置开始
IRET 指令等同于远RET + POPF。
其中 IRET 用于实模式,IRETD 用于保护模式。
Interrupt Instructions⚓︎
- 四种中断指令:
INT N、INT 1、INT3和INTO - 在实模式下,每个指令从向量表中获取一个向量,然后调用存储在该向量所指地址处的过程
- 在保护模式下,每个指令从中断描述符表中获取一个中断描述符
- 类似于远
CALL指令(它将返回地址(IP/EIP和CS)放入栈)
INT N⚓︎
- 256 种不同的软件中断指令(
INT N)可供程序员使用。 - 每个
INT指令都有一个数值操作数,其范围是 0 到 255(00H–FFH) -
在实模式下,中断向量的地址是通过将中断类型号乘以 4 来确定的
- 例如,
INT 10H指令调用存储在内存位置40H(10H * 4)开始的中断服务例程的地址
- 例如,
-
在保护模式下,中断描述符通过将类型号乘以 8 来定位,因为每个描述符长度为 8 字节
-
每条
INT N指令长度为 2 字节- 第一个字节为操作码
- 第二个字节为向量类型号
-
当软件中断执行时,它:
- 将标志压入栈
- 清除
IF标志位 - 将
CS压入栈 - 从中断向量获取
CS的新值 - 将
IP/EIP压入栈 - 从向量获取
IP/EIP的新值 - 跳转到由
CS和IP/EIP指定的新位置
-
软件中断最常用于调用系统过程,因为不需要知道函数的地址
- 也通常控制打印机、视频显示器和磁盘驱动器等外设上
INT N替代了原本用于调用系统函数的远CALLINT N指令长度为 2 字节,而远CALL长度为 5 字节- 因此每次
INT N指令替代一个远CALL时,可以节省 3 字节的内存 - 如果
INT N在程序中经常出现,例如在系统调用中,这可以带来相当可观的内存节省
INT3⚓︎
- 它是单字节长的中断指令(0xCC
) (而其他中断指令都是两字节的) - 通常在软件中插入
INT3以中断或打断软件的流程,即断点(breakpoints),有助于调试故障软件 - 对于任何软件中断都会发生一个断点,但由于
INT3长度为 1 个字节,因此更容易实现此功能
GDB 实现端点的方式:
- 假设在
OFFSET处的原始指令是0x8345fc01(ADD DWORD PTR[ebp-0x4], 0x1) - 当我们在 GDB 中输入
break OFFSET时,它会记住这个字的第一个字节(0x83) ,并将该字更改为0xcc45fc01
- 当这个程序遇到 GDB 刚插入的
INT3指令时,调试的程序将陷入内核,而内核将反过来向 GDB 发出信号 - 然后,GDB 将使用它记住的原始值恢复
OFFSET处的字节,并将指令指针EIP移回OFFSET,以重新启动OFFSET处的指令
思考:为什么 INT3 应该是一字节的呢?
假如 INT3 比某些 x86 指令更长。当 GDB 插入一个断点时,它可能会覆盖多个指令,这可能会导致问题。
考虑以下包含两个单字节指令的示例:
如果代码想要跳转到 OFFSET+1,它将跳转到 INT3 指令的中间,这可能会生成一个无效操作码异常(#UDINT3 是一个字节,就不会有这个问题了。
INTO⚓︎
-
在对符号操作数进行算术运算时
- 可以通过
JO指令直接测试OF标志,或者 - 使用
INTO指令来处理溢出情况
- 可以通过
-
溢出中断(
INTO)指令检查EFLAGS寄存器中OF标志的状态- 如果
O = 1,则通过向量类型编号 4 生成溢出陷阱 - 如果
O = 0,则INTO不执行任何操作
- 如果
-
使用
INTO指令的好处是,如果检测到溢出异常,可以自动调用异常处理程序来处理溢出情况
Machine Control and Miscellaneous Instructions⚓︎
Controlling the Carry Flag Bit⚓︎
- 进位标志(
C)在多字 / 双字加法和减法中传播进位或借位,并且可以指示汇编语言程序中的错误 - 控制进位标志的内容的指令:
STC(设置进位)CLC(清除进位)CMC(取反 (complement) 进位)
HLT⚓︎
HLT停止指令执行并将处理器置于 HALT 状态-
退出暂停的几种方式:
- 一个启用的中断(
NMI和SMI) - 一个调试异常
- 硬件重置(
BINIT#、INIT#或RESET#信号)
- 一个启用的中断(
-
保存的指令指针(
CS:EIP)指向HLT指令之后的指令
NOP⚓︎
- 在早年,软件开发工具尚未出现时
, NOP(无操作指令)常用于为未来的机器语言指令留出空间 - 当微处理器遇到
NOP时,它会花费短暂的时间来执行 - 多字节
NOP指令可以用作填充,以将函数或循环对齐到 16 或 32 字节边界(比如NOP EAX) -
推荐的多字节
NOP有(来自 Intel 优化参考手册) :长度 汇编指令 字节序列 2 字节 66 NOP66 90H3 字节 NOP DWORD ptr [EAX]0F 1F 00H4 字节 NOP DWORD ptr [EAX + 00H]0F 1F 40 00H5 字节 NOP DWORD ptr [EAX + EAX*1 + 00H]0F 1F 44 00 00H6 字节 66 NOP DWORD ptr [EAX + EAX*1 + 00H]66 0F 1F 44 00 00H7 字节 NOP DWORD ptr [EAX + 00000000H]0F 1F 80 00 00 00 00H8 字节 NOP DWORD ptr [EAX + EAX*1 + 00000000H]0F 1F 84 00 00 00 00 00H9 字节 66 NOP DWORD ptr [EAX + EAX*1 + 00000000H]66 0F 1F 84 00 00 00 00 00H
LOCK Prefix⚓︎
-
Intel486 CPU 保证基本内存操作以原子方式处理:
- 读取 / 写入一个字节
- 读取 / 写入一个在 16 位边界对齐的单字
- 读取 / 写入一个在 32 位边界对齐的双字
-
LOCK前缀确保某些类型的内存读取 - 修改 - 写入操作以原子方式执行- 读取 - 修改 - 写入操作指的是读取、修改并将值写回同一内存位置
- 实现技术:
- 原子指令(例如,CMPXCHG)
- LOCK 前缀
-
LOCK 前缀旨在为处理器在多处理器系统中提供对共享内存的独占使用,它只能与以下写入内存操作数的指令一起使用:
ADC、ADD、AND、DEC、INC、NEG、NOT、OR、SBB、SUB、XORCMPXCHG、CMPXCHG8B、CMPXCHG16B、XADD、XCHG–BTC、BTR、BTS
-
LOCK 前缀仅在某些修改内存的指令中被允许。
- 如果出现以下情况之一,那么会发生未定义操作码异常(undefined opcode operations)(
#UD)- 如果 LOCK 前缀与无算术或逻辑指令一起使用
- 如果目标操作数不是内存操作数
- 如果源操作数是内存操作数
例子
BOUND⚓︎
BOUND 指定第一个操作数(数组索引)是否在第二个操作数(边界操作数)指定的数组范围内
- 语法
BOUND REG, MEM - 边界操作数(
MEM)是两个单字或一个双字的内存位置 - 数组索引(
REG)是一个 16 位或 32 位寄存器 - 如果索引(
REG)不在边界内(MEM) ,则会发出BOUND范围超出异常(#BR,向量类型编号 5) - 如果在范围内,则继续执行下一条指令
- 该指令在 64 位模式下无效
例子
ENTER and LEAVE⚓︎
ENTER和LEAVE指令为被调用的过程创建和释放栈帧- 栈帧(stack frame)(也称为激活帧 (activation frame) 或激活记录 (activation record))是一种内存管理技术,用于支持过程的执行
- 栈帧帮助编程语言支持子程序的递归功能
- 栈帧包含局部变量和其调用者传递的参数
-
一个栈帧由以下部分组成:
- 参数
- 返回地址
- 前一个栈帧指针(显示(display))
- 局部变量
- 被调用程序(被调用者)修改的需要恢复的寄存器保存副本
-
栈帧仅在运行时进程(runtime process) 中存在;当程序执行完成后,相关的栈帧会从栈中消除
- 栈帧还为被调用程序提供了访问嵌套栈帧(nested stack frames) 中其他变量的接入点
EBP(基指针(base pointer))在管理函数调用期间的栈帧中发挥着关键作用:EBP充当函数栈帧内的稳定参考点(stable reference point),用于访问局部变量和函数参数EBP的值在整个函数执行过程中保持不变,而ESP(栈指针)可能会改变- 在设置好栈帧后,函数参数通过
EBP的正偏移量进行访问(比如第一个参数为EBP + 8) ,而局部变量则通过负偏移量进行访问(比如EBP - 4) ,这种一致的访问方式使编译和调试过程更加简单 - 在函数开始时,调用约定(calling convention) 通常通过将当前的
EBP压入栈中来保存它,然后将EBP设置为ESP,建立了一个新的栈帧;在函数结束时,EBP被恢复以释放栈帧并返回到调用者的帧 - 在递归调用(recursive calls) 或调试时,保留的
EBP使得函数能够跟踪和维护一系列栈帧,从而允许调用栈向上遍历回主函数 - 简而言之,
EBP提供了稳定性和对函数栈帧的便捷访问,有助于参数和局部变量管理,并支持调试和递归
例子
void test(int x) {
int a = x; // local variable 1
int b = 10; // local variable 2
int c = 20; // local variable 3
}
经 x86-64 clang 19.1.0 –O0 –m32 编译后,得到以下汇编代码:
push ebp ; save the caller's EBP
mov ebp, esp ; set EBP as the base pointer
sub esp, 12 ; allocate 12 bytes for local variables
mov eax, [ebp+8] ; access the function parameter 'x'
; access local variables:
mov [ebp - 4], eax ; assign x to 'a'
mov [ebp - 8], 10 ; assign 10 to 'b'
mov [ebp - 12], 20 ; assign 20 to 'c'
add esp, 12 ; release stack frame
pop ebp
ENTER和LEAVE指令创建和释放调用过程的栈帧- 语法:
ENTER stack space, nesting level- 第一个操作数指定栈帧中动态存储的大小
- 第二个操作数指定嵌套级别(0 到 31,该值会自动掩码为 5 位)
- 嵌套级别决定了在调整栈指针之前,从前一个帧复制到新栈帧的栈帧指针数量,这允许从被调用函数访问多个父帧
-
嵌套函数不被标准 C 支持,局部变量仅在其声明的函数范围内有效
-
ENTER支持块结构化语言(block structured language) 的支持,如使用嵌套函数的 Pascal:-
以下代码创建了一个栈帧链(作用域链(scope chain)
) ,以便给过程 Z 访问 Y 和 X 的局部变量
-
-
以下嵌套函数的栈帧:
- 对于嵌套级别为 1 或更高的情况,
ENTER在调整栈指针之前会复制早期的栈帧指针 - 用于访问先前函数变量的堆栈帧指针集合称为显示(display)
- 对于嵌套级别为 1 或更高的情况,
-
以下伪代码展示
ENTER指令的定义: -
x86 最初是作为一种 Pascal 机器设计的,这就是为什么有特殊指令来支持其特性的原因:
- 使用
enter和leave的嵌套函数 - Pascal 调用约定:被调用者使用
ret N从栈中弹出参数数量 - 使用
bound(ISO 7185 Pascal)进行边界检查(bounding check)
- 使用
评论区







































