背景-链接器
静态链接库时程序员迷惑的源头之一。原因是Linux 链接器使用它们解析外部引用的方式。
在符号解析阶段,链接器从左到右按照它们在编译器驱动程序命令行上出现的顺序来扫描可重定位目标文件和存档文件。
(驱动程序自动将命令行中所有的.c
文件翻译为.o
文件。在这次扫描中,链接器维护一个可重定位目标文件的集合E
(这个集合中的文件会被合并起来形成可执行文件),一个未解析的符号(即引用了但是尚未定义的符号)集合U
,以及一个在前面输入文件中已定义的符号集合 D
。初始时,E
、U
和D
均为空。
- 对于命令行上的每个输入文件
f
,链接器会判断是一个目标文件还是一个存档文件(静态库)。如果f是一个目标文件,那么链接器把广添加到E
,修改U
和D
来反映f
中的符号定义和引用,并继续下一个输人文件。 - 如果
f
是一个存档文件,那么链接器就尝试匹配U
中未解析的符号和由存档文件成员定
义的符号。如果某个存档文件成员m
,定义了一个符号来解析U
中的一个引用,那么就
将m
加到E
中,并且链接器修改U
和D
来反映m
中的符号定义和引用。对存档文件中
所有的成员目标文件都依次进行这个过程,直到U
和D
都不再发生变化。此时,任何不
包含在E
中的成员目标文件都简单地被丢弃,而链接器将继续处理下一个输入文件。 - 如果当链接器完成对命令行上输人文件的扫描后,
U
是非空的,那么链接器就会输出一
个错误并终止。否则,它会合并和重定位E
中的目标文件,构建输出的可执行文件。
不幸的是,这种算法会导致一些令人困扰的链接时错误,因为命令行上的库和目标文件的顺序非常重要。在命令行中,如果定义一个符号的库出现在引用这个符号的目标文件之前,那么引用就不能被解析,链接会失败。
问题描述
1.1 示例
源程序如下:
#include <iostream>
#include <mpi.h>
using namespace std;
int main(){
int provided = -1;
MPI_Init_thread(NULL,NULL,MPI_THREAD_SINGLE,&provided);
}
执行指令如下:
g++ -I/usr/include/x86_64-linux-gnu/openmpi -L/lib/x86_64-linux-gnu -lmpi -lmpi_cxx mpi_init_thread.cpp
1.2 问题
1.3 描述
在处理mpi_init_thread.cpp文件时,U
是空的,所以没有mpi
中的成员目标文件添加到E
。因此,对于MPI_Init_thread
的引用是绝不会被解析的,所以链接器产生一条错误信息并终止。
应对方式
2.1 解决方式
关于库的一般准则是将它们放在命令行的结尾。
- 如果各个库的成员是相互独立的(也就是说没有成员引用另一个成员定义的符号),那么这些库就可以以任何顺序放置在命令行的结尾处。
- 另一方面,如果库不是相互独立的,那么必须对它们排序,使得对于每个被存档文件的成员外部引用的符号
s
,在命令行中至少有一个s
的定义是在对s
的引用之后的。比如,假设foo.c
调用libx.a
和libz.a
中的函数,而这两个库又调用liby.a
中的函数。那么,在命令行中libx.a
和libz.a
必须处在liby.a
之前:
gcc foo.c libx.a libz.a liby.a
2.2 解决案例
将源程序mpi_init_thread.cpp
放到命令行靠前位置。
g++ mpi_init_thread.cpp -I/usr/include/x86_64-linux-gnu/openmpi -L/lib/x86_64-linux-gnu -lmpi -lmpi_cxx
2.3 拓展
2.3.1 依赖问题
如果需要满足依赖需求,可以在命令行上重复库。比如,假设·foo.c
调用 libx.a
中的函数,该库又调用 liby.a
中的函数,而 liby.a
又调用 libx.a
中的函数。
2.3.2 解决方案
libx.a
必须在命令行上重复出现:
gcc foo.c libx.a liby.a libx.a
- 另一种方法是:我们可以将
libx.a
和liby.a
合并成一个单独的存档文件。
[1] RANDALE.BRYANT,DAVIDR.O‘HALLARON.深入理解计算机系统[M].机械工业出版社,2011.