Clang中的内置函数并不是真正的内置函数?

10
如果我在 strlen.c 中有以下内容:
int call_strlen(char *s) {
  return __builtin_strlen(s);
}

然后像这样使用gcc和clang编译它:

gcc -c -o strlen-gcc.o strlen.c

clang -c -o strlen-clang.o strlen.c

我很惊讶地发现strlen-clang.o中包含对"strlen"的引用,而gcc已经将该函数内联并且没有这样的引用。(请参见下面的objdumps)。这是clang的一个bug吗?我已经在几个版本的clang编译器中进行了测试,包括3.8。
编辑:这对我很重要的原因是我正在链接-nostdlib,而clang编译的版本给我一个链接错误,说找不到strlen。 Clang
@> objdump -d strlen-clang.o 

strlen-clang.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <call_strlen>:
   0: 55                    push   %rbp
   1: 48 89 e5              mov    %rsp,%rbp
   4: 48 83 ec 10           sub    $0x10,%rsp
   8: 48 89 7d f8           mov    %rdi,-0x8(%rbp)
   c: 48 8b 7d f8           mov    -0x8(%rbp),%rdi
  10: e8 00 00 00 00        callq  15 <call_strlen+0x15>
  15: 89 c1                 mov    %eax,%ecx
  17: 89 c8                 mov    %ecx,%eax
  19: 48 83 c4 10           add    $0x10,%rsp
  1d: 5d                    pop    %rbp
  1e: c3                    retq   


@> objdump -t strlen-clang.o 

strlen-clang.o:     file format elf64-x86-64

SYMBOL TABLE:
0000000000000000 l    df *ABS*  0000000000000000 strlen.c
0000000000000000 l    d  .text  0000000000000000 .text
0000000000000000 l    d  .data  0000000000000000 .data
0000000000000000 l    d  .bss 0000000000000000 .bss
0000000000000000 l    d  .comment 0000000000000000 .comment
0000000000000000 l    d  .note.GNU-stack  0000000000000000 .note.GNU-stack
0000000000000000 l    d  .eh_frame  0000000000000000 .eh_frame
0000000000000000 g     F .text  000000000000001f call_strlen
0000000000000000         *UND*  0000000000000000 strlen

GCC

@> objdump -d strlen-gcc.o 

strlen-gcc.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <call_strlen>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   48 89 7d f8             mov    %rdi,-0x8(%rbp)
   8:   48 8b 45 f8             mov    -0x8(%rbp),%rax
   c:   48 c7 c1 ff ff ff ff    mov    $0xffffffffffffffff,%rcx
  13:   48 89 c2                mov    %rax,%rdx
  16:   b8 00 00 00 00          mov    $0x0,%eax
  1b:   48 89 d7                mov    %rdx,%rdi
  1e:   f2 ae                   repnz scas %es:(%rdi),%al
  20:   48 89 c8                mov    %rcx,%rax
  23:   48 f7 d0                not    %rax
  26:   48 83 e8 01             sub    $0x1,%rax
  2a:   5d                      pop    %rbp
  2b:   c3                      retq   

@> objdump -t strlen-gcc.o 

strlen-gcc.o:     file format elf64-x86-64

SYMBOL TABLE:
0000000000000000 l    df *ABS*  0000000000000000 strlen.c
0000000000000000 l    d  .text  0000000000000000 .text
0000000000000000 l    d  .data  0000000000000000 .data
0000000000000000 l    d  .bss 0000000000000000 .bss
0000000000000000 l    d  .note.GNU-stack  0000000000000000 .note.GNU-stack
0000000000000000 l    d  .eh_frame  0000000000000000 .eh_frame
0000000000000000 l    d  .comment 0000000000000000 .comment
0000000000000000 g     F .text  000000000000002c call_strlen

