在基本块的中间跳转

Jump in the middle of basic block

基本块被定义为以跳转(直接或间接)指令结尾的一系列(非跳转)指令。跳转目标地址应该是另一个基本块的开始。假设我有以下汇编代码:

106ac:       ba00000f        blt     106f0 <main+0xb8>
106b0:       e3099410        movw    r9, #37904      ; 0x9410
106b4:       e3409001        movt    r9, #1
106b8:       e79f9009        ldr     r9, [pc, r9]
106bc:       e3a06000        mov     r6, #0
106c0:       e1a0a008        mov     sl, r8
106c4:       e30993fc        movw    r9, #37884      ; 0x93fc
106c8:       e3409001        movt    r9, #1
106cc:       e79f9009        ldr     r9, [pc, r9]
106d0:       e5894000        str     r4, [r9]
106d4:       e7941105        ldr     r1, [r4, r5, lsl #2]
106d8:       e1a00007        mov     r0, r7
106dc:       e12fff31        blx     r1
106e0:       e0806006        add     r6, r0, r6
106e4:       e25aa001        subs    sl, sl, #1
106e8:       e287700d        add     r7, r7, #13
106ec:       1afffff4        bne     106c4 <main+0x8c>
106f0:       e30993d0        movw    r9, #37840      ; 0x93d0
106f4:       e3409001        movt    r9, #1

bb1

106a4:       ...
106ac:       ba00000f        blt     106f0 <main+0xb8>

第一个基本块bb1的目标地址是bb4的开始。

bb2

106b0:       e3099410        movw    r9, #37904      ; 0x9410
....        All other instructions
106c4:       e30993fc        movw    r9, #37884      ; 0x93fc
....        All other instructions
106d8:       e1a00007        mov     r0, r7
106dc:       e12fff31        blx     r1

第二个基本块bb2有一个间接分支,所以目标地址只有在运行时才知道。

bb3

106e0:       e0806006        add     r6, r0, r6
106e4:       e25aa001        subs    sl, sl, #1
106e8:       e287700d        add     r7, r7, #13
106ec:       1afffff4        bne     106c4 <main+0x8c>

第三个基本块的目标地址不是另一个基本块的开始,而是在bb2的中间。 根据基本块的定义,是不可能的。但是,在实践中,我在多个地方看到了这种行为(跳到基本块的中间)。如何解释这种行为?是否可以强制编译器 (LLVM) 生成除基本块开头以外不跳转到其他任何地方的代码?

bb4

106f0:       e30993d0        movw    r9, #37840      ; 0x93d0
106f4:       e3409001        movt    r9, #1
....
Ends with a branch (direct or indirect)

我正在使用工具 (SPEDI) 生成基本块,使用的编译器是 LLVM(CLANG 前端),目标架构是 ARM V7 Cortex-A9。

基本块是控制流图中的节点,这意味着一旦控制进入块,除了运行遍历整个块并退出它之外,它不能做任何其他事情。这并不意味着它们必须以跳转指令开始或结束。为了更好地理解,请参阅 Wikipedia 的摘录:

Because of its construction procedure, in a CFG, every edge A→B has the property that:

outdegree(A) > 1 or indegree(B) > 1 (or both).

The CFG can thus be obtained, at least conceptually, by starting from the program's (full) flow graph—i.e. the graph in which every node represents an individual instruction—and performing an edge contraction for every edge that falsifies the predicate above, i.e. contracting every edge whose source has a single exit and whose destination has a single entry.

根据这个定义,我将以不同的方式分析 106b0 和 106ec 之间的代码:一个块 B1 从 106b0 到 106c0,一个块 B2 从 106c4 到 106ec。这段代码是一个循环,B1是循环的设置部分,B2是循环体。

在 ARM 中,bl 指令(例如 106dc 处的指令)是一个函数调用,这意味着控制权将传递给被调用的函数,然后返回到 bl 之后的指令。因此,如果我们仅为调用函数构建 CFG,我不会将此指令视为块边界。如果我们对整个程序进行 CFG,应该有一条边指向被调用函数,然后另一条边从被调用函数返回到下一条指令。

正如 Samuel 的回答所解释的那样,基本块不包含分支目标。指令块中的分支目标也是基本块之间的边界。

您正在使用编译器生成此代码,因此使用clang -O3 -S foo.c 获取带有分支目标标签的编译器 asm 输出。

一直编译到一个目标文件然后反汇编,这意味着您需要一个反汇编程序将标签放回它在反汇编时找到的所有分支的目标。 Agner Fog's x86 disassembler, objconv 这样做。也许 ARM 有类似的东西,但我不认为 GNU binutils objdump -d 有一个选项。

我没有安装 ARM clang,但输出可能与 x86 非常相似。例如,将使用分支编译的非常简单的函数:

int sa, sb;
void foo(int a, int b) {
    if (a>b) {
        sb = b;
    }
    sa = a;
}

为 x86 编译 on the Godbolt compiler explorer with clang5.0 -O3。 (Godbolt 已安装 ARM-gcc,但未安装 ARM-clang)

foo(int, int):                               # @foo(int, int)
        cmp     edi, esi
        jle     .LBB0_2
        mov     dword ptr [rip + sb], esi
.LBB0_2:
        mov     dword ptr [rip + sa], edi
        ret

这里有3个基本块:cmp/jle,第一个mov,第二个mov+ret。第二个块没有标签,因为它在条件分支的 fall-through 之后开始。

.LBB0_2 标签名称是 auto-generated。 .L 表示它是一个 "local" 标签(目标文件的 symbol-table 中没有符号;它仅供内部在汇编此文件时使用)。 BB 代表基本块。 我认为 BB0_2 表示它是第一个函数中的基本块 #2(从 0 开始计数)。 (用不同的名称复制函数会给我们一个 .LBB1_2 标签。)在一个函数中,不同的标签有不同的最后一个数字。


Clang甚至在评论中标注所有基本块:

在 Godbolt 上,单击 // 按钮以禁用隐藏评论行。然后你得到:

foo(int, int):                               # @foo(int, int)
# BB#0:
        #DEBUG_VALUE: foo:a <- %EDI
        #DEBUG_VALUE: foo:b <- %ESI
        cmp     edi, esi
        jle     .LBB0_2
# BB#1:
        #DEBUG_VALUE: foo:b <- %ESI
        #DEBUG_VALUE: foo:a <- %EDI
        mov     dword ptr [rip + sb], esi
.LBB0_2:
        #DEBUG_VALUE: foo:b <- %ESI
        #DEBUG_VALUE: foo:a <- %EDI
        mov     dword ptr [rip + sa], edi
        ret

即不是分支目标的基本块得到一个注释来分隔 + 编号它们,而不是 .L 本地标签。它还显示了哪些 C 变量在进入 BB 时在哪些寄存器中。