基本块定义为以跳转(直接或间接)指令结束的(非跳转)指令序列。跳转目标地址应该是另一个基本块的开始。考虑我有以下汇编代码:
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。
答案 0 :(得分:3)
基本块是控制流图中的节点,这意味着一旦控制进入块,除了遍历整个块并退出之外,它不能做任何其他操作。这并不意味着他们必须以跳转指令开始或结束。为了更好地理解,请参阅Wikipedia的摘录:
由于其构造过程,在CFG中,每个边缘A→B都有 属性:
outdegree(A)&gt; 1或indecree(B)&gt; 1(或两者)。
因此,CFG可以 从概念上讲,从程序开始(完整) 流程图 - 即。每个节点代表一个人的图表 指令 - 并为每个边缘执行边缘收缩 伪造上面的谓词,即收缩每个边缘 source有一个出口,其目的地只有一个条目。
根据这个定义,我将以不同的方式分析106b0和106ec之间的代码:一个块B1从106b0到106c0,一个块B2从106c4到106ec。这段代码是循环,B1是循环的设置部分,B2是正文。
在ARM中,bl
指令(例如106dc处的指令)是一个函数调用,这意味着控制将被传递给被调用的函数,但随后会在bl
之后返回到指令。因此,如果我们只为调用函数构造CFG,我不会将此指令视为块边界。如果我们为整个程序做CFG,那么在这里应该有一个对被调用函数的优势,然后是从被调用函数到下一条指令的另一个边缘。
答案 1 :(得分:0)
正如塞缪尔的回答所解释的那样,基本块不包含分支目标。指令块的分支目标也是基本块之间的边界。
您正在使用编译器生成此代码,因此使用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
。第二个块没有标签,因为它是在条件分支掉落之后开始的。
.LBB0_2
标签名称是自动生成的。 .L
表示其本地&#34; label(目标文件的符号表中没有符号;仅在组装此文件时供内部使用)。 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的哪个寄存器中。