链接器基础:
编译器一般由以下分程序组成:
- 编译驱动器(compiler driver):控制程序
- 预处理器
- 语法分析器
- 语义分析器
- 代码生成器
- 汇编器
- 优化器
- 链接器
编译器创建一个输出文件,包含了可重定地址的对象,这些对象是和源文件相对应的数据和机器指令
一个对象文件不是直接可执行的,需要首先被链接器处理。链接器找到main程序作为入口,将符号绑定到内存地址,合并所有的对象文件,然后把它们和库文件联合在一起生成可执行文件
动态链接和静态链接:
PC和更大型的系统相比在链接机制上有一个很大的区别。PC只提供很少的被称为BIOS例程的基础I/O服务,并放到固定的存储地址。而且不属于任何一个可执行文件的一部分。如果PC程序想要更多的复杂服务,这些服务可以作为库提供,而且链接器需要把这些库链接到需要的可执行文件上
UNIX系统以前也是这样,当需要链接一个程序的时候,这个库例程的一个副本就会嵌入到可执行文件中
一个新的更高级的方式被称为动态链接库:动态链接允许系统提供大量的库文件提供服务,而且程序不必将这些库的二进制文件作为可执行文件的一部分,而是在运行期间寻找这些服务
区别:
- 静态链接:库文件的副本被作为可执行文件的一部分
- 动态链接:可执行文件只有文件名,在运行时加载器使用这个文件名寻找库文件
将各模块整合到一起并为执行做准备有三个过程:链接编辑(link-editing)、加载(loading)、运行时链接(runtime linking)
- 静态链接的过程是:链接编辑->加载并执行
- 动态链接的过程是:链接编辑->加载->动态链接并执行
动态链接模式在执行的时候,运行时加载器会在main函数调用之前将对应的数据对象放到程序的地址空间中
只有在函数真正被调用的时候才解析外部函数,所以如果不调用外部函数,动态链接不会造成额外的消耗
动态链接的优点:代价是很小的运行时损耗,因为一些链接器的工作被推迟到运行时执行
- 因为库只会在需要的时候才被加载到程序中,动态链接生成的可执行文件要小得多,节省了磁盘空间和虚拟内存。静态链接如果想要避免将库文件副本嵌入到可执行文件中就只能把服务放到内核中,这会导致内核膨胀
- 所有链接到相同库的可执行文件将在运行时共享一份库文件的副本。系统内核保证映射到库文件中的库可以被所有程序访问到。这会带来更好的I/O和交换空间利用率,并且节约物理存储,提高系统整体吞吐率。如果是静态链接,每个可执行程序都将有一份库的副本
- 动态链接提供更好的版本控制,新的库版本安装之后,程序就会自动的直接寻找到新的库,而不必重新执行链接过程
- 有一个并不经常用到的好处就是,动态链接允许用户选择运行时执行那个库文件。可以选择不同的库文件版本,比如速度更快的、更节约存储空间的或者包含了更多的debug信息
动态链接库的问题:
因为程序是在运行时寻找库文件的,所以要指定文件名和文件路径。之后这个库文件的位置就不能改变了。而且如果在不同的机器上执行,必须有相同的文件路径,但是如果是系统标准库文件就不会有这个问题
动态链接的主要目的是充分利用ABI的好处,使软件不必随着每一次系统更新或库文件更新而重新编译
ABI:
我们有这样的约定:系统为程序提供接口,这个接口随着操作系统版本的发布也是稳定的。程序可以调用接口承诺的服务而不需要考虑他们是怎么实现的或者底层实现可能会改变。因为这个程序和服务之间的接口是通过可执行二进制库文件提供的,所以被称为应用程序二进制接口(Application Binary Interface ABI)
以前,应用程序供应商在每次库文件或操作系统更新的时候都需要重新链接他们的软件。ABI不必这样,它保证了软件不会被底层系统软件的更新影响到
静态链接是过时的方式。静态链接的主要风险是新版本的操作系统可能和绑定到可执行文件的系统库文件不兼容
命令:
- 创建动态链接库的命令:cc -o libxxx.so -G xxx.c
- 使用的命令:cc test.c -L/filepath1 -R/filepath2 -lxxx
- -L/filepath1作用是告诉链接器在链接期间去哪里找库文件
- -R/filepath2作用是告诉链接器在运行期间去哪里找库文件
-K pic编译选项:
作用是为库产生位置独立的代码
位置独立意味着所有的全局数据读写都是通过额外的一次间接引用,这样在想要为数据换位置的时候就只需要改变全局偏移表中的一个值
类似的,所有的函数调用都是通过间接引用程序链接表中的地址实现的,这段文本就可以通过改变偏移表轻易的迁移。所以当代码在运行时被映射的时候,运行时链接器就可以把这段代码放到任意有空间的位置,而这段代码本身不必改变
默认情况下,编译器不会执行PICode选项,因为额外的指针解引用会导致运行时速度稍微变慢。但是如果不使用PICode,生成的代码会绑定到固定的地址,这对可执行程序而言是无所谓的,但是对于一个共享的库效率会降低。因为运行时每次全局的引用都需要通过页修改来恢复,也就导致这个页不可共享。虽然运行时链接器总会恢复页引用,但是如果是位置独立的代码这项工作会简单很多
经验法则是总是使用PICode
位置独立的代码对于共享的库文件是非常有用的,因为每次获取都是把它映射到虚拟地址中(但是保持相同的物理地址)
一个相关的术语是纯代码,纯代码意味着可执行文件中只含有代码而没有静态的或初始化数据。所以在被任意程序执行的时候都不需要被修改就可以执行,它从栈或者其他的代码段取数据。纯代码段是可以共享的。如果使用PICode,使用纯代码是最好的
关于链接库文件的五点:
- 动态链接库的命名:libname.so;静态链接库的命名:libname.a,name是文件名
- 通过-lname选项告知编译器链接到哪个文件。如库文件名是 libthread.so 那么编译选项就是 -lthread——去掉"lib"并加上"l"
- 编译器会到特定的目录下去寻找库文件
- 编译命令 -Lpathname 会告诉编译器去哪个目录下寻找库文件
- 环境变量LD_LIBRARY_PATH和LD_RUN_PATH也被用来提供这个信息
- 但是使用环境变量被官方认为是不好的,因为安全性、性能和编译/执行独立性等问题
- 通过包含的头文件确定使用了那些库
- 每个头文件代表了一个必须包含的库文件。
- 问题:
- 头文件名和库文件名通常不一致
- 一个库文件可能包含了可以满足多个头文件原型声明的例程。比如,<string.h><stdio.h><time.h>都由libc.so提供
- 如何将一个符号匹配到库文件:如果程序有一个符号未定义的错误,到库文件目录下执行:
-
% foreach i (lib?*) ? echo $i ? nm $i | grep symble | grep -v UNDEF ? end
找到缺失的库文件,并在编译时加上对它的动态链接
-
- 从静态链接库中提取符号将比从动态链接库中收到更多的限制
- 静态链接和动态链接在语义上有一个很大的区别:
- 动态链接,所有库的符号都会加载到输出文件的虚拟地址空间中,在链接过程中所有的符号对所有其他文件都是可见的
- 静态链接只会在静态库文件加载的时候在里面寻找加载器目前的未定义符号
- 也就是说,由于静态库在编译命令中是从左到右解析的,所以编译命令中的顺序会造成很大的不同。如果相同的符号在不同的文件中有不同的定义,那么编译命令中这两个文件的不同相对顺序就会产生不同的结果。如果编译命令中自己的代码是在静态库文件之后,那么在解析静态库文件的时候就不会有任何未定义的符号,就不会解析任何东西
- 而math库,由于要尽可能的提高效率,被设计为静态库,如果采用这样的语句:cc -lm main.c,就会产生错误,应使用:cc main.c -lm
- 静态链接和动态链接在语义上有一个很大的区别:
代码植入(Interpositioning):
指通过使用相同的函数名,用户用自己定义的函数代替库函数。问题是,不仅用户的代码会调用用户实现的版本,系统例程也会调用用户自己实现的版本,而且编译器不会对重新定义库函数产生任何错误
举例:
用户实现:
main(){
mktemp();
getwd();
}
C库:
mktemp(){...}
getwd(){...mktemp()...}
如果用户实现了自己的mktemp()定义,那么用户代码中的main()和C库中的geted()都会调用用户实现的版本
出现过的一个BUG就是C库的mktemp()需要一个参数,而用户实现的版本需要三个。但是C库中的getwd()调用的时候是按照C库的版本调用的,只会传递一个参数。但是用户版本会尝试进行三个参数的解析,这会根据栈中的值产生out of memory错误