一个C源文件经历两个主要阶段:(1) 预处理阶段,在此阶段中,C源代码通过预处理器工具进行处理,该工具查找预处理指令并执行这些操作;(2) 编译阶段,在此阶段中,处理后的C源代码实际上被编译以生成目标代码文件。
预处理器是一个执行文本操作的实用程序。它将包含可能包含预处理指令的文本(通常是C源代码)的文件作为输入,并输出一个修改后的版本,通过应用任何找到的指令来生成一个文本输出。
文件不必是C源代码,因为预处理器执行文本操作。我曾看到C预处理器被用于扩展make实用程序,通过允许在make文件中包括预处理器指令。带有C预处理器指令的make文件通过C预处理器实用程序运行,生成的输出然后被馈入make以执行目标构建。
库和链接
库是包含各种函数的目标代码文件。这是一种将多个源文件的输出编译成单个文件的方式。通常,伴随着库文件的提供一起提供了一个头文件(包含文件),通常具有.h文件扩展名。头文件包含函数声明、全局变量声明以及库所需的预处理器指令。因此,要使用库,您需要使用#include指令包含提供的头文件,并与库文件链接。
库文件的一个好处是,您提供了源代码的已编译版本,而不是源代码本身。另一方面,由于库文件包含已编译的源代码,用于生成库文件的编译器必须与用于编译自己的源代码文件的编译器兼容。
通常使用两种类型的库。第一种和较旧的类型是静态库。第二种和较新的类型是动态库(在Windows中为Dynamic Link Library或DLL,在Linux中为Shared Library或SO)。两种类型之间的区别在于将库中的函数绑定到使用库文件的可执行文件时的时间点。
链接器是一个实用程序,它将各种目标文件和库文件组合起来创建可执行文件。当在C源文件中使用外部或全局函数或变量时,使用一种标记告诉链接器需要在那个点插入函数或变量的地址。
C编译器只知道它编译的源文件中的内容,而不知道其他文件(例如目标文件或库)中的内容。因此,链接器的工作是获取各种对象文件和库并通过替换标记与实际连接线之间建立最终连接。因此,链接器是一个“链接”各种组件的实用程序,它用实际为该全局函数或变量生成的对象代码的链接替换对象文件和库中全局函数或变量的标记。在链接器阶段,静态库和动态或共享库之间的区别变得明显。当使用静态库时,库的实际目标代码包含在应用程序可执行文件中。当使用动态或共享库时,应用程序可执行文件中包含的对象代码是用于查找共享库并在运行应用程序时连接它的代码。
在某些情况下,相同的全局函数名称可能在几个不同的目标文件或库中使用,因此链接器通常只使用它遇到的第一个,并发出有关其他找到的警告。
编译和链接C程序的基本过程如下:
1. 预处理器生成要编译的C源代码
2. 编译器将C源代码编译成目标代码生成一组目标文件。
3. 链接器将各种目标文件以及任何库链接到可执行文件中。
上述是基本过程,但是当使用动态库时,它可能会变得更加复杂,尤其是如果正在生成的应用程序的一部分具有它正在生成的动态库。
还有一个阶段是当应用程序实际加载到内存并开始执行。操作系统提供了一个实用程序——加载器,它读取应用程序可执行文件并将其加载到内存中,然后启动应用程序运行。可执行文件的起始点或入口点在可执行文件中指定,因此在加载器将可执行文件读入内存后,它将通过跳转到入口点内存地址开始运行可执行文件。
链接器可能会遇到的一个问题是,有时它在处理目标代码文件时可能会遇到需要实际内存地址的标记。然而,链接器不知道实际的内存地址,因为该地址将根据应用程序加载到内存的位置而变化。因此,当加载器将可执行文件加载到内存并准备好启动它运行时,链接器将该标记标记为需要修复的内容。
最后一个主题是C Runtime和main()以及可执行文件的入口点。
C Runtime是编译器制造商提供的包含用C编写的应用程序的入口点的目标代码。main()函数是由编写应用程序的程序员提供的入口点,但这不是加载器看到的入口点。在应用程序启动之后,C Runtime代码调用main()函数,并设置应用程序的环境。
C Runtime并不是标准的C库。C运行时库的目的是管理应用程序的运行环境。标准C库的目的是提供一组有用的工具函数,使程序员不必自己创建。
当加载器加载应用程序并跳转到C Runtime提供的入口点时,C Runtime执行所需的各种初始化操作,以为应用程序提供适当的运行时环境。完成此操作后,C Runtime调用main()
函数,使应用程序开发人员或程序员创建的代码开始运行。当main()
返回或调用exit()
函数时,C Runtime执行任何需要清理和关闭应用程序的操作。