我最近阅读了CSAPP,并对其中的编译系统部分有疑问。
现在我们有一个使用HelloWorld.c的示例(只需打印hello world)。该书说在预处理阶段,他们用此头文件的内容替换了“ #include”行。但是,当我打开stdio.h时,我发现只有printf()的声明,没有具体的实现。因此,在编译系统中,何时将引入printf()的特定实现?
书还说,在链接阶段,链接器(ld)链接了helloworld.o和printf.o。为什么链接器知道将我的目标文件链接到printf.o?在编译系统中,为什么要在第一步(预处理阶段)中声明此功能,而在最后一步(链接阶段)中链接具体的实现?
答案 0 :(得分:1)
实际上,过于简化:
.a
或.so
文件)。libc.so
具有printf
函数,该函数从库文件0xaabbccdd
中的字符编号libc.so
开始。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
-位于地址libc.so
的{{1}}中。因此,链接器将0xaabbccdd
替换为call printf
类指令。goto libs.so file to address 0xaabbccdd
将跳至指定位置的文件call printf
中。我上面编写的内容仅用于说明目的。
答案 1 :(得分:0)
为什么链接程序知道将我的目标文件链接到printf.o?
LD知道如何搜索和查找它们。您可以看到与男人ld.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
个文件。
答案 2 :(得分:0)
为什么链接程序知道将我的目标文件链接到printf.o
因为编译器在生成的内容(通常称为目标文件(.o))中注明了这一点。
为什么要在第一步中声明此功能...
要了解它。
...并在最后一步中链接具体的实现
因为不需要更早进行此操作。
答案 3 :(得分:0)
所有C和C ++标准都告诉您,您需要#include
给定的头文件以引入一些功能(在某些平台上甚至没有必要,尽管包含进来是一个好主意,从那时起,您正在编写便携式代码)。
这为编译器提供了很大的灵活性。
链接(如果有)将自动完成。请注意,某些功能甚至可能被硬编码到编译器本身中。
答案 4 :(得分:0)
默认情况下,库(包含printf的实现)每次都在C程序中链接。
通过包含标头,您只需在编译时(暂时)指定声明的函数的实现(在标头内部)在其他位置即可。然后在链接阶段,这些功能实现会在您的代码中“添加”。