【问题标题】:How does a compiler choose which function to link?编译器如何选择要链接的函数?
【发布时间】:2021-09-08 00:56:51
【问题描述】:

我有一个程序 (main.c):

#include <stdio.h>
#include <math.h>

int main() {
    int result = sqrt(9);
    printf("result: %d\n" ,result);
    return 0;
}

double sqrt(double blah) {
    return 0;
}

当我运行它时,我的结果是

result: 3

这将告诉我链接器正在选择 libm 库的 sqrt 函数而不是我的函数来调用我的 main 函数。

在启用所有警告的情况下编译此程序时,我没有收到任何错误或警告:

gcc main.c -Wall

我的问题:

  • 为什么链接器没有选择我定义的sqrt来调用?
    • 这是确定性的吗?
  • 为什么我没有收到任何错误或警告?似乎具有相同签名的函数的多个定义是一个陷阱,应该以某种方式指出。
  • 有没有办法输出链接到哪里的函数?那么如果遇到引用了意外定义的情况,我可以调试吗?

我唯一能想到的是当我运行gcc --precompile时,我看到了这个函数声明:

extern double sqrt(double);

这是否告诉链接器 sqrt 是在此文件之外定义的?而且既然这已经满足sqrt的定义了,那么链接的时候我自己的定义就被忽略了?

gcc 信息(我知道它真的很响,因为我在 mac 上,不确定这是否对这个问题有影响)

gcc --version
Configured with: --prefix=/Library/Developer/CommandLineTools/usr --with-gxx-include-dir=/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/4.2.1
Apple clang version 11.0.0 (clang-1100.0.33.17)
Target: x86_64-apple-darwin19.6.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin

编辑:

汇编输出:

    .section    __TEXT,__text,regular,pure_instructions
    .build_version macos, 10, 15    sdk_version 10, 15
    .section    __TEXT,__literal8,8byte_literals
    .p2align    3               ## -- Begin function main
LCPI0_0:
    .quad   4621256167635550208     ## double 9
    .section    __TEXT,__text,regular,pure_instructions
    .globl  _main
    .p2align    4, 0x90
_main:                                  ## @main
    .cfi_startproc
## %bb.0:
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register %rbp
    subq    $16, %rsp
    movsd   LCPI0_0(%rip), %xmm0    ## xmm0 = mem[0],zero
    movl    $0, -4(%rbp)
    sqrtsd  %xmm0, %xmm0
    cvttsd2si   %xmm0, %eax
    movl    %eax, -8(%rbp)
    movl    -8(%rbp), %esi
    leaq    L_.str(%rip), %rdi
    movb    $0, %al
    callq   _printf
    xorl    %esi, %esi
    movl    %eax, -12(%rbp)         ## 4-byte Spill
    movl    %esi, %eax
    addq    $16, %rsp
    popq    %rbp
    retq
    .cfi_endproc
                                        ## -- End function
    .globl  _sqrt                   ## -- Begin function sqrt
    .p2align    4, 0x90
_sqrt:                                  ## @sqrt
    .cfi_startproc
## %bb.0:
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register %rbp
    movsd   %xmm0, -8(%rbp)
    xorps   %xmm0, %xmm0
    popq    %rbp
    retq
    .cfi_endproc
                                        ## -- End function
    .section    __TEXT,__cstring,cstring_literals
L_.str:                                 ## @.str
    .asciz  "result: %d\n"


.subsections_via_symbols

【问题讨论】:

  • 如果将函数调用替换为在编译时计算的常量值,您不会感到惊讶。检查汇编输出。
  • 我不是这个编译到的程序集家族的专家,但看起来好像 sqrt 是在 .s 程序集文件中定义的。我可以看到我认为的定义和我认为的调用。
  • @Shawn 我将程序集输出添加到原始问题中
  • sqrtsd %xmm0, %xmm0。那不是函数调用。所以看来编译器已经将其转换为直接汇编指令(这是有道理的)。
  • 感谢两位的帮助。这间接回答了我的大部分问题。我现在看到函数调用类似于“callq _printf”。将来,当我不确定正在链接什么函数时,我会检查程序集输出

标签: c linker


【解决方案1】:

标准 C 库函数的名称保留用作具有外部链接的标识符。您不应该将它们用于您自己的功能。这意味着当您使用保留名称时,编译器可能会假定它是标准函数,而不是您自己的实现。然后,为了优化您的程序(即使没有打开完全优化),编译器可能会用处理器的平方根指令替换对sqrt 的调用。或者编译器甚至可以自己计算结果并将其构建到汇编代码中。

【讨论】:

  • 感谢您的详细回答。您说编译器“可能”假定它是标准函数,并且“可能”用指令替换调用。这是否意味着情况并非总是如此?如果我不确定,我应该经常检查汇编输出吗?
  • @northsideknight:编译器是决定用处理器指令替换对sqrt的调用还是自行计算可能取决于多种因素:它是哪个编译器,是否启用了优化,编译器是否可以推导出参数的准确值,编译器能否看到程序不依赖于sqrt中没有的任何特性,例如设置errno变量等等。与其检查程序集,不如永远不要使用 sqrt 作为自己的函数名。
  • 我将此答案标记为正确。我认为这里重要的是:检查生成的程序集以查看编译器集合对相关代码所做的确切操作。我同意,我永远不会命名函数 sqrt(或任何与包含的库冲突的东西),这个问题更多是为了解释正在发生的事情以及如何调试。
【解决方案2】:

有一个你不知道的问题:

当您链接您的可执行文件时,您指定了-lm 以便提供sqrt() 函数的两个版本,您提供的一个版本和libm.so 共享可执行文件。

问题在于共享可执行文件可能会优先考虑,因为链接是在加载时完成的,您的函数是一个内在函数,并且编译器在关闭已编译的.o 之前还没有解析引用。这使得动态链接器在共享对象加载时选择其中一个函数。

当没有共享二进制文件时,sqrt() 函数确实位于存储在libm.a 中的sqrt.o 文件中,并且链接器仅从存档中选择了解析某些未解析引用的二进制文件,因为在这种情况下,libm.a 中的 sqrt.o 不应该被包括在内,而是应该引用您的 sqrt()

顺便说一句,编译器如何处理 sqrt() 函数也存在问题,因为编译器接受支持所谓的内在函数(就像 sqrt() 一样)它们通常会优先考虑,所以,你不知何故建议不要将您的函数命名为内在函数。这方面在 FORTRAN 中非常扩展,直到现在还没有成为问题。编译器对内部函数的处理与其他函数完全不同......您会看到其他库函数不会发生这种情况。

如果您尝试将sqrt() 定义为不同的函数(例如,平方根的unsigned sqrt(unsigned n); 整数版本),您将看到这个东西作为编译器的警告出现,编译器将发出警告提示你重命名你的函数,这样你就不会使用与固有函数相同的名称。我没有深入挖掘,但不知何故,编译器在任何定义之前就知道某些函数的存在是#included。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-08-28
    • 2015-05-19
    • 1970-01-01
    • 2012-09-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多