1. 静态库
链接器读取一组可重定位目标文件,并把它们组合称为一个单一的可执行文件。可以通过将所有相关的可重定位目标文件打包成为一个单独的文件,这个文件就叫做静态库。在链接时,链接器只拷贝被程序引用的目标模块。减少了可执行文件在磁盘和存储器中的大小。
Unix系统中,静态库以一种称为存档的特殊文件格式存放在磁盘。例如在ubuntu上,在路径/usr/lib/x86_64-linux-gnu/libc.a就是一个静态库,它将ANSI C标准函数中的函数,将多个函数定义的模块打包后,成为一个单独的文件-libc.a。存档文件是一组连接起来的可重定位目标文件的集合,有一个头部用来描述每个成员目标文件的大小和位置。
2. 静态库的生成
有addvec.c和multvec.c两个函数模块
// addvec.c
void addvec(int *x, int * y, int * z, int n) {
int i;
for (i = 0; i < n; i++) {
z[i] = x[i] + y[i];
}
}
// multvec.c
void multvec(int * x, int * y, int * z, int n) {
int i;
for (i = 0; i < n; i++)
z[i] = x[i] * y[i];
}
将addvec.c和multvec.c编译为addvec.o和multvec.o文件,然后通过ar生成libvector.a静态库。
kernel@Ubuntu:~/Desktop/StaticLib$ gcc -c addvec.c multvec.c
kernel@Ubuntu:~/Desktop/StaticLib$ ar rcs libvector.a addvec.o multvec.o
参数r:在库中插入模块(替换)。当插入的模块名已经在库中存在,则替换同名的模块。如果若干模块中有一个模块在库中不存在,ar显示一个错误消息,并不替换其他同名模块。默认的情况下,新的成员增加在库的结尾处,可以使用其他任选项来改变增加的位置。
参数c:创建一个库。不管库是否存在,都将创建。
参数s:创建目标文件索引,这在创建较大的库时能加快时间。(如果不需要创建索引,可改成大写S参数;如果.a文件缺少索引,可以使用ranlib命令添加)
在main.c中负责调用函数,因此需要一个头文件去引用addvec和multvec。
// vector.h
extern void addvec(int *, int *, int *, int);
extern void multvec(int *, int *, int *, int);
// main.c
#include <stdio.h>
#include "vector.h"
int main(void) {
int x[2] = {1, 2};
int y[2] = {3, 4};
int z[2] = {0};
addvec(x, y, z, 2);
printf("%d %d\n", z[0], z[1]);
return 0;
}
为了创建可执行文件,需要链接main.o和libvector.a
kernel@Ubuntu:~/Desktop/StaticLib$ gcc -c main.c
kernel@Ubuntu:~/Desktop/StaticLib$gcc -static main.o ./libvector.a -o vector
kernel@Ubuntu:~/Desktop/StaticLib$ ./vector
4 6
-static参数告诉编译驱动程序,链接器应该构建一个完全链接的可执行目标,它可以加载到存储器并执行,在加载时无需更进一步的链接。
当连接器运行时,链接器解析addvec.o定义的addvec符号被main.o引用,所以它拷贝addvec.o到可执行文件,由于multvec.o的符号没有被引用,所以不会被拷贝到可执行文件,此外还有libc.a的printf.o也被main.o引用,所以会被拷贝到可执行文件。还有许多C运行时系统中的模块。
3. 链接器如何使用静态库来解析引用(libvector.a)
在符号解析阶段,链接器从左到右按照它们在编译器驱动程序命令行上出现的相同顺序来扫描可重定位目标文件main.o和存档文件libvector.a。在这扫描中,链接器维持一个可重定位目标文件的集合E,一个未解析的符号(即引用了尚未定义的符号)集合U,以及一个在前面输入文件中已定义的符号集合D。
(1)对于命令行上每个输入文件f,链接器会判断f是目标文件还是存档文件。如果是一个目标文件,那么链接器把f添加到E,此时E={mian.o},修改D和U来反映f中的符号定义和引用,U={addvec},并继续下一个输入文件。
(2)如果f是一个存档文件,那么链接器就尝试匹配U中未解析的符号和由存档文件成员定义的符号。在存档文件中定义了addvec符号,用来解析U中对addvec符号的引用,将addvec.o加入到E中,并且链接器修改D和U来反映addvec.o中的符号的定义和引用。对存档文件所有目标文件都反复进行上述过程,直到U和D不在发生变化,也就是找不到一个符号的定义与引用相联系。此时,任何不包含在E中的成员目标文件都将被丢弃,也就是在main.o未引用的符号,该符号定义的目标文件,此处为multvec.o,而链接器继续下一个输入文件。
(3)如果链接器完成对命令行上文件的扫描,U是非空,那么链接器会输出一个错误并终止,也就是一个未定义的引用。否则,它会合并和E中的目标文件,从而构建输出的可执行文件。
kernel@Ubuntu:~/Desktop/StaticLib$ gcc -static ./libvector.a main.o -o vector
main.o:在函数‘main’中:
main.c:(.text+0x41):对‘addvec’未定义的引用
collect2: error: ld returned 1 exit status
在命令行输入,将libvector.a 和main.o位置调换,就会出现上述错误。因为首先链接器判断f为存档文件,此时U为空集,所以libvector.a中的addvec.o定义的符号无法解析对addvec的引用。因此产生错误。因此最好在链接的时候,将存档文件放到最后。
如果库不是独立的,也就是一个成员引用另一个成员的符号,他们必须排序,使得对于每个被存档文件的成员外部引用的符号s,在命令行中至少有一个s的定义是在对s的引用之后。例如fun.c引用了libx.a和liby.a中的函数,而这两个库又调用libz.a的函数,则有:
gcc fun.o libx.a liby.a libz.a
如果需要满足依赖需求,可以在命令行上重复库。fun.c调用libx.a中的函数,该库又调用liby.a的函数,而liby.a又调用libx.a的函数,则有:
gcc fun.o libx.a liby.a libx.a