跳转与控制流转移
在 Intel x86/x64 架构中,跳转指令的本质是修改指令指针寄存器(IP/EIP/RIP),有时同时修改代码段寄存器(CS),从而打破程序的顺序执行,实现分支、循环和子程序调用。
一、 标志寄存器 EFLAGS/RFLAGS
绝大多数条件跳转指令的执行依据是标志寄存器中的状态位。通常由 CMP(减法比较)或 TEST(逻辑与测试)指令隐式设置。
-
ZF (Zero Flag - 零标志): 运算结果为 0 时置 1。常用于判断“相等”。
-
CF (Carry Flag - 进位/借位标志): 无符号运算发生最高位进位或借位时置 1。常用于无符号数大小比较。
-
SF (Sign Flag - 符号标志): 运算结果的最高位(符号位)为 1(即负数)时置 1。
-
OF (Overflow Flag - 溢出标志): 有符号运算结果超出可表示范围时置 1。常与 SF 结合判断有符号数大小。
-
PF (Parity Flag - 奇偶标志): 结果最低字节中“1”的个数为偶数时置 1。
-
IF (Interrupt Flag - 中断标志): 决定是否响应外部可屏蔽中断。相关指令:
cli(关中断,IF=0),sti(开中断,IF=1)。 -
TF (Trap Flag - 陷阱标志): 用于单步调试。
-
DF (Direction Flag - 方向标志): 控制字符串操作的地址处理方向。相关指令:
cld(使 DF=0,正方向地址递增),std(使 DF=1,反方向地址递减)。 -
OF 与跳转条件:
-
OF 的触发本质: 当两个正数相加变成负数,或者两个负数相加变成正数时,
OF=1。 -
jl(符号数小于则跳) 的逻辑: 条件为SF != OF(异号)。注意:jl的跳转与ZF(零标志)无关,即使ZF=1(两数相等),只要SF != OF,jl依然会发生跳转。 -
jb(非符号数小于则跳) 的逻辑: 条件为CF = 1。因此,指令jb与jc(Jump if Carry) 是完全等价的。
-
二、 无条件跳转指令:JMP
JMP 指令强制 CPU 转移到目标地址执行,不检查任何条件。
1. 按转移距离分类 (基于指令编码)
在机器码层面,跳转通常记录的是相对当前 IP 的偏移量,而不是绝对物理地址。
-
短跳转 (Short Jump):
JMP SHORT label-
机器码特征:以
EB开头,后跟 1 字节偏移量。 -
偏移量计算公式:
目标地址 - 下条指令的偏移地址。 -
跳转范围:当前 IP 前后 -128 到 +127 字节。
-
-
近跳转 (Near Jump):
JMP NEAR PTR label(默认缺省值)-
机器码特征:以
E9开头,后跟 2 字节(16位)或 4 字节(32位)的小端格式偏移量。 -
段内转移(不修改 CS),跳转范围覆盖整个当前代码段。
-
-
远跳转 (Far Jump):
JMP FAR PTR label-
机器码特征:以
EA开头。 -
段间转移(同时修改 CS 和 IP)。指令中直接硬编码目标地址的段基址和偏移量(小端格式,例如
EA 00 30 00 20代表跳往2000:3000)。
-
-
直接写
jmp label, 汇编时会自动计算选择合适的指令。
2. 按寻址方式分类
-
直接转移: 目标地址直接写在指令中(标号)。
JMP L1(编译后为相对偏移量)
-
间接转移: 目标地址存放在寄存器或内存中。
-
JMP EAX(跳到 EAX 存放的地址处,绝对地址) -
JMP DWORD PTR [EBX](跳到内存地址 EBX 所指向的 32 位绝对地址处)
-
三、 条件跳转指令:Jcc (Jump if Condition Is Met)
条件跳转仅支持短跳转和近跳转(段内转移),不支持远跳转。它们通常成对出现,拥有多个助记符,但底层机器码可能相同。
1. 状态标志位直接测试
| 指令助记符 | 含义 | 触发条件 (标志位) |
|---|---|---|
| JZ / JE | 结果为零则跳转 / 相等则跳转 | ZF = 1 |
| JNZ / JNE | 结果不为零则跳转 / 不等则跳转 | ZF = 0 |
| JC / JNC | 有进位则跳转 / 无进位则跳转 | CF = 1 / CF = 0 |
| JS / JNS | 结果为负则跳转 / 结果为正(含0)则跳转 | SF = 1 / SF = 0 |
| JO / JNO | 发生溢出则跳转 / 未溢出则跳转 | OF = 1 / OF = 0 |
| JP(JPE) / JNP(JPO) | 偶校验则跳转 / 奇校验则跳转 | PF = 1 / PF = 0 |
2. 无符号数比较 Unsigned Comparison
无符号数用 A (Above,高于) 和 B (Below,低于)。
前提:执行 CMP dest, src 之后
| 指令 | 含义 | 触发条件 |
|---|---|---|
| JA / JNBE | 大于则跳转 (dest > src) | CF = 0 且 ZF = 0 |
| JAE / JNB | 大于或等于则跳转 (dest >= src) | CF = 0 |
| JB / JNAE / JC | 小于则跳转 (dest < src) | CF = 1 |
| JBE / JNA | 小于或等于则跳转 (dest <= src) | CF = 1 或 ZF = 1 |
3. 有符号数比较 Signed Comparison
有符号数用 G (Greater,大于) 和 L (Less,小于)。
前提:执行 CMP dest, src 之后。判断逻辑依赖于符号标志(SF)和溢出标志(OF)的异或关系。
| 指令 | 含义 | 触发条件 |
|---|---|---|
| JG / JNLE | 大于则跳转 (dest > src) | ZF = 0 且 SF = OF |
| JGE / JNL | 大于或等于则跳转 (dest >= src) | SF = OF |
| JL / JNGE | 小于则跳转 (dest < src) | SF != OF (异号) |
| JLE / JNG | 小于或等于则跳转 (dest <= src) | ZF = 1 或 SF != OF |
4. 计数寄存器特定跳转
不需要 CMP 或 TEST 影响标志位,直接检查特定的计数寄存器。
JCXZ/JECXZ/JRCXZ: 如果 CX / ECX / RCX 寄存器的值为 0,则执行短跳转。常用于在进入循环前进行安全检查,防止执行 \(2^{16}\) 或 \(2^{32}\) 次的死循环。
四、 循环控制指令:LOOP
循环指令是条件跳转的一种封装,专门用于控制循环次数。它隐式地使用 CX/ECX/RCX 作为循环计数器。
-
LOOP label-
执行逻辑: 先将计数器 (CX/ECX/RCX) 减 1。如果不为 0,则跳转到 label 继续循环;如果为 0,则顺序执行下一条指令。
-
注意1:不影响任何标志位。
-
注意2:当
CX/ECX的初始值为 0 时,执行LOOP指令会首先进行减 1 操作(0 减 1 变成 0FFFFh 或 0FFFFFFFFh),这会导致循环执行 \(10000h\) (65536) 次,并非 0 次。因此,在进入循环前常用JCXZ进行拦截预防。
-
-
LOOPE / LOOPZ label(相等/为零时循环)-
执行逻辑: 计数器减 1。如果计数器不为 0 且 ZF = 1,则跳转到 label。
-
常用于数组中查找第一个非零元素。
-
-
LOOPNE / LOOPNZ label(不等/不为零时循环)-
执行逻辑: 计数器减 1。如果计数器不为 0 且 ZF = 0,则跳转到 label。
-
常用于数组中寻找特定的值(如字符串结束符)。
-
五、 子程序调用与返回:CALL 与 RET
虽然归类为调用指令,但本质上也是控制流转移。与 JMP 的核心区别在于:CALL 会在跳转前将返回地址压入堆栈,而 RET 会从堆栈弹出地址并跳转回原处。
1. CALL 指令
-
近调用 (Near Call):
CALL label-
仅将当前
IP/EIP(即 CALL 的下一条指令的地址)压入堆栈。 -
然后将 IP 修改为目标地址(段内转移)。
-
-
远调用 (Far Call):
CALL FAR PTR label-
跨段调用。先将当前的
CS压栈,再将IP/EIP压栈。 -
更新 CS 和 IP 为目标绝对地址。
-
2. RET 指令
-
近返回 (RET / RETN): 从栈顶弹出一个值给 IP/EIP,继续执行。
-
远返回 (RETF): 从栈顶先弹出 IP/EIP,再弹出 CS。
-
带参数返回 (RET n / RETF n): 弹出返回地址后,再将堆栈指针(SP/ESP)增加
n个字节。常用于平栈(清理压入堆栈的函数参数,如stdcall调用约定)。
六、 补充细节与现代架构注意事项
-
性能与分支预测 (Branch Prediction): 现代 CPU 高度依赖流水线。遇到条件跳转时,CPU 会预测跳转是否发生并提前执行。如果预测失败,流水线会被清空,导致极大的性能惩罚。在性能关键代码中,常通过无分支编程(如
CMOVcc条件传送指令)来替代Jcc跳转指令。 -
64 位模式 (x64):
-
段寄存器(CS, DS 等)在纯 64 位用户态代码中基本不再起寻址作用(平坦内存模型)。远跳转(Far Jump)和远调用(Far Call)在现代应用层编程中极为罕见,主要限于操作系统内核态的上下文切换。
-
x64 强制使用基于 RIP 的相对寻址,这使得生成位置无关代码(PIC)更加容易。
-
运算与逻辑处理
在 Intel 汇编中,所有的数学运算和逻辑处理最终都由 CPU 的算术逻辑单元(ALU)执行。这些指令不仅修改目标操作数的值,绝大多数还会隐式更新标志寄存器(EFLAGS/RFLAGS),为后续的条件跳转提供依据。
一、 基本算术运算指令 (加、减、求补)
这类指令处理有符号和无符号数的加减法。对于 CPU 而言,底层的加减法采用 补码(Two's Complement) 机制,因此同一种加减指令可以同时处理有符号和无符号数。
| 指令 | 语法 | 功能描述 | 标志位影响 | 细节与场景 |
|---|---|---|---|---|
ADD |
ADD dest, src |
加法 (dest = dest + src) |
影响所有状态标志 | 基础加法。 |
ADC |
ADC dest, src |
带进位加法 (dest = dest + src + CF) |
影响所有状态标志 | 用于多精度(大数)加法,将低位运算产生的进位(CF)累加到高位。 加法 ( 18000h + 28001h): 先算低 16 位 ADD AX, 8001h (产生进位 CF=1),再算高 16 位 ADC DX, 2 (DX = 1 + 2 + CF = 4)。 |
INC |
INC dest |
自增 (dest = dest + 1) |
影响除 CF 外的所有标志 | 注意:INC 不会修改进位标志 CF。常用于循环计数。 |
SUB |
SUB dest, src |
减法 (dest = dest - src) |
影响所有状态标志 | 基础减法。 |
SBB |
SBB dest, src |
带借位减法 (dest = dest - src - CF) |
影响所有状态标志 | 用于多精度减法,将低位运算产生的借位(CF)从高位扣除。 减法 ( 38000h - 18001h): 先算低 16 位 SUB AX, 8001h (借位 CF=1),再算高 16 位 SBB DX, 1 (DX = 3 - 1 - CF = 1)。 |
DEC |
DEC dest |
自减 (dest = dest - 1) |
影响除 CF 外的所有标志 | 同 INC,不影响 CF。 |
NEG |
NEG dest |
求补/取负 (dest = 0 - dest) |
影响所有状态标志 | 执行二进制补码求负(按位取反后加 1)。只有当操作数为 0 时,CF 才被清零,否则 CF 置 1。 |
二、 乘除法指令 (特殊寄存器绑定)
由于乘法会使结果位宽翻倍,除法需要提供高低两部分的被除数,Intel 的基本乘除法指令在设计上强绑定了特定的累加器(AX/EAX/RAX)和数据寄存器(DX/EDX/RDX)。
1. 乘法指令 (MUL / IMUL)
-
MUL src(无符号乘法):-
隐式操作数:被乘数永远是
AL/AX/EAX/RAX。 -
执行逻辑:
累加器 * src。 -
结果存放:
-
8位:
AL * src(8位) = AX -
16位:
AX * src(16位) = DX:AX -
32位:
EAX * src(32位) = EDX:EAX -
64位:
RAX * src(64位) = RDX:RAX
-
-
-
IMUL(有符号乘法):-
单操作数形式:
IMUL src,逻辑与存放位置同MUL,但按有符号数处理。 -
现代多操作数扩展: 现代编译器(8086 不适用)极少使用单操作数
IMUL。x86 扩展了双操作数和三操作数形式的IMUL,不强绑定 DX,且结果如果截断不会影响其他寄存器(MUL 会覆盖 DX/EDX/RDX)。-
IMUL reg, src(reg = reg * src) -
IMUL reg, src, imm(reg = src * 立即数)
-
-
2. 除法指令 (DIV / IDIV)
-
DIV src(无符号除法):-
被除数必须是除数位宽的两倍,提前存放在隐式寄存器中。(注意清除高位寄存器)
-
执行逻辑:
-
除数为 8 位:
AX / src\(\rightarrow\) 商存AL,余数存AH。 -
除数为 16 位:
DX:AX / src\(\rightarrow\) 商存AX,余数存DX。 -
除数为 32 位:
EDX:EAX / src\(\rightarrow\) 商存EAX,余数存EDX。
-
-
-
IDIV src(有符号除法) 与 符号扩展指令:-
逻辑同
DIV。但在执行有符号除法前,必须将被除数的高半部分(如 DX/EDX)进行正确的符号扩展,否则会导致严重的除法异常。 -
符号扩展专用指令(无操作数):
-
CBW: 将AL符号扩展为AX(Convert Byte to Word)。 -
CWD: 将AX符号扩展为DX:AX。 -
CDQ: 将EAX符号扩展为EDX:EAX。 -
CQO: 将RAX符号扩展为RDX:RAX。
-
-
三、 逻辑位运算指令
用于按位操作,速度极快。此类指令通常会清除进位标志 (CF) 和溢出标志 (OF),并根据结果更新 ZF, SF, PF。
| 指令 | 语法 | 功能与位级表现 | 典型应用场景 |
|---|---|---|---|
AND |
AND dest, src |
对应位都为 1 时结果为 1,否则为 0。 | 位掩码(Masking): 将某些位清零,保留其他位。例如 AND AL, 0Fh 可保留低 4 位,清空高 4 位。 |
OR |
OR dest, src |
对应位只要有 1 结果就为 1。 | 置位(Setting): 将某些位强制设为 1。 |
XOR |
XOR dest, src |
对应位不同为 1,相同为 0。 | 1. 翻转特定位。 2. 清零寄存器: XOR EAX, EAX 是将寄存器清零最快、机器码最短的方法。 |
NOT |
NOT dest |
所有位取反(0变1,1变0)。 | 反码操作。 注意:NOT 指令不影响任何标志位。 |
TEST |
TEST dest, src |
执行 AND 运算,但丢弃结果,只设置标志位。 |
1. 检查某一位是否为 1: TEST EAX, 1,若最低位为 0 则 ZF=1。2. 判空: TEST EAX, EAX,若 EAX 为 0 则 ZF=1。 |
四、 移位与循环移位指令 (Shifts and Rotates)
移位操作是底层的乘除法平替方法(对 2 的幂次进行乘除)。移位的位数(Count)可以直接作为立即数给出(通常限制在 1 字节内),或者存放在 CL 寄存器中(必须是 CL,不能是 CX/ECX)。
1. 移位指令 (Shift)
移位会将移出的最后一位送入进位标志 (CF)。
-
SHL dest, count(逻辑左移) /SAL dest, count(算术左移):-
两者的机器码和行为完全相同。
-
行为:所有位向左移,右侧空出的位全部补 0。最高位移入 CF。
-
数学意义:相当于乘以 2 的 count 次方。
-
-
SHR dest, count(逻辑右移):-
行为:所有位向右移,左侧(最高位)空出的位全部补 0。最低位移入 CF。
-
数学意义:用于无符号数除以 2 的 count 次方。
-
-
SAR dest, count(算术右移):-
行为:所有位向右移,左侧空出的位用原符号位(最高位)填充。最低位移入 CF。
-
数学意义:用于有符号数除以 2 的 count 次方(保持负数依然是负数)。
-
与 C 语言的映射:
-
SAR针对有符号数:对应 C 语言中有符号变量的右移(如char a = 0xFE; a >>= 1;结果为0xFF / -1)。 -
SHR针对无符号数:对应 C 语言中无符号变量的右移(如unsigned char b = 0xFE; b >>= 1;结果为0x7F / 127)。
2. 循环移位指令 (Rotate)
像履带一样循环,移出的位会被塞回另一端。
-
ROL dest, count(循环左移):- 最高位被移出后,同时填入最低位和 CF 标志位。
-
ROR dest, count(循环右移):- 最低位被移出后,同时填入最高位和 CF 标志位。
-
RCL / RCR(带进位循环移位):- 将 CF 标志位作为数据的一部分,与目标操作数拼接成一个更大的环进行循环移位。常用于多精度数据的级联移位。(相当于将 CF 作为数据的最高位延伸)
五、 性能优化
-
位运算替代算术运算: 现代编译器会自动将乘以/除以 \(2^n\) 的操作优化为
SHL/SAR,将求余运算% 2^n优化为AND dest, (2^n - 1),因为移位和按位与只需 1 个时钟周期,远快于MUL/DIV。 -
LEA (Load Effective Address - 加载有效地址):
- **核心功能:** 用于取某个变量或内存表达式的**偏移地址**,而不解引用内存。例如 `LEA DI, DS:[BX+SI+4]` 执行后,单纯将算术结果 `BX+SI+4` 存入 `DI`。 - 编译器常利用其不需要算术单元(不影响标志位)的特性,通过如 `LEA EAX, [EBX + ECX*4 + 10]` 这样一条指令替代多条 `ADD/MUL` 指令进行复杂的指针或数值加法。 -
零扩展代替符号扩展: 在处理无符号数除法前,不需要
CDQ,只需使用XOR EDX, EDX将高位寄存器清零即可。
内存操作
一、 语法指示符 Size Directives
当指令中缺少寄存器来暗示数据大小时(例如将立即数写入内存),必须通过大小指示符 (PTR) 明确告诉汇编器要操作的内存字节数,否则将引发编译错误。
-
BYTE PTR: 8 位(1 字节) -
WORD PTR: 16 位(2 字节) -
DWORD PTR: 32 位(4 字节) -
QWORD PTR: 64 位(8 字节)
语法示例:
MOV BYTE PTR [EBX], 0xFF ; 仅向 EBX 指向的地址写入 1 个字节
MOV DWORD PTR [EBX], 0xFF ; 向 EBX 指向的地址写入 4 个字节 (0x000000FF)
INC QWORD PTR [RAX+8] ; 将 RAX+8 指向的 64 位内存数据加 1
二、 基础数据读写指令
1. MOV (标准移动)
-
语法:
MOV dest, src -
规则:
-
禁止内存到内存的直接
MOV。MOV [EAX], [EBX]是非法的,必须借助寄存器中转。 -
源和目标操作数的位宽必须完全一致。
-
不能将立即数直接写入段寄存器(必须通过通用寄存器中转)。
-
2. MOVSX / MOVZX (带扩展的读取)
当从内存读取较小的数据到较大的寄存器时使用。
-
MOVSX reg, mem(符号扩展): 将内存数据读入寄存器,并根据最高位(符号位)填充寄存器剩余的高位。常用于读取有符号数。 -
MOVZX reg, mem(零扩展): 将内存数据读入寄存器,高位全部无条件补零。常用于读取无符号数。
3. XCHG (数据交换) (*)
-
语法:
XCHG reg, mem或XCHG mem, reg -
底层细节: 当操作数包含内存地址时,
XCHG会隐式断言硬件LOCK信号(见第六节),保证整个交换过程的绝对原子性。
三、 堆栈专用内存指令
堆栈指令隐式依赖栈顶指针寄存器 (SP/ESP/RSP)。硬件堆栈始终向下生长(向低地址延伸)。
1. 基础压弹栈 (PUSH / POP) -- 操作数必须是 16 位 或 32 位
-
PUSH mem/reg/imm:-
执行逻辑:
ESP = ESP - size,然后[ESP] = operand。 -
在 32 位模式下,默认压入 4 字节;64 位模式下,默认压入 8 字节。
-
-
POP mem/reg:- 执行逻辑:
operand = [ESP],然后ESP = ESP + size。
- 执行逻辑:
2. 状态寄存器压弹 (PUSHF / POPF )
-
PUSHF/POPF(16位): 将 16 位的FLAGS压入/弹出内存堆栈。 -
PUSHFD/POPFD(32位): 将 32 位的EFLAGS标志寄存器完整压入/弹出内存堆栈。 -
PUSHFQ/POPFQ(64位): 压入/弹出 64 位的RFLAGS。
3. 高级栈帧控制 (ENTER / LEAVE) (*)
-
ENTER imm16, imm8: 建立函数栈帧。等价于PUSH EBP->MOV EBP, ESP->SUB ESP, imm16(分配局部变量空间)。(现代编译体系中基本都是 imm8 = 0,且基本不使用 ENTER,涉及函数内部定义函数等历史问题) -
LEAVE: 销毁函数栈帧。等价于MOV ESP, EBP->POP EBP。现代编译器极度依赖LEAVE,但通常用拆解的指令替代ENTER以提升性能。
四、 块级内存/字符串指令 String Operations
专门用于高速处理连续内存块。该指令族在硬件层面实现了“指针自增/自减”与“循环计数”的解耦,是硬件级内存操作的基石。
1. 核心依赖寄存器与控制
字符串指令隐式依赖一组固定的寄存器组合:
| 寄存器角色 | 现代 x86/x64 规范 (32位 / 64位) | 8086 规范 (16位) | 隐式段绑定限制 (8086) |
|---|---|---|---|
| 源指针 (Source) | ESI / RSI |
SI |
默认绑定到 DS:SI。允许使用段超越前缀(如 CS:, SS:, ES:)重写。 |
| 目的指针 (Dest) | EDI / RDI |
DI |
严格且唯一绑定到 ES:DI。在所有 x86 模式下均不允许任何段超越前缀。 |
| 计数器 (Count) | ECX / RCX |
CX |
隐式循环计数器。 |
| 累加器 (Accumulator) | AL / AX / EAX / RAX |
AL (字节) / AX (字) |
用于暂存、填充、比较或扫描的数值。 |
方向与步长控制
-
DF 标志 (Direction Flag):
-
CLD(Clear Direction):使DF = 0,地址递增(正向处理,从低地址到高地址)。 -
STD(Set Direction):使DF = 1,地址递减(反向处理,常用于处理内存重叠拷贝的memmove场景)。
-
-
步长 (Step): 由指令后缀决定:
-
B(Byte) = 1 字节 -
W(Word) = 2 字节 (8086 架构支持的最大单步上限) -
D(Doubleword) = 4 字节 -
Q(Quadword) = 8 字节 -
自增减逻辑: 每次操作后,硬件会自动对相关指针(
SI/DI等)进行+Step(若DF=0)或-Step(若DF=1)的更新。
-
2. 核心操作指令家族
① MOVS (Move String 复制)
-
核心行为:
[目的地址] = [源地址]。[ES:DI] = [DS:SI][EDI] = [ESI]或[RDI] = [RSI]
-
特性: x86 架构中唯一允许“内存到内存”直接拷贝的指令家族。
-
指针更新: 源指针和目的指针同步自增/自减。
② STOS (Store String 存储/填充)
-
核心行为: 将累加器的值写入目的地址。
[ES:DI] = AL / AX[EDI] = AL / AX / EAX / RAX
-
特性: 常用于高效率的内存块初始化(等价于 C 标准库的
memset)。 -
指针更新: 仅目的指针(
DI系列)自增/自减。
③ LODS (Load String 加载)
-
核心行为: 将源地址的值读入累加器。
AL / AX = [DS:SI]AL / AX / EAX / RAX = [ESI]
-
特性: 用于逐个字符/字读取流数据,以便在寄存器中进行复杂的逻辑处理。
-
指针更新: 仅源指针(
SI系列)自增/自减。
④ CMPS (Compare String 比较内存)
-
核心行为: 隐式执行
[源地址] - [目的地址]。[DS:SI] - [ES:DI][ESI] - [EDI]
-
特性: 不保存减法结果,仅根据结果修改 CPU 的状态标志位(如
ZF,CF,SF,OF)。用于memcmp。 -
指针更新: 源指针和目的指针同步自增/自减。
⑤ SCAS (Scan String 扫描内存)
-
核心行为: 隐式执行
累加器 - [目的地址]。AL / AX - [ES:DI]AL / AX / EAX / RAX - [EDI]
-
特性: 不保存减法结果,仅修改标志位。常用于在连续内存中寻找特定字符/值(如 C 语言中
strlen寻找\0,或strchr寻找特定字符)。 -
指针更新: 仅目的指针(
DI系列)自增/自减。
3. REP 循环前缀 (硬件级批处理加速)
单独的字符串指令只会执行一次。加在字符串指令前的 REP 族前缀,能够利用计数器(CX/ECX/RCX)控制指令在硬件级别自动循环。每次单步指令执行完毕后,计数器会自动减 1,整个循环判定不消耗常规条件跳转(如 LOOP 或 JNZ)的软件时钟周期。
[ 执行串指令 (如 MOVSB) ] -> [ 计数器 (CX/ECX) 自动减 1 ]
|
v
[ 检查终止条件 (如 CX==0 或 ZF 状态) ] -> (不满足) -> 硬件内循环继续
| (满足)
v
[ 退出串循环 ]
-
REP(Repeat):-
终止条件: 计数器
= 0。 -
典型搭配:
REP MOVSB/REP STOSB。
-
-
REPE / REPZ(Repeat while Equal / Zero):-
终止条件: 计数器
= 0或ZF = 0(即一旦发现数据不相等/不为零,立刻强行终止)。 -
典型搭配:
REPE CMPSB(检查两块内存是否完全一致,若遇到不同字节,ZF被清零,循环在中途停止)。
-
-
REPNE / REPNZ(Repeat while Not Equal / Not Zero):-
终止条件: 计数器
= 0或ZF = 1(即一旦搜寻到目标相等的值,立刻强行终止)。 -
典型搭配:
REPNE SCASB(在内存块中搜索特定字符,一旦找到,ZF被置 1,循环停止)。
-
4. 与 C 标准库的等价映射
① 内存拷贝 memcpy
-
汇编实现 (8086):
② 字符串长度 strlen 深度解析
在 C 语言中,strlen 是通过查找末尾的 \0 (0x00) 来计算长度的。
-
汇编实现 (8086):
CLD ; 地址递增 MOV DI, OFFSET str; ES:DI 指向待测字符串首地址 MOV AL, 0 ; 目标搜索字符为 '\0' MOV CX, 0FFFFh ; 将 CX 设为最大计数 (16位下为 65535) REPNE SCASB ; 只要不是 '\0' 且 CX!=0 就一直往后扫 ; 【推导】 ; 此时,DI 停留在 '\0' 后面的那一个字节。 ; 经历的循环次数 = 0FFFFh - 最终的 CX。 ; 因为最后一次匹配到 '\0' 时,CX 也减了 1,所以: ; 包含 '\0' 的字符串总长度 = 0FFFFh - 最终的 CX ; 不含 '\0' 的纯文本长度 = 0FFFFh - 最终的 CX - 1 NOT CX ; 在补码运算中,NOT CX 等价于 (0FFFFh - CX) DEC CX ; 再减 1,此时 CX 寄存器中的值就是精准的字符串长度!
5. 调试器行为与底层中断机理
在利用调试器(如 DOSBox 下的 Debug.exe、现代的 OllyDbg / x64dbg / IDA)对这些指令进行逆向或分析时:
-
常规表现:
-
F8 (Step Over / 步过): 调试器会把
REP MOVSB这一整行批处理当成一个单条高级函数,直接在底层一口气执行完CX次循环,然后停在下一行指令。 -
F7 (Trace Into / 单步步入): 能够观察到内部执行细节。每按一次 F7,你会看到
IP指针死死停在当前指令不动,但是CX在递减,SI/DI在不断自增或自减。
-
-
底层硬件机理 (8086):
(*)为什么
REP指令在循环时IP(指令指针)不往前走,甚至中断发生后还能正确返回?-
指令重入设计: 当
REP MOVSB执行时,CPU 在每一次微操作循环(单步拷贝 +CX-1+SI/DI修正)完成后,都会重新评估CX的状态。 -
中断响应窗口: 如果
CX != 0,CPU 会把当前控制流的IP强行保持复位在当前REP指令的起始地址上。这意味着,如果在执行一个耗时极长的REP串操作(例如拷贝 64KB 数据)期间突然触发了硬件外部中断(如时钟中断、键盘输入),CPU 会允许在单步完成的间隙立刻响应中断。 -
中断保护: 此时压入堆栈保护的
IP依然指向这条REP指令。当中断处理程序执行完IRET返回时,程序将无缝回到REP指令继续未完的CX次循环。这也是为什么使用 F7 调试时,你其实是在驱动 CPU 内部的这种微步迭代。
-
五、 原子性内存指令 Atomic Operations (*)
在多核处理器(SMP)时代,用于防止多线程同时读写同一内存地址导致数据竞争。
1. LOCK 前缀
-
语法:
LOCK 指令 -
机制: 锁住系统总线(或独占 Cache 行),确保其后跟随的指令在执行期间,其他 CPU 核心绝对无法访问该内存地址。
-
合法指令: 只能加在修改内存的指令前,如
ADD, SUB, AND, OR, XOR, XCHG, XADD, CMPXCHG。
2. 核心并发指令
-
XADD dest_mem, src_reg(交换并相加):-
临时保存
dest_mem和src_reg的值。 -
将两者相加的结果写入
dest_mem。 -
将
dest_mem原本的值写入src_reg。(常配合
LOCK用于实现线程安全的全局计数器 fetch_and_add)
-
-
CMPXCHG dest_mem, src_reg(比较并交换 - CAS 原语):-
比较
AL/AX/EAX/RAX(隐式操作数)与dest_mem。 -
如果相等:说明内存未被篡改,将
src_reg写入dest_mem,置ZF=1。 -
如果不等:将
dest_mem的当前值读入AL/AX/EAX/RAX,置ZF=0。(所有现代操作系统的无锁队列、互斥锁的基础)
-
六、 特殊与缓存控制指令
1. XLAT / XLATB (查表指令)
-
核心机制
隐式依赖基址寄存器(存放查找表的首地址)和
AL寄存器(存放表内下游的单字节无符号索引/下标)。CPU 执行该指令后,会自动将表中对应位置的字节内容加载回AL中,从而实现“输入下标 \(\rightarrow\) 输出内容”的硬件级快速映射。\[ \text{执行前: } AL = \text{下标} \quad \xrightarrow{\text{XLAT}} \quad \text{执行后: } AL = \text{表内对应的单字节数据} \] -
**运行模式与架构的差异
指令会根据处理器当前的运行模式绑定不同的基址寄存器:
| 运行模式 / 段属性 | 默认地址尺寸 | 隐式绑定的基址寄存器 | 唯一确切的物理/线性地址计算公式 |
|---|---|---|---|
16位模式(实模式 / USE16) |
16 位 (Word) | BX |
AL = DS:[BX + ZeroExtend(AL)] |
32位模式(保护模式 / USE32) |
32 位 (Dword) | EBX |
AL = DS:[EBX + ZeroExtend(AL)] |
| 64位模式(长模式 / x64) | 64 位 (Qword) | RBX |
AL = [RBX + ZeroExtend(AL)] (通常忽略段基址) |
2. 内存屏障 (Memory Fences) (*)
由于现代 CPU 具有乱序执行(Out-of-Order Execution)特性,这会导致多线程环境下的内存访问顺序不可预期。
-
SFENCE(Store Fence): 保证此指令之前的写(Store)操作必定先于此指令之后的写操作落入内存。 -
LFENCE(Load Fence): 保证读(Load)操作的顺序。 -
MFENCE(Memory Fence): 读写双向全屏障。
3. PREFETCH (数据预取) (*)
-
语法:
PREFETCHT0 [mem] -
机制: 这是一个性能提示指令(Hint)。它告诉 CPU:“我稍后会用到这块内存,请提前把它从较慢的主存加载到较快的 CPU 高速缓存 (L1/L2 Cache) 中”。不影响程序逻辑,只影响性能。