Skip to content

跳转与控制流转移

在 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 调用约定)。


六、 补充细节与现代架构注意事项

  1. 性能与分支预测 (Branch Prediction): 现代 CPU 高度依赖流水线。遇到条件跳转时,CPU 会预测跳转是否发生并提前执行。如果预测失败,流水线会被清空,导致极大的性能惩罚。在性能关键代码中,常通过无分支编程(如 CMOVcc 条件传送指令)来替代 Jcc 跳转指令。

  2. 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 作为数据的最高位延伸)

五、 性能优化

  1. 位运算替代算术运算: 现代编译器会自动将乘以/除以 \(2^n\) 的操作优化为 SHL / SAR,将求余运算 % 2^n 优化为 AND dest, (2^n - 1),因为移位和按位与只需 1 个时钟周期,远快于 MUL/DIV。

  2. LEA (Load Effective Address - 加载有效地址):

    - **核心功能:** 用于取某个变量或内存表达式的**偏移地址**,而不解引用内存。例如 `LEA DI, DS:[BX+SI+4]` 执行后,单纯将算术结果 `BX+SI+4` 存入 `DI`。
    
    - 编译器常利用其不需要算术单元(不影响标志位)的特性,通过如 `LEA EAX, [EBX + ECX*4 + 10]` 这样一条指令替代多条 `ADD/MUL` 指令进行复杂的指针或数值加法。
    
  3. 零扩展代替符号扩展: 在处理无符号数除法前,不需要 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

  • 规则:

    1. 禁止内存到内存的直接 MOV。MOV [EAX], [EBX] 是非法的,必须借助寄存器中转。

    2. 源和目标操作数的位宽必须完全一致。

    3. 不能将立即数直接写入段寄存器(必须通过通用寄存器中转)。

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):

    CLD               ; DF=0, 正向拷贝
    MOV CX, count     ; 拷贝字节数
    ; (确保 DS:SI 指向源首地址,ES:DI 指向目的首地址)
    REP MOVSB         ; 硬件加速拷贝
    

② 字符串长度 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(指令指针)不往前走,甚至中断发生后还能正确返回?

    1. 指令重入设计: 当 REP MOVSB 执行时,CPU 在每一次微操作循环(单步拷贝 + CX-1 + SI/DI修正)完成后,都会重新评估 CX 的状态。

    2. 中断响应窗口: 如果 CX != 0,CPU 会把当前控制流的 IP 强行保持复位在当前 REP 指令的起始地址上。这意味着,如果在执行一个耗时极长的 REP 串操作(例如拷贝 64KB 数据)期间突然触发了硬件外部中断(如时钟中断、键盘输入),CPU 会允许在单步完成的间隙立刻响应中断。

    3. 中断保护: 此时压入堆栈保护的 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 (交换并相加):

    1. 临时保存 dest_mem 和 src_reg 的值。

    2. 将两者相加的结果写入 dest_mem。

    3. 将 dest_mem 原本的值写入 src_reg。

      (常配合 LOCK 用于实现线程安全的全局计数器 fetch_and_add)

  • CMPXCHG dest_mem, src_reg (比较并交换 - CAS 原语):

    1. 比较 AL/AX/EAX/RAX(隐式操作数)与 dest_mem。

    2. 如果相等:说明内存未被篡改,将 src_reg 写入 dest_mem,置 ZF=1。

    3. 如果不等:将 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) 中”。不影响程序逻辑,只影响性能。