链接
链接(linking)是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载(或被拷贝)到存储并执行。链接可以执行于编译时(compile time),也就是在源代码被翻译成机器代码时;也可以执行于加载时(load time),也就是在程序被加载器(loader)加载到存储器并执行时;甚至执行于运行时(run time),由应用程序来执行。
静态链接
Unix的静态链接器(static linker)ld,以一组可重位目标文件和命令行参数作为输入,生成一个完全链接的可以加载和运行的可执行目标文件作为输出。输入的可重定位目标文件由各种不同的代码和数据节(section)组成。指令在一个节中,初始化的全局变量在另一个节中,而未初始化的变量又在另外一个节中。
为了构造可执行文件,链接器必须完成两个主要任务:
1、符号解析(symbol resolution)。目标文件定义和引用符号。符号解析的目的是将每个符号引用刚好和一个符号定义联系起来。
2、重定位(relocation)。编译器和汇编器生成从地址0开始的代码和数据节。链接器通过把每个符号定义与一个存储器位置联系起来,然后修改所有对这些符号的引用,使得它们指向这个存储器位置,从而重定位这些节。
目标文件
目标文件有三种形式:
1、可重定位目标文件。包含二进制代码和数据,其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件。
2、可执行目标文件。包含二进制代码和数据,其形式可以被直接拷贝到存储器并执行。
3、共享目标文件。一种特殊类型的可重定位目标文件,可以在加载或者运行地被动态地加载到存储器并链接。
编译器和汇编器生成可重定位目标文件(包括共享目标文件)。链接器生成可执行目标文件。从技术上来说,一个目标模块(object module)就是一个字节序列,而一个目标文件(object file)就是一个存放在磁盘文件中的目标模块。
可重定位目标文件
一个典型的ELF可重定位目标文件的格式:
.text:已编译程序的机器代码。
.rodata:只读数据,比如printf语句中的格式串和开关语句的跳转表。
.data:已初始化的全局C变量。
.bss:未初始化的全局C变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。
.symtab:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。
.rel.text:一个.text节中位置的列表,当链接器把这个目标文件和其他文件结合时,需要修改这些位置。
.rel.data:被模块引用或定义的任何全局变量的重定位信息。
.debug:一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。只有以-g选项调用编译驱动程序时才会得到这张表。
.line:原始C源程序中的行号和.text节中机器指令之间的映射。
.strtab:一个字符串表,其内容包括:.symtab和.debug节中的符号表,以及节头部中的节名字。字符串表就是以null结尾的字符串序列。
符号和符号表
每个可重定位目标模块m都有一个符号表,它包含m所定义和引用的符号的信息。在链接器的上下文中,有三种不同的符号:
1、由m定义并能被其他模块引用的全局符号。全局链接器符号对应于非静态的C函数以及被定义为不带C static属性的全局变量。
2、由其他模块定义并被模块m引用的全局符号。这些符号称为外部符号(external),对应于定义在其他模块中的C函数和变量。
3、只被模块m定义和引用的本地符号。有的本地链接器符号对应于带static属性的C函数和全局变量。
符号解析
链接器解析符号引用的方法是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义联系起来。
链接如何解析多重定义的全局符号
在编译时,编译器向汇编器输出每个全局符号,或者是强或者弱的符号,而汇编器会把这个信息隐含地编码在可重定位目标文件的符号表里。函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。
根据强弱符号的定义,Unix链接器使用下面的规则来处理多重定义的符号(其它系统应该也适用的,I think,而且还要看编译器):
规则1:不允许有多个强符号。
规则2:如果有一个强符号和多个弱符号,那么选择强符号。
规则3:如果有多个弱符号,那么从这些弱符号中任意选择一个。
与静态库链接
所有的编译系统都提供一种机制,将所有相关的目标模块打包成为一个单独的文件,称为静态库(static library)(Linux下是存档文件,Windows下是lib),它可以用做链接器的输入。当链接器构造一个输出的可执行文件时,它只拷贝静态库里被应用程序引用的目标模块。
在Unxi系统中,静态库以一种称为存档(archive)的特殊文件格式存放在磁盘中。存档文件是一组连接起来的可重定位目标文件的集合,有一个头部用来描述每个成员目标文件的大小和位置。存档文件名由后缀.a标识。我们可以用AR工具创建静态库。
链接时加上-static参数告诉编译器驱动程序,链接器应该构建一个完全链接的可执行目标文件,它可以加载到存储器并执行,在加载时无需更进一步的链接。
链接器如何使用静态库来解析引用
在符号解析的阶段,链接器从左到右按照它们在编译器驱动程序命令行上出现的相同顺序来扫描可重定位目标文件和存档文件。(驱动程序自动将命令行中所有的.c文件翻译为.o文件。)在这次扫描中,链接器维持一个可重定位目标文件的集合E(这个集合中的文件会被合并起来形成可执行文件),一个未解析的符号(即引用了但是尚未定义的符号)集合U,以及一个在前面输入文件中已定义的符号集合D。初始时U和D都是空的。
/*每个C程序中启动例程crtl.o的伪代码*/
0x080480c0 <_start> : /*Entry point in .text*/
call __libc_init_first /*Startup code in .text*/
call _init /*Startup code in .init*/
call atexit /*Startup code in .text*/
call main /*Application main routine*/
call _exit /*Returns control to OS*/
/*Control never reaches here*/
操作系统的加载程序会执行下面的步骤:
加载程序先为新的进程创建一个虚拟地址空间,并将可执行模块映射到新进程的地址空间中。加载程序接着解析可执行模块的导入段。对导入段中列出的每个DLL,加载程序会在用户的系统中该DLL模块进行定位,并将该DLL映射到进程的地址空间中。注意,由于DLL模块可以从其他DLL模块中导入函数和变量,因此DLL模块可能有自己的导入段并需要并它所需DLL模块映射到进程的地址空间中。我们可以看到,初始化一个进程可能会耗费很长的时间。
还有这篇Blog也提到了这个问题:关于动态链接与静态链接
所以,关于某公司的笔试题
如下关于编译链接的说法错误的是()
A、编译优化会使得编译速度变慢
B、预编译头文件可以优化程序的性能
C、静态链接会使得可执行文件偏大
D、动态链接库会使进程启动速度偏慢
我觉得不应该选D,而应该选B。虽然网上大部人都选了D。关于B选项,引用Jerry19880126的话就是“有B的表述不严谨了,预编译头只能优化编译的性能,但不能优化程序的性能。”
从应用程序中加载和链接共享库
另外一种共享方式就是“显式链接”(呵呵,在Window中的说法)
前面已经讨论了在应用程序执行之前,即应用程序被加载时,动态链接器加载和链接共享库的情景。然而,应用程序还可能在它运行时要求动态链接器加载和链接任意共享库,而无需在编译时链接那些库到应用中。
其中几个关键函数:
#include
//返回:若成功则为指向句柄的指针,若出错则为NULL
void *dlopen(const char *filename,int flag) ;
//返回:若成功则为指向符号的指针,若出错则为NULL
void *dlsym(void *handle,char *symbol) ;
//返回:若成功则为0,若出错则为-1
int dlclose(void *handle) ;
//返回:如果前面对dlopen、dlsym或dlclose的调用失败,则为错误消息,如果前面的调用成功,则为NULL
const char *dlerror(void) ;