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
设置为 1CF
≔ 0OF
≔ 0AF
未定义
应用
- 将同一个寄存器传递两次:
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])CF
、OF
和AF
跳转指令允许程序员(间接)设置 EIP 寄存器的值。作为参数传递的位置通常是一个标签。跳转后执行的第一条指令是紧随标签之后的指令。除了 jmp
之外,所有跳转指令都是条件跳转,这意味着只有在条件为真时才会改变程序流。这些指令通常在比较指令(见上文)之后使用,但由于许多其他指令也设置标志,因此此顺序不是必需的。
有关标志及其含义的更多信息,请参见章节“X86 架构”,§ “EFLAGS 寄存器”。
jmp loc
用指定的地址加载 EIP
(即,执行的下一条指令是 jmp
指定的指令)。
je loc
ZF = 1
如果先前 cmp
指令的操作数相等,则用指定的地址加载 EIP
。je
与 jz
相同。例如
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
指令的操作数不相等,则用指定的地址加载 EIP
。jne
与 jnz
相同
jg loc
SF = OF 且 ZF = 0
如果先前 cmp
指令的 被减数
大于第二个(执行带符号比较),则用指定的地址加载 EIP
。
jge loc
SF = OF 或 ZF = 1
如果先前 cmp
指令的 被减数
大于或等于 减数
(执行带符号比较),则用指定的地址加载 EIP
。
ja loc
CF = 0 且 ZF = 0
如果先前 cmp
指令的 被减数
大于 减数
,则用指定的地址加载 EIP
。ja
与 jg
相同,只是它执行无符号比较。
这意味着,以下代码段始终跳转(除非 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`,有几种情况满足该条件
- `被减数` < `减数` 且操作没有溢出
- `被减数` > `减数` 且操作溢出
在第一种情况下,`SF` 将被设置,但 `OF` 不会被设置;在第二种情况下,`OF` 将被设置,但 `SF` 不会被设置,因为溢出会将最高有效位重置为零,从而阻止 `SF` 被设置。`SF` ≠ `OF` 条件避免了以下情况
- `被减数` > `减数` 且操作没有溢出
- `被减数` < `减数` 且操作溢出
- `被减数` = `减数`
在第一种情况下,`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
这将使 EBP
和 ESP
设置为函数序言开始之前各自的值,从而逆转序言期间对堆栈进行的任何修改。
hlt
停止处理器。执行将在处理下一个硬件中断后恢复,除非 IF
被清除。
nop
无操作。此指令不执行任何操作,但在处理器中浪费(一个)指令周期。
此指令通常表示为 xchg
操作,操作数为 EAX
和 EAX
(一个没有副作用的操作),因为没有指定的操作码用于不执行任何操作。这只是一个顺便提及,这样你不会与反汇编代码混淆。
lock
在下一条指令上断言 #LOCK 前缀。
wait
等待 FPU 完成其最后一个计算。