本文内容基于《CSAPP》第7章,只是符号解析的一部分,从使用的角度阐述了静态库的由来和使用,仅仅是个人见解,可能从编译的角度看有不严谨的地方,如发现错误,还请指正,谢谢!
1 静态库
首先我们要知道,链接器将一组可重定位目标文件链接起来可以组成一个可执行文件,如
$ ld -o prog ./a.o ./b.o
但对于一些基础的操作,如C标准库中提供的printf、scanf、rand等一些列常用的函数,如果每次编译,我们都要操作带有这些函数的可重定位目标文件,那么一次简单的编译过程就会变成下面这样:
$ gcc -o a.out main.c /usr/lib/printf.o /usr/lib/scanf.o /usr/lib/rand.o ...
这样一来,不仅每次都要编写冗长的命令行,而且程序员还必须维护一个包含所需的源文件或目标文件的文件夹。
但实际上,我们在编译我们的程序时,并没有考虑过这样的问题,对于一个仅仅使用了标准库中函数的源文件而言,也并不需要程序员手动的进行额外的链接操作。如对于下面main.c这个源文件而言,
// main.c
#include<stdio.h>
int main()
{
printf("Hello World!");
return 0;
}
我们只需要简单的执行
$ gcc -o a.out main.c
这是因为,标准库中的函数都被编译成了独立的目标模块,然后相关模块会被封装成一个单独的静态库文件,如libc.a包含了C标准库中的标准I/O、字符串操作等函数,libm.a包含了C标准库中的整数数学函数,在执行链接操作时,编译器的驱动程序会将这些标准静态库传送给链接器,链接器会从中选择适当的模块同我们自己编写的目标模块(main.o)链接起来得到可执行文件。
在Linux系统中,静态库以一种称为存档(archive)的文件格式存储,后缀名.a,它由一个头和一系列的目标模块构成,头负责描述每个成员目标模块的位置和大小。
2 使用静态库
既然有标准库,那我们也可以把自己编写的函数、全局变量、宏等封装成静态库。
例如我们实现两个自定义的整型操作函数,分别定义在下面两个源文件中,
// add.c
int add(int a, int b){
return a+b
}
// sub.c
void sub(int a, int b){
return a-b;
}
创建静态库需要使用AR工具,使用以下命令:
$ gcc -c add.c sub.c
$ ar rcs libcal.a add.o sub.o
如此便得到了一个静态库libcal.a,在源文件中引用,即可使用静态库中定义的符号(非static函数、全局变量等)。
// main2.c
#include "cal.h"
int main()
{
int a = 0, b = 3, c = 0;
c = add(a, b);
printf("%d", c);
return 0;
}
编译该源文件,
$ gcc -c main2.c
$ gcc -static -o prog2c main2.o
或者等价地使用,
$ gcc -c main2.c
$ gcc -static -o prog2c main2.o -L. -lcal
链接器运行时,它就会判定main2.o引用了add.o定义的add符号,所以复制add.o到可执行文件,此外,他也会从/usr/lib/libc.a中复制printf所在的目标文件到可执行文件。
3 链接器如何使用静态库来解析引用
命令行上库和目标文件的顺序非常重要,如果我们对上一条命令做一些小小的改动,使之变为
$ gcc -static -o prog2c ./libcal.a main2.o
这条命令的执行就会报错“undefined reference to 'add'”,之所以出现这样的情况,是链接器解析外部引用的方式导致的。
链接器是按照命令行上从左到右的顺序来扫描文件的,在扫描文件时,链接器会维护三个集合:E(这个集合中的文件会被合并起来形成可执行文件)、U(未解析的符号)以及D(在前面输入文件中已定义的符号集合),三个集合初始为空。
- 对于命令行上的每个文件f,链接器会首先判断这一文件是目标文件还是静态库文件。若该文件是一个目标文件,则放入E中,并修改U和D来反映f中的符号定义和引用。
- 但如果f是一个静态库文件,那么链接器就试图对U中未解析的符号和f的成员所定义的符号进行匹配。如果f中的某一成员m定义了一个符号来解析U中的一个引用,那么就将m加入E中,再相应地修改U和D中的内容来反映m中的符号定义和引用,对f中的所有成员逐个进行匹配操作直至U和D不再发生变化,连接器便开始处理下一个文件。
- 当链接器扫描完所有命令行中的文件后,若U是空的,那么连接及就会合并和重定位E中的文件,得到一个可执行文件;否则,链接器就会报错并终止。
现在,是不是理解了上面的错误了呢,链接器扫描到libcal.a时,U中尚是空的,故直接继续扫描后面的main2.o,然后,main2.o中的add符号未解析,被加入到U中,随后,结束扫描,U中非空,链接器报错。
需要注意的是,库和库之间也可能存在依赖关系,故使用多个库时要注意其先后顺序,若存在相互依赖的关系,则可以选择在命令行上重复库,如下面一条命令中,libx.a调用了liby.a中的函数,liby.a又调用了libx.a中的函数,
$ gcc foo.c libx.a liby.a libx.a
当然,把两者合并为单独的一个静态库也不失为一种好方法。