【问题标题】:In the compilation system, how does linker(ld) know who to link myprogram.o to?在编译系统中,linker(ld) 如何知道将 myprogram.o 链接到谁?
【发布时间】:2019-01-21 16:20:34
【问题描述】:

我最近阅读了CSAPP,对其中的编译系统部分有一些疑问。

现在我们有一个使用 HelloWorld.c 的示例(只需打印 hello world)。书中说在预处理器阶段,他们用这个头文件的内容替换了“#include”行。但是当我打开stdio.h时,发现里面只有一个printf()的声明,并没有具体的实现。那么在编译系统中,什么时候会引入printf()的具体实现呢?

这本书还说,在链接阶段,链接器(ld)链接了 helloworld.o 和 printf.o 。为什么链接器知道将我的目标文件链接到 printf.o?在编译系统中,为什么在第一步(预处理阶段)声明这个函数,在最后一步(链接阶段)链接具体实现?

【问题讨论】:

  • printf 是标准库的一部分,始终链接(除非另有说明)
  • 链接器默认链接一些库,例如libc.solibdl.so

标签: c++ c compiler-construction


【解决方案1】:

实际上,过于简单化了:

  • 您可以将函数编译到库中(例如,.a.so 在 unix 上的文件)。
  • 库有一个函数体(汇编指令)和一个函数名。前任。库libc.so 具有printf 函数,该函数从库文件libc.so 中的字符号0xaabbccdd 开始。
  • 你想编译你的程序。
  • 您需要知道printf 接受哪些参数。需要int 吗?需要char *吗?需要uint_least64_t吗?它在头文件中 - int printf(const char *, ...);。头文件告诉编译器如何调用函数(函数接受什么参数以及返回什么类型)。请注意,每个.c 文件都是单独编译的。
  • 函数声明(函数接受什么参数以及返回什么)没有存储在库文件中。它存储在标题中(仅)。该库具有函数名称(仅printf)和编译的函数体。标头有 int printf(const char *, ...); 没有函数体。
  • 您编译您的程序。编译器生成代码,以便将具有适当大小的参数推入堆栈。并且从堆栈中,您的代码获取从函数返回的变量。现在您的程序被编译成类似于push pointer to "%d\n" on the stack; push some int on the stack; call printf; pop from the stack the returned "int"; rest of the instructions; 的程序集。
  • 链接器搜索你编译的程序,它看到call printf。然后它说:“哦,你的代码中没有printf 正文”。然后它在库中搜索printf,看看它在哪里。链接器遍历您链接程序的所有库,并在标准库中找到printf - 它位于地址0xaabbccddlibc.so 中。所以链接器用call printf 代替goto libs.so file to address 0xaabbccdd 类型的指令。
  • 在所有“符号”(即函数名、变量名)都被“解析”(链接器在某处找到它们)之后,您就可以运行程序了。 call printf 将跳转到文件libc.so 的指定位置。

我上面写的只是为了说明目的。

【讨论】:

  • 非常感谢您。这对我来说很清楚,对我来说已经足够了。 ofc 我以后会读相关的书。所以,再次感谢您。
【解决方案2】:

为什么链接器知道将我的目标文件链接到 printf.o

因为编译器在其生成的文件中记录了这一点,通常称为目标文件 (.o)。

为什么会在第一步声明这个函数...

了解它。

...并链接上一步的具体实现

因为没有必要早点这样做。

【讨论】:

    【解决方案3】:

    所有 C 和 C++ 标准都告诉您,您需要 #include 给定的头文件才能引入某些功能(在某些平台上,尽管包含是一个好主意,但从那时起您就可以编写可移植代码)。

    这为编译器提供了很大的灵活性。

    链接(如果有)将自动完成。请注意,某些函数甚至可能被硬编码到编译器本身中。

    【讨论】:

      【解决方案4】:

      默认情况下,库(包含 printf 的实现)每次都在您的 C 程序中链接。

      通过包含头文件,您只需在编译时(暂时)指定已声明函数的实现(在头文件内)在其他地方。稍后在链接阶段,这些函数实现会“添加”到您的代码中。

      【讨论】:

      • 感谢您的回答。这是否意味着不仅函数的声明,而且具体实现的位置都包含在头文件中?那么在链接阶段,他们知道哪些功能需要链接以及在哪里找到它们?
      【解决方案5】:

      为什么链接器知道将我的目标文件链接到 printf.o?

      LD 知道如何搜索和找到它们。可以看到with manld.so

      如果共享对象依赖项不包含斜线,那么它是 按以下顺序搜索:

      • 使用二进制文件的 DT_RPATH 动态部分属性中指定的目录(如果存在)并且 DT_RUNPATH 属性不存在 存在。不推荐使用 DT_RPATH。
      • 使用环境变量 LD_LIBRARY_PATH,除非可执行文件在安全执行模式下运行(见下文),其中 如果忽略此变量。
      • 使用二进制文件的 DT_RUNPATH 动态部分属性中指定的目录(如果存在)。仅搜索此类目录 找到 DT_NEEDED 所需的那些对象(直接依赖项) 条目并且不适用于这些对象的孩子,这必须 他们有自己的 DT_RUNPATH 条目。这与 DT_RPATH 不同, 它适用于搜索依赖关系树中的所有子项。
      • 来自缓存文件 /etc/ld.so.cache,其中包含先前在增强版中找到的候选共享对象的编译列表 库路径。但是,如果二进制文件与 -z nodeflib 链接 链接器选项,默认路径中的共享对象被跳过。共享 安装在硬件功能目录(见下文)中的对象是 优先于其他共享对象。
      • 在默认路径 /lib 中,然后是 /usr/lib。 (在某些 64 位架构上,64 位共享对象的默认路径是 /lib64, 然后是 /usr/lib64。)如果二进制文件与 -z nodeflib 链接 链接器选项,则跳过此步骤。

      在一个编译系统中,为什么要在第一步(预处理阶段)声明这个函数,在最后一步(链接阶段)链接具体实现?

      在编译阶段,您需要知道要链接到什么并进行相应的编译,因此它需要读取带有定义的.h 文件。在链接阶段,只需要.o文件。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-09-05
        • 1970-01-01
        相关资源
        最近更新 更多