分支

分支语句 属于 x86-64 Assembly

4 个练习

关于 分支语句

RFLAGs

有一个特殊的寄存器,叫做rflags。它的各个位分别充当不同条件的标志。

下面列出其中一部分:

name symbol bit
进位 CF 0
零 ZF 6
符号 SF 7
溢出 OF 11

完整列表请参阅 Intel 手册。

比较指令

rflags中的标志_不是_直接修改的。相反,它们由许多不同的指令设置。

例如,当结果为零时,许多算术运算或位运算都会设置ZF。

用于测试条件的最常见指令之一是cmp。它接受两个操作数并更新标志,但不会修改它的操作数。

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。

Note

我们已经见过两条这样的指令: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会跳转。

Note

有些后缀是相同条件的别名。例如,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
通过 GitHub 编辑 该链接会在新窗口或标签页中打开

学习 分支语句