测量使用 Clang/LLVM 生成的函数的大小?

Measuring size of a function generated with Clang/LLVM?

最近,在做一个项目时,我需要测量 C 函数的大小以便能够将其复制到其他地方,但找不到任何 "clean" 解决方案(最终,我只是想在我可以引用的函数末尾插入一个标签。

为这个架构编写了 LLVM 后端(虽然它可能看起来像 ARM,但它不是)并且知道它为该架构发出汇编代码,我选择了以下 hack(我认为评论解释了它很好):

/***************************************************************************
 * if ENABLE_SDRAM_CALLGATE is enabled, this function should NEVER be called
 * from C code as it will corrupt the stack pointer, since it returns before
 * its epilog. this is done because clang does not provide a way to get the
 * size of the function so we insert a label with inline asm to measure the
 * function. in addition to that, it should not call any non-forceinlined
 * functions to avoid generating a PC relative branch (which would fail if
 * the function has been copied)
 **************************************************************************/
void sdram_init_late(sdram_param_t* P) {
    /* ... */
#ifdef ENABLE_SDRAM_CALLGATE
    asm(
        "b lr\n"
        ".globl sdram_init_late_END\n"
        "sdram_init_late_END:"
    );
#endif
}

它按预期工作,但需要一些汇编胶水代码才能调用它,这是一个非常肮脏的 hack,它之所以起作用,是因为我可以假设有关代码生成过程的一些事情。

我还考虑过其他方法来执行此操作,如果 LLVM 发出机器代码,这些方法会更好(因为一旦我将 MC 发射器添加到我的 LLVM 后端,这种方法就会中断)。我考虑的方法涉及获取函数并搜索终止符指令(可能是 b lr 指令或 pop ..., lr 的变体),但这也可能引入额外的复杂性(尽管它看起来比我的更好原解)。

任何人都可以建议一种更简洁的方法来获取 C 函数的大小,而不必诉诸上面概述的极其丑陋和不可靠的 hack 吗?

我认为您是对的,没有任何真正可行的table 方法可以做到这一点。允许编译器对函数重新排序,因此按照源代码顺序获取下一个函数的地址是不安全的(但在某些情况下确实有效)。


如果您可以解析对象文件 (maybe with libbfd),您或许可以从中获取函数大小。

clang 的 asm output 有这个元数据(每个函数后的 .size 汇编程序指令),但我不确定它是否最终出现在目标文件中。

int foo(int a) { return a * a * 2; }

   ## clang-3.8 -O3 for amd64:
   ## some debug-info lines manually removed
    .globl  foo
foo:
.Lfunc_begin0:
        .cfi_startproc
        imul    edi, edi
        lea     eax, [rdi + rdi]
        ret
.Lfunc_end0:
        .size   foo, .Lfunc_end0-foo   ####### This line

clang-3.8 -O3 -Wall -Wextra func-size.c -c 将其编译为 .o,然后我可以这样做:

$ readelf --symbols func-size.o 

Symbol table '.symtab' contains 4 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS func-size.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    2 
     3: 0000000000000000     7 FUNC    GLOBAL DEFAULT    2 foo   ### This line

三个指令共7个字节,与这里的size输出匹配。它不包括对齐入口点或下一个函数的填充:.align 指令在减去计算 .size.

的两个标签之外

这可能不适用于剥离的 executables。甚至它们的全局函数也不会出现在 executable 的符号 table 中。所以你可能需要一个两步构建过程:

  • 编译您的 "normal" 代码
  • 使用 readelf | some text processing > sizes.c
  • 将您关心的函数大小放入 table
  • 编译sizes.c
  • link一切都在一起

警告

一个真正聪明的编译器可以编译多个相似的函数来共享一个共同的实现。所以其中一个函数跳转到另一个函数体的中间。如果幸运的话,所有函数都组合在一起,每个函数的 "size" 从其入口点一直测量到它使用的代码块的末尾。 (但这种重叠会使总大小加起来超过文件的大小。)

当前的编译器不这样做,但是你可以通过将函数放在一个单独的编译单元而不是使用整个程序来防止它link-时间优化。

编译器可以决定在函数入口点之前放置一个有条件执行的代码块,因此分支可以使用较短的编码来实现较小的位移。 可能不会包含在 "size" 函数计算中。不过,当前的编译器也从不这样做。


另一个想法,我不确定是否安全:

在函数末尾放置一个 asm volatile 只是 一个标签定义,然后假设函数大小最多为 + 32 字节或其他。因此,当您复制该函数时,您分配了一个比 "calculated" 大小大 32B 的缓冲区。希望在标签之外只有一个 "ret" insn,但实际上它可能在弹出它使用的所有调用保留寄存器的函数尾声之前。

我不认为优化器可以复制 asm volatile 语句,因此它会强制编译器跳转到一个常见的结尾,而不是像有时在早期条件下那样复制结尾。

但我不确定在之后 asm volatile.

最终会达到多少上限。