跳转到内容

x86 汇编/控制流

来自维基教科书,自由的教科书

几乎所有编程语言都有改变语句执行顺序的能力,汇编也不例外。指令指针 (EIP) 寄存器包含要执行的下一条指令的地址。为了改变控制流,程序员必须能够修改 EIP 的值。这就是控制流函数发挥作用的地方。

mov eip, label   ; wrong
jmp label        ; right


比较指令

[编辑 | 编辑源代码]
test 引用,累加器 GAS 语法
test 累加器,引用 Intel 语法


累加器引用 进行按位逻辑 and 操作,我们将结果称为 commonBits,并根据 commonBits 设置 ZF(零)、SF(符号)和 PF(奇偶校验)标志。然后丢弃 CommonBits


操作数

引用

  • 寄存器
  • 立即数

累加器

  • 寄存器(如果 引用 是立即数,则可以对 AL/AX/EAX 进行特殊编码)
  • 内存

修改后的标志

  • SF ≔ MostSignificantBit(commonBits)
  • ZF ≔ (commonBits = 0),因此设置 ZF 意味着 累加器引用 没有共同的设置位
  • PF ≔ BitWiseXorNor(commonBits[Max-1:0]),因此当且仅当 commonBits[Max-1:0] 具有偶数个 1 位时,PF 设置为 1
  • CF ≔ 0
  • OF ≔ 0
  • AF 未定义

应用

  • 将同一个寄存器传递两次:test rax, rax
    • SF 成为 rax符号,这是一个简单的非负性测试
    • 如果 rax,则设置 ZF
    • 如果 rax 具有偶数个设置位,则设置 PF


cmp 被减数,减数 GAS 语法
cmp 减数,被减数 Intel 语法


被减数减数 之间执行比较操作。比较操作通过从 被减数 中减去 减数(称为 )来执行。然后丢弃 。如果 减数 是一个立即数,它将被符号扩展到 被减数 的长度。EFLAGS 寄存器以与 sub 指令相同的方式设置。

请注意,GAS/AT&T 语法可能相当令人困惑,例如 cmp $0, %rax 后跟 jl branch 将分支到 %rax < 0(而不是像操作数顺序可能预期的那样相反)。


操作数

被减数

  • AL/AX/EAX(仅当 减数 是立即数时)
  • 寄存器
  • 内存

减数

  • 寄存器
  • 立即数
  • 内存


修改后的标志

  • SF ≔ MostSignificantBit(difference),因此未设置 SF 意味着 difference 为非负(被减数 ≥ 减数 [注意:带符号比较])
  • ZF ≔ (difference = 0)
  • PF ≔ BitWiseXorNor(difference[Max-1:0])
  • CFOFAF

跳转指令

[编辑 | 编辑源代码]

跳转指令允许程序员(间接)设置 EIP 寄存器的值。作为参数传递的位置通常是一个标签。跳转后执行的第一条指令是紧随标签之后的指令。除了 jmp 之外,所有跳转指令都是条件跳转,这意味着只有在条件为真时才会改变程序流。这些指令通常在比较指令(见上文)之后使用,但由于许多其他指令也设置标志,因此此顺序不是必需的。

有关标志及其含义的更多信息,请参见章节“X86 架构”,§ “EFLAGS 寄存器”

无条件跳转

[编辑 | 编辑源代码]

jmp loc

用指定的地址加载 EIP(即,执行的下一条指令是 jmp 指定的指令)。

如果相等则跳转

[编辑 | 编辑源代码]

je loc

ZF = 1

如果先前 cmp 指令的操作数相等,则用指定的地址加载 EIPjejz 相同。例如

mov ecx, $5
mov edx, $5
cmp ecx, edx
je equal
; if it did not jump to the label equal,
; then this means ecx and edx are not equal.
equal:
; if it jumped here, then this means ecx and edx are equal

如果不相等则跳转

[编辑 | 编辑源代码]

jne loc

ZF = 0

如果先前 cmp 指令的操作数不相等,则用指定的地址加载 EIPjnejnz 相同

如果大于则跳转

[编辑 | 编辑源代码]

jg loc

SF = OF 且 ZF = 0

