18

好吧,直到今天早上,我都对这些术语感到困惑。我想我有不同的地方,希望如此。

首先,令人困惑的是,由于预处理器已经将头文件包含在包含函数的代码中,链接器将哪些库函数链接到汇编器/编译器生成的目标文件?部分混淆主要是由于我对头文件和库之间的区别一无所知。

经过一番谷歌搜索和堆栈溢出(这是术语吗?:p),我收集到头文件主要包含函数声明,而实际实现在另一个名为库的二进制文件中(我仍然不是 100%确定这一点)。

因此,假设在以下程序中:-

#include<stdio.h>
int main()
{
      printf("whatever");
      return 0;
}

预处理器将头文件的内容包含在代码中。编译器/编译器+汇编器完成它的工作,最后链接器将这个目标文件与另一个实际存储了printf()工作方式的目标文件结合起来。

我的理解正确吗?我可能会走得很远......所以你能帮帮我吗?

编辑:我一直想知道 C++ STL。它总是让我困惑它到底是什么,所有这些标题的集合还是什么?现在阅读回复后,我可以说 STL 是一个目标文件/类似于目标文件的东西吗?

而且,我想我可以在哪里读取函数的函数定义,比如pow()sqrt()。我会打开头文件,但什么也找不到。那么,库中的函数定义是不是二进制不可读的形式呢?

4

3 回答 3

23

AC 源文件经历两个主要阶段,(1) C 源代码由预处理器实用程序处理的预处理器阶段,该实用程序查找预处理器指令并执行这些操作,以及 (2) 处理后的 C 源代码的编译阶段。实际编译生成目标代码文件。

预处理器是一个进行文本操作的实用程序。它将包含可能包含预处理器指令的文本(通常是 C 源代码)的文件作为输入,并通过将找到的任何指令应用于文本输入以生成文本输出来输出文件的修改版本。

该文件不必是 C 源代码,因为预处理器正在执行文本操作。我已经看到 C Preprocssor 用于make通过允许将 preprossor 指令包含在 make 文件中来扩展实用程序。带有 C Preprocessor 指令的 make 文件通过 C Preprocessor 实用程序运行,然后将生成的输出输入到makemake 目标的实际构建中。

库和链接

库是包含各种功能的目标代码的文件。这是一种在将多个源文件编译成单个文件时将其输出打包的方法。很多时候,库文件与头文件(包含文件)一起提供,通常带有 .h 文件扩展名。头文件包含函数声明、全局变量声明以及库所需的预处理器指令。因此,要使用该库,您需要包含使用该#include指令提供的头文件并链接到该库文件。

库文件的一个很好的特性是您提供的是源代码的编译版本,而不是源代码本身。另一方面,由于库文件包含已编译的源代码,因此用于生成库文件的编译器必须与用于编译您自己的源代码文件的编译器兼容。

常用的库有两种。第一种和较旧的类型是静态库。第二个也是最近的动态库(Windows 中的动态链接库或 DLL 和 Linux 中的共享库或 SO)。两者之间的区别在于库中的函数绑定到使用库文件的可执行文件时。

链接器是一个实用程序,它使用各种目标文件和库文件来创建可执行文件。当在 C 源文件中使用外部或全局函数或变量时,使用一种标记告诉链接器需要在该点插入函数或变量的地址。

C 编译器只知道它编译的源代码中的内容,而不知道其他文件(例如目标文件或库)中的内容。所以链接器的工作是获取各种目标文件和库,并通过用实际连接替换标记来建立部件之间的最终连接。因此,链接器是将各种组件“链接”在一起的实用程序,将目标文件和库中的全局函数或变量的标记替换为指向为该全局函数或变量生成的实际目标代码的链接。

在链接器阶段,静态库与动态或共享库之间的差异变得明显。当使用静态库时,库的实际目标代码包含在应用程序可执行文件中。当使用动态或共享库时,应用程序可执行文件中包含的目标代码是在应用程序运行时找到共享库并与之连接的代码。

在某些情况下,相同的全局函数名称可能会在几个不同的目标文件或库中使用,因此链接器通常只会使用它遇到的第一个并发出有关找到其他对象的警告。

编译链接总结

所以一个 C 程序的编译和链接的基本过程是:

  • 预处理器实用程序生成要编译的 C 源代码

  • 编译器将 C 源代码编译成目标代码,生成一组目标文件

  • 链接器将各种目标文件以及任何库链接到可执行文件中

以上是基本过程,但是当使用动态库时,它可能会变得更加复杂,特别是如果正在生成的应用程序的一部分具有它正在生成的动态库。

装载机

还有应用程序实际加载到内存并开始执行的阶段。操作系统提供了一个实用程序,加载程序,它读取应用程序可执行文件并将其加载到内存中,然后启动应用程序运行。可执行文件的起始点或入口点在可执行文件中指定,因此在加载程序将可执行文件读入内存后,它将通过跳转到入口点内存地址来启动可执行文件运行。

链接器可能遇到的一个问题是,有时它在处理需要实际内存地址的目标代码文件时可能会遇到一个标记。但是,链接器不知道实际的内存地址,因为地址会根据应用程序在内存中的加载位置而有所不同。因此,链接器将其标记为加载程序实用程序在加载程序将可执行文件加载到内存并准备开始运行时修复的内容。

对于具有硬件支持的虚拟地址到物理地址映射或转换的现代 CPU,实际内存地址的问题很少成为问题。每个应用程序都加载到相同的虚拟地址,硬件地址转换处理实际的物理地址。然而,较旧的 CPU 或成本较低的 CPU,例如缺乏内存管理单元 (MMU) 硬件支持地址转换的微控制器,仍然需要解决这个问题。

入口点和 C 运行时

最后一个主题是 C 运行时和main()可执行入口点。

C 运行时是编译器制造商提供的目标代码,其中包含用 Cmain()编写的应用程序的入口点。函数是编写应用程序的程序员提供的入口点,但这不是加载程序看到的入口点。该main()函数在应用程序启动后由 C Runtime 调用,C Runtime 代码为应用程序设置环境。

C 运行时不是标准 C 库。C 运行时的目的是管理应用程序的运行时环境。标准 C 库的目的是提供一组有用的实用程序函数,以便程序员不必创建自己的函数。

当加载程序加载应用程序并跳转到 C 运行时提供的入口点时,C 运行时执行各种初始化操作,为应用程序提供适当的运行时环境。完成此操作后,C 运行时将调用该main()函数,以便应用程序开发人员或程序员创建的代码开始运行。当main()返回或exit()调用函数时,C 运行时执行清理和关闭应用程序所需的任何操作。

于 2012-08-29T15:25:38.463 回答
3

这是一个非常常见的混淆来源。我认为了解正在发生的事情的最简单方法是举一个简单的例子。暂时忘掉图书馆,考虑以下几点:

$ cat main.c
extern int foo( void );
int main( void ) { return foo(); }
$ cat foo.c
int foo( void ) { return 0; }
$ cc -c main.c
$ cc -c foo.c
$ cc main.o foo.o

该声明extern int foo( void )执行与库的头文件完全相同的功能。 foo.o正在执行库的功能。如果您理解了这个示例,以及为什么它们都cc main.c不起作用cc main.o,那么您就会理解头文件和库之间的区别。

于 2012-08-29T13:48:20.197 回答
2

是的,几乎正确。除了链接器不链接目标文件外,还链接库 - 在这种情况下,它是 C 标准库 (libc) 链接到您的目标文件。关于编译阶段+头文件和库之间的差异,您的其余假设似乎是正确的。

于 2012-08-29T12:24:05.407 回答