这个答案适用于链接程序(例如,C、C++),而不是解释程序(例如,shell 脚本、大多数字节码语言、解释器)。此处忽略字节码语言的灰色阴影。
链接器首先收集对象模块(此处称为 .o 文件)和命令行指向它的库,在其中构建所有提供和引用的全局名称的列表。到这个时候,实际的源代码已经被遗忘了。忽略稍后描述的调试信息,实际上只有全局变量和函数的名称与相关值一起保留在 .o 文件中。
目标代码跟踪 .o 文件的哪些部分是变量,哪些部分是可执行代码以及外部条目的名称和位置。一些语言 (C++) 跟踪参数签名,而另一些 (C) 则不跟踪。
保持简单,在构建的命令行中包含 .o 文件后,链接器的主要工作是查看这些 .o 文件以提取对所有外部的引用,然后搜索库以满足这些外部的要求。全部捆绑到您的可执行文件中,或者在执行期间加载动态库的情况下,将指向可加载模块的适当链接放入可执行文件中。
链接过程为链接器放入可执行文件的所有内容分配一个内存地址以驻留。链接器将所有这些连同其他元数据(例如文件标题和可选调试信息)一起放入可执行文件中,加载程序可以在程序运行时很好地提取这些信息。这通常分为可执行代码的程序段、具有与其相关联的初始值的程序变量存在时的数据段,以及没有特定值的变量存在的位置,它们获得二进制零的默认值。这些段提供了可执行文件的运行时布局。
一些基本的东西,比如函数的名称和位置,通常会保留在所有构建中,但这可能非常少,以至于在调试大多数故障时毫无价值,这导致开发人员在大多数 POSIX/ 开发过程中使用 -g Linus 类型编译器。
如果未告知链接器将其保留在构建中,则在 -g(或其他)下编译的 .o 模块的调试信息将被丢弃。此信息包括所有外部变量名称、函数、它们的地址等;您可以使用调试器看到的所有内容都包含在此处,并且大大增加了可执行文件的大小。这通常包括将函数的位置和其中的代码与原始源文件相关联。
链接器还确定执行的开始位置。一件小而关键的事情,不是初学者倾向于认为是代码运行的开始的“main()”类型函数,而是在语言库深处的某个地方,该语言库以可能模糊的方式自动包含在构建中。此启动代码确保成功执行所需的所有内容,例如准备处理 malloc()、使用继承的打开文件、设置环境变量、设置堆栈和堆等。一旦所有这些都完成了,main() 的代码就被用来启动用户的代码。虽然链接器与此初始化代码无关,也与程序退出或主程序返回时运行的最终完成代码无关,但链接器确实指向确保此启动代码存在,其中还包括在用户代码完成时正确退出.
链接器一旦构建完成,就排除了处理调用时加载的动态模块的可能性。如何处理这些在很大程度上取决于操作系统和链接的性质。有时可能会涉及链接器,有时它“只是”加载模块文件。
在我看来,作为链接器的关键合作伙伴,将可执行文件读入适当内存的“加载程序”将一些内存块和其他启动处理归零。虽然是关键合作伙伴,但它绝对是一个单独的计划。