1
你没有设置编译器进行优化,所以clang没有进行优化。令人震惊。 - EOF
3
那有什么相关性呢?如果被告知要使用内置函数,那么应该使用内置函数,而不是“如果你愿意的话,可能可以使用更优化的模式来使用内置函数”。builtin只是像inline这样的可选请求吗?如果是这样,请您提供Clang文档中指定此内容的部分链接。 - underscore_d
2
@EOF 顺便说一下,我使用了“-O3”编译选项,汇编代码变得更短了,但仍然包含了“jmp _strlen”。 - Siguza
1
@Siguza:我的gcc 4.8.4只有在不进行优化(-O0)时才会为调用strlen()__builtin_strlen()创建不同的代码,对于所有其他优化级别,它都会为两者生成相同的代码。有时它会内联(-O1,-Os),有时它会调用库(-O2,-O3)。 - EOF
1
@EOF,我认为这不是关于优化的问题,而是关于GCC内置函数文档中提到的“GCC内置函数总是在内联中展开”的问题。诚然,我找不到Clang的等效语言,但是在没有其他文档的情况下,我认为Clang应该在功能上等同于GCC。 - brooks94
2个回答

8

为了优化:

使用 clang -O0

t.o:
(__TEXT,__text) section
_call_strlen:
0000000000000000    pushq   %rbp
0000000000000001    movq    %rsp, %rbp
0000000000000004    subq    $0x10, %rsp
0000000000000008    movq    %rdi, -0x8(%rbp)
000000000000000c    movq    -0x8(%rbp), %rdi
0000000000000010    callq   _strlen
0000000000000015    movl    %eax, %ecx
0000000000000017    movl    %ecx, %eax
0000000000000019    addq    $0x10, %rsp
000000000000001d    popq    %rbp
000000000000001e    retq

使用 clang -O3

t.o:
(__TEXT,__text) section
_call_strlen:
0000000000000000    pushq   %rbp
0000000000000001    movq    %rsp, %rbp
0000000000000004    popq    %rbp
0000000000000005    jmp _strlen

现在,来看看问题本身:
clang文档称clang支持所有GCC支持的内建函数。然而,GCC文档似乎将内建函数和它们的库等价名称视为同义词:
“两种形式具有相同的类型(包括原型),相同的地址(当它们的地址被取出时),并且与C库函数具有相同的含义[...]。”
此外,它不保证具有库等效项的内建函数(如strlen)确实得到优化:
“这些函数中的许多函数仅在某些情况下进行了优化;如果它们在特定情况下未被优化,则会发出对库函数的调用。”
另外,clang内部手册仅一次提到__builtin_strlen
  • __builtin_strlenstrlen:如果参数是字符串字面值,则将其作为整数常量表达式进行常量折叠。

除此之外,它们似乎没有作出任何承诺。

由于在您的情况下,__builtin_strlen的参数不是字符串字面值,并且由于GCC文档允许将内置函数调用转换为库函数调用,因此clang的行为似乎完全有效。

clang开发人员邮件列表上的"待审核补丁"也表示:

[...]如果不可能/不需要进行编译时评估,则仍将回退到运行时使用库函数strlen[...]。

这是2012年的事情,但该文本表明至少当时仅支持编译时评估。

现在,我看到两个选择:

  • 如果您只需要自己编译程序并使用和/或分发它,我建议您简单地使用gcc。
  • 如果您希望他人能够在gcc和clang下编译您的代码,我建议添加一个C库作为静态链接的依赖项。

我强烈建议不要自己实现标准库函数,即使对于看似简单的情况也是如此(如果您不同意,请尝试编写自己的strlen实现,然后将其与glibc one进行比较)。


我强烈建议不要自己实现标准库函数。除了性能之外,还有其他原因吗?请注意,很容易设置调试模式,通过运行两个函数并检查它们是否输出相同的结果来进行比较。 - TLW
1
@TLW 缺陷和规范一致性。基本功能并不难正确获取或调试,但边缘情况很难发现。即使是简单的函数,如果你试图优化性能,也可能变得复杂。而且,如果它们与用户输入接触,就会有很多内存错误可以被利用。相比之下,C标准库函数已经经过数十年的审计。 - Siguza
@Siguza - 很好的回答,尽管AFL / UBSAN / Valgrind以及与标准库行为的比较可以让你走得更远。 - TLW

7
GCC和Clang都不保证内联此内建函数。你引用了一些GCC文档,看起来似乎作出了这样的承诺:

...GCC内置函数总是被展开为内联形式...

但是这是从上下文中抽取出来的一个句子片段。完整的句子是这样说的:

除了存在库等效函数的内建函数(例如下面讨论的标准C库函数)或者展开为库调用的函数之外,GCC内置函数总是被展开为内联形式,因此没有对应的入口点,也无法获得其地址。

__builtin_strlen具有库等效函数strlen,因此这个句子并不保证它是否被内联。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接