如果先前 cmp 指令的 被减数 大于第二个(执行带符号比较),则用指定的地址加载 EIP

如果大于或等于则跳转

[编辑 | 编辑源代码]

jge loc

SF = OF 或 ZF = 1

如果先前 cmp 指令的 被减数 大于或等于 减数(执行带符号比较),则用指定的地址加载 EIP

如果高于(无符号比较)则跳转

[编辑 | 编辑源代码]

ja loc

CF = 0 且 ZF = 0

如果先前 cmp 指令的 被减数 大于 减数,则用指定的地址加载 EIPjajg 相同,只是它执行无符号比较。

这意味着,以下代码段始终跳转(除非 rbx 也为 -1),因为负一在二进制补码中表示为所有位都设置。

mov rax, -1 // rax := -1
cmp rax, rbx
ja loc

将所有位都设置为 1(不将任何位视为符号)的值为 2ⁿ-1(其中 n 是寄存器的长度)。这是寄存器可以保存的最大无符号值。

如果高于或等于(无符号比较)则跳转

[编辑 | 编辑源代码]

jae loc

CF = 0 或 ZF = 1

如果之前 `cmp` 指令的 `被减数` 大于或等于 `减数`,则将 `EIP` 加载到指定地址。`jae` 与 `jge` 相同,区别在于它执行无符号比较。

小于则跳转

[编辑 | 编辑源代码]

jl loc

`jl` 所需的条件是 `SF` ≠ `OF`。如果满足该条件,则将 `EIP` 加载到指定地址。因此,`SF` 或 `OF` 可以设置,但不能同时设置以满足该条件。如果以 `sub`(基本上等同于 `cmp`)指令为例,我们有

`被减数` - `减数`

关于 `sub` 和 `cmp`,有几种情况满足该条件

  1. `被减数` < `减数` 且操作没有溢出
  2. `被减数` > `减数` 且操作溢出


在第一种情况下,`SF` 将被设置,但 `OF` 不会被设置;在第二种情况下,`OF` 将被设置,但 `SF` 不会被设置,因为溢出会将最高有效位重置为零,从而阻止 `SF` 被设置。`SF` ≠ `OF` 条件避免了以下情况

  1. `被减数` > `减数` 且操作没有溢出
  2. `被减数` < `减数` 且操作溢出
  3. `被减数` = `减数`

在第一种情况下,`SF` 和 `OF` 都不设置;在第二种情况下,`OF` 将被设置,`SF` 也将被设置,因为溢出会将最高有效位重置为 1;在最后一种情况下,`SF` 和 `OF` 都不设置。

小于或等于则跳转

[编辑 | 编辑源代码]

jle loc

`SF` ≠ `OF` 或 `ZF = 1`。

如果之前 `cmp` 指令的 `被减数` 小于或等于 `减数`,则将 `EIP` 加载到指定地址。有关条件的更详细说明,请参阅`jl` 部分

小于则跳转(无符号比较)

[编辑 | 编辑源代码]

jb loc

CF = 1

如果之前 CMP 指令的第一个操作数小于第二个操作数,则将 EIP 加载到指定地址。`jb` 与 `jl` 相同,区别在于它执行无符号比较。

mov rax, 0   ; rax ≔ 0
cmp rax, rbx ; rax ≟ rbx
jb loc       ; always jumps, unless rbx is also 0

小于或等于则跳转(无符号比较)

[编辑 | 编辑源代码]

jbe loc

CF = 1 或 ZF = 1

如果之前 `cmp` 指令的 `被减数` 小于或等于 `减数`,则将 `EIP` 加载到指定地址。`jbe` 与 `jle` 相同,区别在于它执行无符号比较。

等于零则跳转

[编辑 | 编辑源代码]

jz loc

ZF = 1

如果之前算术表达式的零位被设置,则将 `EIP` 加载到指定地址。`jz` 与 `je` 相同。

不等于零则跳转

[编辑 | 编辑源代码]

jnz loc

ZF = 0

如果之前算术表达式的零位未设置,则将 `EIP` 加载到指定地址。`jnz` 与 `jne` 相同。

符号位被设置则跳转

[编辑 | 编辑源代码]

js loc

SF = 1

