背景
工作时,同事遇到一个编译和链接相关的问题,我跟着一起研究了一下。很有意思的问题,我也做了一些实验,在此记录。
问题描述
问题源自CNTK相关的程序,但在这里,我把问题抽象出来进行描述。
假设,现在有:
- 一个静态链接库
liba.a
,定义了函数int GetInt()
,返回3
; - 一个动态链接库
libb.so
,定义了函数int GetDoubleInt()
,调用静态链接库liba.a
,返回GetInt() * 2
; - 一个静态链接库
liba2.a
,同样定义了函数int GetInt()
,但返回9999
。 - 主程序,调用动态链接库
libb.so
和静态链接库liba2.a
,分别输出GetInt()
和GetDoubleInt()
的返回值。
可以理解成,在实际生产环境中,
liba2.a
是为静态链接库liba.a
开发的新版本,保持了接口,修改了实现。主程序调用了一个用旧版库(
liba.a
)写成的库(libb.so
),同时自己又调用了新版库(liba2.a
)。
实验
依赖关系
首先,整理一下文件、目标文件和库之间的依赖关系。
目标文件 | 依赖项 |
---|---|
a.o | a.cpp |
a2.o | a2.cpp |
b.o | b.cpp |
main.o | main.cpp |
目标库 | 依赖项 |
---|---|
liba.a | a.o |
liba2.a | a2.o |
libb.so | b.o liba.a |
目标可执行文件 | 依赖项 |
---|---|
main | main.o liba2.a libb.so |
源程序
liba.a
和 liba2.a
的公共接口 (a.h
)
int GetInt();
liba.a
的源程序 (a.cpp
)
#include "a.h"
int GetInt()
{
return 3;
}
liba2.a
的源程序 (a2.cpp
)
#include "a.h"
int GetInt()
{
return 9999;
}
libb.so
的接口 (b.h
)
int GetDoubleInt();
libb.so
的源程序 (b.cpp
)
#include "a.h"
#include "b.h"
int GetDoubleInt()
{
return 2 * GetInt();
}
主程序 main
的源程序 (main.cpp
)
#include <iostream>
#include "a.h"
#include "b.h"
using namespace std;
int main()
{
cout << GetInt() << endl;
cout << GetDoubleInt() << endl;
return 0;
}
编译和链接
编译
我们直接来看一下我写好的Makefile中,编译(从源文件到目标文件的过程)相关的部分。
注意编译动态链接库的代码时,需要加上-fPIC
这个选项。PIC
即Position Independent Code,位置无关代码。
这个选项可以让生成的代码中涉及的各种引用地址均使用相对地址,例如跳转的目标地址等。只有使用位置无关代码,才能生成动态链接库。
a.o: a.cpp
g++ a.cpp -c -o a.o
a2.o: a2.cpp
g++ a2.cpp -c -o a2.o
b.o: b.cpp
g++ b.cpp -fPIC -c -o b.o
main.o: main.cpp
g++ main.cpp -c -o main.o
注:
-c
表示生成目标文件,-o xxx.o
指定目标文件名称为xxx.o
。
打包生成静态链接库
调用ar这个Linux中的Archive工具来打包静态链接库所包含的目标文件,其实静态链接库就是一个带符号索引的目标文件包。
liba.a: a.o
ar rcs liba.a a.o
liba2.a: a2.o
ar rcs liba2.a a2.o
链接生成动态链接库
链接liba
库到生成的动态链接库libb.so
。
libb.so: b.o liba.a
g++ -shared b.o -L. -la -o libb.so
注:
-shared
表示生成动态链接库,-L.
表示设置链接库查找目录为当前目录.
,-la
表示链接名为a
或liba
的库。
链接生成主程序
这里,主程序分别链接了liba2
和libb
两个库。
main: main.o liba2.a libb.so
g++ main.o -L. -Wl,-R. -la2 -lb -o main
注:
-Wl,-R.
设置程序运行时查找引用库的目录为当前目录.
,其中-Wl
表示后面的选项为传入链接器的选项。
生成全部
最后,在Makefile中加入all
这个生成目标:
all: main
执行
root@3b172fa2403e:/home/code# make
g++ main.cpp -c -o main.o
g++ a2.cpp -c -o a2.o
ar rcs liba2.a a2.o
g++ b.cpp -fPIC -c -o b.o
g++ a.cpp -c -o a.o
ar rcs liba.a a.o
g++ -shared b.o -L. -la -o libb.so
g++ main.o -L. -Wl,-R. -la2 -lb -o main
root@3b172fa2403e:/home/code# ./main
9999
19998
诶?结果为什么调用的都是liba2
库里定义的GetInt()
?
如果说,按我们的设定,主程序使用新版的liba2
,而主程序调用的libb
应该去使用旧版的liba
才对,也就是说,我们期待的输出结果应该是:
9999
6
换换顺序?再试一下。
现在,把链接生成主程序的链接库顺序颠倒一下(先链接libb
,再链接liba2
):
main: main.o liba2.a libb.so
g++ main.o -L. -Wl,-R. -lb -la2 -o main
执行结果:
3
6
是不是很神奇?结果又都变成调用liba
了。
不链接liba2
可以吗?
之所以会出现这种情况,多半是因为主程序使用的liba2
,和libb
使用的liba
发生了冲突。
因为liba
和liba2
是静态链接库,所以在编译动态链接库libb
的时候,liba
的代码实际上是也被链接进了libb
中。
那么,我们直接让主程序链接libb
会怎么样呢?是不是也可以成功链接,并且直接调用liba
?
main: main.o liba2.a libb.so
g++ main.o -L. -Wl,-R. -lb -o main
执行结果:
3
6
果然不出所料。
解释
事实上,因为libb
链接了静态链接库liba
,默认地,也就导出了所有liba
导出的符号。
我们可以借助nm
工具,查看库导出的符号都有什么:
root@3b172fa2403e:/home/code# nm liba.a
a.o:
0000000000000000 T _Z6GetIntv
root@3b172fa2403e:/home/code# nm liba2.a
a2.o:
0000000000000000 T _Z6GetIntv
root@3b172fa2403e:/home/code# nm -D libb.so
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
00000000000005c5 T _Z12GetDoubleIntv
00000000000005d2 T _Z6GetIntv
0000000000201028 B __bss_start
w __cxa_finalize
w __gmon_start__
0000000000201028 D _edata
0000000000201030 B _end
00000000000005e0 T _fini
00000000000004c8 T _init
可以看到,动态链接库libb.so
是包括了GetInt
和GetDoubleInt
两个符号的。
当我们将liba2
和libb
同时链接到主程序中时,自然出现了重名的冲突。
而根据前面的实验结果,可以看到,最终被链接的程序,来自首先被链接的库。
那么,重名符号冲突,到底有哪些情况呢?
重名符号优先级
符号的定义域,一般有三种:
- 目标文件或静态链接库
- 动态链接库导出符号
- 动态链接库本地符号(static function,文件作用域函数)
其中,对于优先级最高的第1类符号,重名符号冲突会直接导致报错,不被允许。
跨优先级的重名情况,高优先级一般总会覆盖低优先级的符号。
而同优先级的动态链接库导出符号重名,则根据链接顺序有所不同。
不过,两个不同动态链接库的本地符号重名,是毫无干扰的,因为它们的作用域仅限于所在文件。
一个疑惑
可是,本文前面的例子,明明就是一个静态链接库和一个动态链接库,并不是静态链接库优先级高就覆盖了动态链接库的导出符号,而是与链接顺序有关啊!
其实并非如此。
本文的例子,按理来说,确实应该优先使用静态链接库的程序。但是由于我们只调用了GetDoubleInt()
和GetInt()
,而这两个名称在动态链接库中均能够解析并链接到,静态链接库就被链接器认为没有存在的必要了。
如果此时,我们在静态链接库liba2
中再定义一个独一无二的函数,并在main
中调用,会发现无论如何修改链接库的链接顺序,最终的结果都是调用liba2
的程序。
在这里,具体原因我没有找到明确的参考依据,我的推断大概是:
链接器根据链接顺序解决符号链接,如果已经解决了全部符号,那么后续链接库就不予考虑;而静态链接库一旦有链接的必要,那么全部符号的优先级就会高于动态链接库的导出符号。
我查阅资料了解到--no-as-needed
和--as-needed
这一对链接选项(使用gcc/g++
设置时应在前面加上-Wl,
)可以控制链接器是否按需链接库,不过应该只是对动态链接库进行按需链接。
所以,这个行为的具体原因我仍然没有确定,但大概情况应该差不多是我描述的这样。
解决方案:隐藏符号
那么,有什么办法可以实现我们的预期结果,就是如何让动态链接库的GetDoubleInt
继续调用它原本调用的旧版liba
,而主程序调用的GetInt
则调用新版的liba2
呢?
其实,只要把动态链接库导出的GetInt()
符号隐藏,就不存在符号重名冲突的问题了。
方法有两种。
方法一:链接选项--exclude-libs
在链接生成动态链接库的时候,加入-Wl,--exclude-libs liba.a
即可避免导出静态链接库liba
的符号,也就不会导出GetInt()
这个函数。
这样,libb
动态链接库中的GetInt()
就相当于本地符号。
主程序调用动态链接库libb
的GetDoubleInt()
时,动态链接库内部还是会调用liba
的GetInt()
;而主程序调用GetInt()
,因为无法在动态链接库中找到这个符号,自然就会调用liba2
的了。
libb.so: b.o liba.a
g++ -shared b.o -L. -Wl,--exclude-libs liba.a -la -o libb.so
执行结果:
9999
6
是我们想要的!
方法二:Version Script
可以使用链接器支持的Version Script控制导出的符号名称,即提供一个文件来指定具体导出哪些符号。
具体可以参考: GNU ld 文档 - Version Scripts
参考文献
https://www.safaribooksonline.com/library/view/advanced-c-and/9781430266679/9781430266679_Ch09.xhtml