有一个特殊的寄存器,叫做rflags。它的各个位分别充当不同条件的标志。
下面列出其中一部分:
| name | symbol | bit |
|---|---|---|
| 进位 | CF | 0 |
| 零 | ZF | 6 |
| 符号 | SF | 7 |
| 溢出 | OF | 11 |
完整列表请参阅 Intel 手册。
rflags中的标志_不是_直接修改的。相反,它们由许多不同的指令设置。
例如,当结果为零时,许多算术运算或位运算都会设置ZF。
用于测试条件的最常见指令之一是cmp。它接受两个操作数并更新标志,但不会修改它的操作数。
cmp指令用第一个操作数减去第二个操作数,并根据结果设置标志。
如果 A 是第一个操作数,B 是第二个操作数:
| flag | set when |
|---|---|
| CF | A < B(无符号) |
| ZF | A == B |
| SF | A < B(有符号,无溢出) |
| OF | 有符号减法中发生溢出 |
默认情况下,x86-64 中的代码会从上到下顺序执行。
但在很多情况下,有必要修改这种行为。例如,根据某个条件执行另一组不同的指令。
在高级语言中,这通常通过if...else条件语句之类的抽象来实现。然而,x86-64 中并不存在这些抽象。
x86-64 提供了一些指令,可以把执行流程转移到代码中的另一个位置。这叫做branching。
我们已经见过两条这样的指令:call和ret。
调用函数时,执行流程会从调用方转移到被调用的函数。而返回时,执行流程又会转移回调用方。
如果没有找到ret,执行流程就会从一个函数直接落入下一个函数。有时可以用这一点来优化代码流程。
指令 jmp 会无条件地把程序的执行转移到代码中的另一个位置。它唯一的操作数是一个标签,该标签保存了执行将继续的位置的地址。
例如,看看下面这个函数:
fn:
mov rax, 5
jmp end
add rax, 10
end:
ret
调用fn时,执行从mov rax, 5开始。这会把rax中的值设为 5。
下一条指令是jmp end,它把执行转移到标签end。
在end之后,下一条指令是ret,它把执行转移回调用方函数。
注意,由于add rax, 10位于jmp end之后、end之前,它永远不会被执行。fn返回时rax的值是 5。
jcc 系列指令只有在满足特定条件时,才会把程序的执行转移到另一个位置。否则,执行会继续顺序进行。
每个条件都对应rflags中的一个或多个标志。有些jcc变体检测某个标志是否被置位,另一些则检测它是否被清零。
jcc中的cc并不是字面意思,而是指与被检测标志相关联的特定后缀。
后缀有很多,其中许多测试的条件与另一个后缀相同。其中一些直接对应某个标志,因此当特定标志被置位时,指令会跳转到某个标签:
| suffix | jumps if |
|---|---|
| z | ZF == 1 |
| c | CF == 1 |
| s | SF == 1 |
| o | OF == 1 |
还有许多后缀是为了对应它们在cmp指令中的含义而选用的。例如:
| instruction | suffix | jumps if |
|---|---|---|
| cmp A, B | e | A == B |
| cmp A, B | l | A < B(有符号) |
| cmp A, B | b | A < B(无符号) |
| cmp A, B | g | A > B(有符号) |
| cmp A, B | a | A > B(无符号) |
可以在l、b、g或a后面加上e,把相等的情况也包含进条件里:
cmp rcx, r8
jge two ; this jumps to 'two' if rcx is greater than, or equal to, r8 in a signed comparison
jbe two ; this jumps to 'two' if rcx is lesser than, or equal to, r8 in an unsigned comparison
对于所有后缀,都有检测相反行为的变体。它们的语法相同,只是在后缀前面加一个n。
例如,jnz会在ZF没有被置位时跳转。类似地,当 A 不大于等于 B 时(A 和 B 按无符号整数解释),jnae会跳转。
有些后缀是相同条件的别名。例如,jz(后缀z,对应ZF)和je(后缀e,表示相等)都会在ZF被置位时跳转。这是因为,使用cmp时,减法结果为零就会置位ZF,也就意味着两个操作数相等。
不过,另一些后缀检测的是多个标志的组合,无法用一个单一标志后缀直接替换。
请优先选择更能描述你比较语义的后缀。
标签在整个源文件中都可见,它们并不局限于某个函数。因此无法重复使用同一个标签名。
为了模拟局部标签的行为,NASM 为以句点(.)开头的标签提供了一种特殊写法。这种写法定义的标签会隐式包含前一个不带点的标签名:
section .text
fn1:
...
.example: ; this is fn1.example
...
ret
fn2:
...
.example: ; this is fn2.example
...
ret
仍然可以在代码的任何位置使用完整标签名跳转到这个标签,例如jmp fn1.example。
不过,如果跳转时只使用标签从点开始的部分,那么跳转的目标会是上层函数_内部_的那个标签。例如,.example的行为就好像它只属于该函数一样:
section .text
fn1:
...
.example:
...
jmp .example ; this jumps to fn1.example
fn2:
...
.example:
...
jmp .example ; this jumps to fn2.example
注意,函数内部不带点的标签实际上会定义另一个函数:
section .text
fn1:
...
.example: ; this is 'fn1.example'
...
non_dotted:
...
.example: ; this is 'non_dotted.example'
...
ret