编写一些大型应用程序时,如果将所有的源文件一起编译,则牵一发而动全身,任何文件微小的修改都需要重新编译整个项目,则显然是相当低效的。所以,将一个项目分为几个依赖库,运行的时候再查找对应库中的入口,能提高效率。这就是动态链接库,Linux下为so文件,Windows下则为DLL文件。
在一篇文章将介绍常用的开发动态链接库的技巧和方法。
动态链接介绍
众所周知,任何程序从源代码变成二进制文件,需要经过编译和连接两个过程。编译是将每个源代码文件转为对应的机器代码;连接是将每份机器代码整合成一个文件,将调用的函数用相应地址代替。
正如前面介绍,如果将所有调用的文件都打包起来,不利于高效开发,我们有个新的思路:不妨先做编译和初步的连接,等运行的时候再从内存等地方实时地调用相应函数。这就是动态链接。
下面我们来实现一个简单的例子:
main.c
代码:
#include "libmewwoof_hello.h"
int main() {
mewwoof_hello_print();
return 0;
}
libmewwoof_hello.h
代码:
#ifndef _LIB_MEWWOOF_HELLO_H_
#define _LIB_MEWWOOF_HELLO_H_
void mewwoof_hello_print();
#endif
libmewwoof_hello.c
代码:
#include <stdio.h>
#include "libmewwoof_hello.h"
void mewwoof_hello_print() {
printf("Hello, mewwoof!\n");
}
然后分别编译成动态库和主程序。-f PIC
表示生成位置无关代码 (Position-Independent Code),-L .
指定在当前目录下查找链接库。
gcc libmewwoof_hello.c -fPIC -shared -o libmewwoof_hello.so
gcc main.c -L. -lmewwoof_hello -o mewwoof-hello
得到可执行文件mewwoof-hello
和动态库文件libmewwoof_hello.so
。执行:
$ LD_LIBRARY_PATH=. ./mewwoof-hello
Hello, mewwoof!
可以使用ldd
命令查看动态链接的内容:
$ LD_LIBRARY_PATH=. ldd ./mewwoof-hello
linux-vdso.so.1 (0x00007ffd1c5f6000)
libmewwoof_hello.so => ./libmewwoof_hello.so (0x00007f318d3d8000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007f318d17e000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f318d3e4000)
库命名规范
一般使用libfoo.so
、libfoo_bar.so
这样的命名方式。gcc也是通过这种方式查找动态库的。
但动态链接库一般还附带有版本信息:如libfoo.so.1.2.1
。这个命名也有规范。对于libname.so.x.y.z
:
- x表示主版本号,当这个值变动表示API可能发生较大变化,不兼容之前版本的应用
- y表示次版本号,当这个值变动表示API功能只增不减,向后兼容
- z表示向后兼容的版本数。比如说版本1.5是版本1.4,1.3的超集,但不兼容版本1.2,则应该命名为1.5.2。(实际上有争议,比如Apache使用z表示patch数,仅仅表示bug修复:https://apr.apache.org/versioning.html)
除了版本命名有讲究,我们在查看Linux库文件时,也常常会看到这种情形:
$ ls -la
...
lrwxrwxrwx 1 root root 18 12月 24 01:43 libvtkverdict.so -> libvtkverdict.so.1
lrwxrwxrwx 1 root root 22 12月 24 01:43 libvtkverdict.so.1 -> libvtkverdict.so.9.1.0
-rwxr-xr-x 1 root root 214712 12月 24 01:43 libvtkverdict.so.9.1.0
...
同样一个库却被“复制”了多次,事实上,libname.so.x.y.z
为realname,libname.so.x
为soname,libname.so
为linker name。realname对应文件是真正用来存储二进制库代码的,soname是动态链接时查找的,linker name是编译时查找的。
https://www.cnblogs.com/lidabo/p/13862630.html?ivk_sa=1024320u
例如:gcc -o test test.o -lz , 链接时就会找libz.so 。若没有这个文件,链接器就报错。
库版本管理
既然库是相对于主程序独立的,有自己的版本,那么一个问题就出现了:不同版本的库提供的功能也不一样,就算是不同次版本号的库,也会有功能的增加。如果一个应用使用了高版本库的功能,而运行时却只安装了低版本库,那么动态链接时就会出错。如何避免这些错误,让程序早点知道这个问题呢?这是就需要使用符号版本管理了。可以参考前面的一篇文章,介绍了符号版本:
现在我们来为自己编写的库添加上符号版本。
现在我们的主程序main.c
是这样的:
#include "libmewwoof_hello.h"
int main() {
mewwoof_hello_print();
return 0;
}
编译,查看符号列表:
$ objdump -T ./mewwoof-hello
./mewwoof-hello: 文件格式 elf64-x86-64
DYNAMIC SYMBOL TABLE:
0000000000000000 DF *UND* 0000000000000000 (GLIBC_2.34) __libc_start_main
0000000000000000 w D *UND* 0000000000000000 Base _ITM_deregisterTMCloneTable
0000000000000000 DF *UND* 0000000000000000 Base mewwoof_hello_print
0000000000000000 w D *UND* 0000000000000000 Base __gmon_start__
0000000000000000 w D *UND* 0000000000000000 Base _ITM_registerTMCloneTable
0000000000000000 w DF *UND* 0000000000000000 (GLIBC_2.2.5) __cxa_finalize
$ objdump -T ./libmewwoof_hello.so
./libmewwoof_hello.so: 文件格式 elf64-x86-64
DYNAMIC SYMBOL TABLE:
0000000000000000 w D *UND* 0000000000000000 Base _ITM_deregisterTMCloneTable
0000000000000000 DF *UND* 0000000000000000 (GLIBC_2.2.5) puts
0000000000000000 w D *UND* 0000000000000000 Base __gmon_start__
0000000000000000 w D *UND* 0000000000000000 Base _ITM_registerTMCloneTable
0000000000000000 w DF *UND* 0000000000000000 (GLIBC_2.2.5) __cxa_finalize
0000000000001109 g DF .text 0000000000000016 Base mewwoof_hello_print
可以看到mewwoof_hello_print
版本都是Base,也就相当于没有版本。
编写版本文件
libmewwoof_hello.ver:
MWF_HE_0.1.0 {
global:
mewwoof_hello_print;
};
重新编译库:
$ gcc libmewwoof_hello.c -fPIC -shared -Xlinker --version-script libmewwoof_hello.ver -o libmewwoof_hello.so
再次使用objdump
,可以看见打上了版本信息:
0000000000001109 g DF .text 0000000000000016 MWF_HE_0.1.0 mewwoof_hello_print
此时直接运行之前的主程序,可以发现运行成功,这是因为原来的主程序并没有包含版本符号。现在我们重新编译主程序:
$ gcc main.c -L. -lmewwoof_hello -o mewwoof-hello
再次使用objdump,可以看见主程序调用的mewwoof_hello_print
也标记了版本符号。
将库命名为libmewwoof_hello.so.0.1.0
,修改ver文件,版本号改为0.1.1,再次编译库文件,查看库文件的符号,确认修改成功:
0000000000001109 g DF .text 0000000000000016 MWF_HE_0.1.1 mewwoof_hello_print
此时直接运行主程序,就会报错了:
$ LD_LIBRARY_PATH=. ./mewwoof-hello
./mewwoof-hello: ./libmewwoof_hello.so: version `MWF_HE_0.1.0' not found (required by ./mewwoof-hello)
指定库版本编译
现在我们有了两个库,0.1.0和0.1.1的。可以在编译时指定使用那个具体的库:
gcc main.c -L. -l:libmewwoof_hello.so.0.1.0 -o mewwoof-hello
在运行时,就会专门查找libmewwoof_hello.0.1.0来动态链接。
同时提供两个版本的函数
一个库还可以同时提供两个版本的符号:
libmewwoof_hello.c
:
#include <stdio.h>
#include "libmewwoof_hello.h"
asm(".symver mewwoof_hello_print, mewwoof_hello_print@MWF_HE_0.1.0");
asm(".symver mewwoof_hello_print_new, mewwoof_hello_print@MWF_HE_0.1.1");
void mewwoof_hello_print() {
printf("Hello, mewwoof!\n");
}
void mewwoof_hello_print_new() {
printf("Hello, mewwoof new!\n");
}
libmewwoof_hello.ver
:
MWF_HE_0.1.0 {
global:
mewwoof_hello_print;
};
MWF_HE_0.1.1 {
global:
mewwoof_hello_print;
local:
mewwoof_hello_print_new;
};
再次编译库文件,查看符号列表:
$ objdump -T ./libmewwoof_hello.so
./libmewwoof_hello.so: 文件格式 elf64-x86-64
DYNAMIC SYMBOL TABLE:
0000000000000000 w D *UND* 0000000000000000 Base _ITM_deregisterTMCloneTable
0000000000000000 DF *UND* 0000000000000000 (GLIBC_2.2.5) puts
0000000000000000 w D *UND* 0000000000000000 Base __gmon_start__
0000000000000000 w D *UND* 0000000000000000 Base _ITM_registerTMCloneTable
0000000000000000 w DF *UND* 0000000000000000 (GLIBC_2.2.5) __cxa_finalize
0000000000000000 g DO *ABS* 0000000000000000 MWF_HE_0.1.1 MWF_HE_0.1.1
000000000000111f g DF .text 0000000000000016 (MWF_HE_0.1.1) mewwoof_hello_print
0000000000001109 g DF .text 0000000000000016 (MWF_HE_0.1.0) mewwoof_hello_print
0000000000000000 g DO *ABS* 0000000000000000 MWF_HE_0.1.0 MWF_HE_0.1.0
编译主程序,这次主程序使用新编译的库文件(不是之前重命名的那个):
gcc main.c -L. -lmewwoof_hello -o mewwoof-hello
编译失败!原因是要在主程序代码中指定使用的版本:
编辑main.c
:
#include "libmewwoof_hello.h"
asm(".symver mewwoof_hello_print, mewwoof_hello_print@MWF_HE_0.1.0");
int main() {
mewwoof_hello_print();
return 0;
}
再次编译,可以看到,保持使用了0.1.0的符号:
0000000000000000 DF *UND* 0000000000000000 (MWF_HE_0.1.0) mewwoof_hello_print
运行,输出“Hello, mewwoof!”。
修改main.c
这一行:
asm(".symver mewwoof_hello_print, mewwoof_hello_print@MWF_HE_0.1.1");
编译,查看版本:
0000000000000000 DF *UND* 0000000000000000 (MWF_HE_0.1.1) mewwoof_hello_print
运行:
$ LD_LIBRARY_PATH=./ ./mewwoof-hello
Hello, mewwoof new!
可以看到,使用了新版本的函数。
提供默认版本
通过上面的例子,可以看到如果每次都需要手动指定版本信息,显然太麻烦。事实上可以指定一个默认版本,使用两个“@”即可:
编辑libmewwoof_hello.c:
#include <stdio.h>
#include "libmewwoof_hello.h"
asm(".symver mewwoof_hello_print, mewwoof_hello_print@MWF_HE_0.1.0");
asm(".symver mewwoof_hello_print_new, mewwoof_hello_print@@MWF_HE_0.1.1");
void mewwoof_hello_print() {
printf("Hello, mewwoof!\n");
}
void mewwoof_hello_print_new() {
printf("Hello, mewwoof new!\n");
}
去掉主程序中的汇编语句,全部重新编译,可以看到使用了0.1.1版本的函数:
$ objdump -T ./mewwoof-hello
./mewwoof-hello: 文件格式 elf64-x86-64
DYNAMIC SYMBOL TABLE:
0000000000000000 DF *UND* 0000000000000000 (GLIBC_2.34) __libc_start_main
0000000000000000 w D *UND* 0000000000000000 Base _ITM_deregisterTMCloneTable
0000000000000000 w D *UND* 0000000000000000 Base __gmon_start__
0000000000000000 DF *UND* 0000000000000000 (MWF_HE_0.1.1) mewwoof_hello_print
0000000000000000 w D *UND* 0000000000000000 Base _ITM_registerTMCloneTable
0000000000000000 w DF *UND* 0000000000000000 (GLIBC_2.2.5) __cxa_finalize
运行:
$ LD_LIBRARY_PATH=. ./mewwoof-hello
Hello, mewwoof new!
静态链接
这里顺便提一下静态链接。
既然动态链接表示系统查找相关的库,那么广义的静态链接就指的是系统无需查找相关库。这里实现“无需查找”有两种方式,一种是直接指定相关库的位置,另一种是直接将相关库的二进制代码打包到一起,形成一个大文件。
指定库位置
参照上文 指定库版本编译
全部打包
全部打包对库有要求:要求这个库是静态的,事实上也就是使用多个源文件直接编译并打包在一起的过程:
# 静态编译库
gcc -c libmewwoof_hello.c -o libmewwoof_hello.o
# 打包成静态库
ar rcs libmewwoof_hello.a ./libmewwoof_hello.o
# 静态编译主程序
gcc -c main.c -o mewwoof-hello.o
# 合并
gcc mewwoof-hello.o -L. -lmewwoof_hello -static -o mewwoof-hello-bin
如果使用本文的例子,以上命令最后一步将执行失败,因为静态链接是不支持符号版本控制的。
参考文献
[1] VERSION Command http://sourceware.org/binutils/docs/ld/VERSION.html
[2] https://gcc.gnu.org/wiki/SymbolVersioning