本文内容基于《CSAPP》第7章,只是符号解析的一部分,从使用的角度阐述了静态库的由来和使用,仅仅是我的看法,可能从编译的角度看有不严谨的地方,如发现错误,还请指正,谢谢!程序员
首先咱们要知道,连接器将一组可重定位目标文件连接起来能够组成一个可执行文件,如shell
$ 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; }
咱们只须要简单的执行code
$ gcc -o a.out main.c
这是由于,标准库中的函数都被编译成了独立的目标模块,而后相关模块会被封装成一个单独的静态库文件,如libc.a包含了C标准库中的标准I/O、字符串操做等函数,libm.a包含了C标准库中的整数数学函数,在执行连接操做时,编译器的驱动程序会将这些标准静态库传送给连接器,连接器会从中选择适当的模块同咱们本身编写的目标模块(main.o)连接起来获得可执行文件。字符串
在Linux系统中,静态库以一种称为存档(archive)的文件格式存储,后缀名.a,它由一个头和一系列的目标模块构成,头负责描述每一个成员目标模块的位置和大小。编译器
既然有标准库,那咱们也能够把本身编写的函数、全局变量、宏等封装成静态库。数学
例如咱们实现两个自定义的整型操做函数,分别定义在下面两个源文件中,io
// 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所在的目标文件到可执行文件。
命令行上库和目标文件的顺序很是重要,若是咱们对上一条命令作一些小小的改动,使之变为
$ gcc -static -o prog2c ./libcal.a main2.o
这条命令的执行就会报错“undefined reference to 'add'”,之因此出现这样的状况,是连接器解析外部引用的方式致使的。
连接器是按照命令行上从左到右的顺序来扫描文件的,在扫描文件时,连接器会维护三个集合:E(这个集合中的文件会被合并起来造成可执行文件)、U(未解析的符号)以及D(在前面输入文件中已定义的符号集合),三个集合初始为空。
如今,是否是理解了上面的错误了呢,连接器扫描到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
固然,把二者合并为单独的一个静态库也不失为一种好方法。