起因
由于业务需要,我司使用了Mellanox某闭源C++程序,Mellanox推荐的定制化开发方法是:对其链接的动态库进行定制化开发,以添加额外的功能。
在方案讨论阶段,发现很多同事对动态库/静态库所代表的的含义并不十分清楚,特别是当同名函数存在时,编译、链接、运行的结果是什么也没有明确的认识,故写下这篇文章。
基本概念
程序函数库可分为下面几种类型:
- 静态函数库(
static libraries
):在编译期间(compile-time)静态链接库会全部拷贝进编译对象中,一般以.a
文件的存在 - 动态函数库(
shared libraries
):在程序启动的时候加载到程序中,它可以被不同的程序共享,一般以.so
文件存在- 动态加载函数库(
dynamically loaded libraries
),在进程运行期间,使用dlfcn.h
中的函数加载、调用、关闭动态库
- 动态加载函数库(
关于动态库和静态库的优缺点,相关文章很多,这里不再赘述
同名函数测试
使用两个.c
文件test2.c
和test2.c
包含同名函数void test()
// test1.c
#include <stdio.h>
void test() {
printf("call from test1.c");
}
// test2.c
#include <stdio.h>
void test() {
printf("call from test2.c");
}
含有main函数的文件main.c
// main.c
extern void test();
int main() {
test();
}
测试1:.o
目标文件
使用如下命令行,将test2.c
和test2.c
生成目标文件,并编译可执行文件
gcc -c ./test1.c
gcc -c ./test2.c
gcc -o main ./test1.o ./test2.o ./main.c
结果报错:
./test2.o: In function `test':
test2.c:(.text+0x0): multiple definition of `test'
./test1.o:test1.c:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status
可见,将包含同名函数的目标文件进行链接,如果其在同一个命名空间中,会报multiple definition
错误。
测试2:静态库
使用如下命令行编译静态库libtest1.a
和libtest2.a
g++ -c ./test1.c
g++ -c ./test2.c
ar crv libtest1.a test1.o
ar crv libtest2.a test2.o
接着我们链接编译:
gcc -L. ./main.c -ltest1 -ltest2 -o main
可以编译成功,无报错。执行结果如下
$ LD_LIBRARY_PATH=. ./main
call from test1.c
有朋友会问:“为什么没有报错呢?我明明把包含同名函数的两个静态库链接进同一个可执行文件了。”
为了探究为什么没有报错,我们增加ld选项-Wl,--verbose
来看看链接时到底发生了什么。再执行编译,我们得到输出:
...
attempt to open ./libtest1.so failed
attempt to open ./libtest1.a succeeded
(./libtest1.a)test1.o
attempt to open ./libtest2.so failed
attempt to open ./libtest2.a succeeded
...
可以发现,最终的链接结果,输出的二进制文件只链接了libtest1.a
背后的test1.o
文件,而没有链接libtest2.a
。编译器这么做的含义是:
- 编译器根据链接先后顺序,依次查找链接库。
- 首先查找
libtest1.a
,发现其有main函数需要的函数void test()
,因此将其进行了链接。 - 再扫描到
libtest2.a
的时候,由于void test()
已经被libtest1.a
中的符号提供,因此不再链接。
Stack Overflow中有个问题也谈到了这点。
如果使用ld参数--whole-archive
强行链接libtest1.a
和libtest2.a
,我们会看到和测试1同样的报错:
$ gcc -L. ./main.c -Wl,--whole-archive -ltest1 -ltest2 -Wl,--no-whole-archive -o main
./libtest2.a(test2.o): In function `test':
test2.c:(.text+0x0): multiple definition of `test'
./libtest1.a(test1.o):test1.c:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status
测试3:动态库
使用如下命令行编译动态库libtest1.so
和libtest2.so
并编译可执行文件。
gcc -shared -fPIC -o libtest1.so test1.c
gcc -shared -fPIC -o libtest2.so test2.c
gcc -L. ./main.c -ltest1 -ltest2 -o main
编译无报错,ldd检查,libtest1.so
和libtest2.so
确实都链接进main可执行文件中。执行结果如下:
$ LD_LIBRARY_PATH=. ./main
call from test1.c
可见,在动态链接时,不同的链接库可以有同名函数,不影响编译。这是由动态链接库的性质决定的,其只有在运行时才会动态加载,并且加载的顺序是由编译时链接的顺序决定的。这也就说符号会以第一个查找到的为准(Symbols are resolved on a first match basis
)。
我们也可以通过设置LD_PRELOAD
,提前将某动态库load进内存。
同名函数的应用
有朋友会提出这样的疑问,上面虽然做了这么多实验,但多少有点语言律师的感觉,这些知识能改善我们日常生活吗?日常工作中能用的到吗?答案当然是能用得到。
最简单的应用场景,比如某开源库中有个函数我不喜欢,我想写个自己的版本替换掉,那么完全可以利用上述的知识,将自己实现的某函数以动态或者静态的方式链接进可执行文件中,替换自己不喜欢的版本。
工业上常见的应用有以下几种:
- 替换库:大名鼎鼎的
tcmalloc
就是以这种方式运行的。我们将tcmalloc链接进程序,只要tcmalloc库的查找顺序优先于libc,就可以替换原生的内存管理函数为tcmalloc版本。 - mock测试:陈硕在文章中详述了如何在C++单元测试中mock系统调用。其中的链接期垫片 (
link seams
)方法,就是利用libc
一般情况下是动态链接的特性,在进程中mock系统调用。