如果之前算术表达式的符号位被设置,则将 `EIP` 加载到指定地址。

符号位未被设置则跳转

[编辑 | 编辑源代码]

jns loc

SF = 0

如果之前算术表达式的符号位未设置,则将 `EIP` 加载到指定地址。

进位标志被设置则跳转

[编辑 | 编辑源代码]

jc loc

CF = 1

如果之前算术表达式的进位标志被设置,则将 `EIP` 加载到指定地址。

进位标志未被设置则跳转

[编辑 | 编辑源代码]

jnc loc

CF = 0

如果之前算术表达式的进位标志未设置,则将 `EIP` 加载到指定地址。

溢出标志被设置则跳转

[编辑 | 编辑源代码]

jo loc

OF = 1

如果之前算术表达式的溢出标志被设置,则将 `EIP` 加载到指定地址。

溢出标志未被设置则跳转

[编辑 | 编辑源代码]

jno loc

OF = 0

如果之前算术表达式的溢出标志未设置,则将 `EIP` 加载到指定地址。

计数器寄存器等于零则跳转

[编辑 | 编辑源代码]

jcxz loc

CX = 0

jecxz loc

ECX = 0

jrcxz loc

RCX = 0

如果计数器寄存器为零,则将 `EIP` 加载到指定地址。

  • 该指令的存在使得计数器寄存器特别适合保存(高级语言)指针:在大多数编程语言中,“空指针”(无效指针)由数字值 0 实现。通常,您不想取消引用这样的空指针,因为结果将是错误的,甚至会导致 GPF。通过使用 jecx 跳转到一些处理此错误的代码,您可以在尝试取消引用指针值之前避免陷入这种状况。您不需要额外的 test ecx, ecx
  • 如果您使用 loop 指令实现循环,但请求的迭代次数可能为零,您可能希望在循环主体之前插入 jecx。否则,loop 将递减零,从而最终执行 232 次迭代。

函数调用

[编辑 | 编辑源代码]

call proc

将 `call` 调用后面的指令地址(即通常是源代码中的下一行)压入堆栈顶部,然后跳转到指定位置。这主要用于子例程。

ret [val]

将堆栈上的下一个值弹出到 `EIP` 中,然后从堆栈中弹出指定数量的字节。如果未提供 `val`,则该指令在返回后不会从堆栈中弹出任何值。

循环指令

[编辑 | 编辑源代码]

loop arg

loop 指令递减 ECX 并跳转到 arg 指定的地址,除非递减 ECX 使其值变为零。例如

	mov ecx, 5 ; ecx ≔ 5
head:
	; the code here would be executed 5 times
	loop head

loop 不设置任何标志。

loopcc arg

这些循环指令递减 ECX 并跳转到 arg 指定的地址,如果它们的条件满足(即,特定的标志被设置),除非递减 ECX 使其值变为零。

  • loope 如果相等则循环
  • loopne 如果不相等则循环
  • loopnz 如果不为零则循环
  • loopz 如果为零则循环

这样,只有测试非零 ECX 可以与测试 ZF 相结合。其他标志不能被测试,比如没有 loopnc “当 ECX ≠ 0 且 CF 未设置时循环”。

进入和离开

[编辑 | 编辑源代码]

enter arg

enter 在堆栈上分配指定的空间创建堆栈帧。

leave

leave 销毁当前堆栈帧,并恢复之前的帧。使用英特尔语法,这等效于

mov esp, ebp ; esp ≔ ebp
pop ebp

这将使 EBPESP 设置为函数序言开始之前各自的值,从而逆转序言期间对堆栈进行的任何修改。

其他控制指令

[编辑 | 编辑源代码]

hlt

停止处理器。执行将在处理下一个硬件中断后恢复,除非 IF 被清除。

nop

无操作。此指令不执行任何操作,但在处理器中浪费(一个)指令周期。

此指令通常表示xchg 操作,操作数为 EAXEAX(一个没有副作用的操作),因为没有指定的操作码用于不执行任何操作。这只是一个顺便提及,这样你不会与反汇编代码混淆。

lock

在下一条指令上断言 #LOCK 前缀。

wait

等待 FPU 完成其最后一个计算。

华夏公益教科书