动态库一般用的多写的少,操作系统、正规软件发布的库都经过了仔细设计和严格测试,用起来很方便,加上头文件和链接指令就可以了。可是轮到自己写动态库的时候却发现问题百出,不是找不到符号就是符号已经定义。这究竟是怎么回事呢?
目录
一、如何编写动态库
程序链接其本质是解决未决符号。
一个真正可以运行的程序由运行时的动态链接和编译时的静态链接两级完成:
编译单元 | 内容 | |
a.c | int x(); int a=x(); | 声明了函数x,但是没有定义,也就是未决符号 调用了未决符号x |
b.c | int y(); int x(){return y();} | 声明了函数y,但是没有定义 定义了x,解决了a.c的问题,但是又引用了未决符号y |
上面没有涉及到.h文件,因为.h不是编译单元,.h只是.c的一部分,编译之前.h会被完全嵌入到.c里面去。(对于C++,编译单元扩展名为.cpp)
定义在.h文件的符号最终会在包含这个头文件的.c里面出现,如果多个.c都包含了同一个.h就会发生符号重定义错误,编译时出现“XXXX已经出现在YYYY.o”的错误。
上面表格中的程序是无法编译的,因为函数y的定义是不存在的。
如果函数y是由静态库提供的,那么只需要链接静态库就可以了。静态库的性质和.c是一样的,只不过是已经编译成.o而已,静态库就是打包起来的一组.o。
如果函数y由动态库提供,那么就复杂一些,因为动态库是在程序运行时链接的,编译器管不了。编译器只能确保编译的时候检查了动态库,确实能找到符号y。操作系统运行程序的时候,又会按照自己的方式去实际链接程序。
编译器寻找动态库的路径和操作系统寻找动态库的路径是不同的配置方法,编译器使用-L参数指定,而操作系统则使用环境变量LD_LIBRARY_PATH。
由于编译器和操作系统找到的动态库可能根本不一样,比如不同的版本,那么,编译器检查觉得正常的程序运行时未必能链接,因为:
- 动态库版本低,没有这个符号
- 动态库删除了原有的符号,导致符号找不到错误。
- 动态库增加了符号,导致符号冲突。
第一点是你的问题,你需要升级。(你也可以推脱是用户的问题,用户需要升级。)
第二点是动态库提供者的问题,升级时未能保持兼容的版本。
第三点是库设计永恒的难题,不可能不提供新功能,提供新功能不可能不增加符号。现在流行的解决办法是用名字空间,保证每个库都有独立的名字空间,名字空间之下加东西就不会和别人冲突了。但是目前的操作系统都是基于C的,没有名字空间这个东西,所以要靠自己。
前面说的头文件里定义东西会在编译时发生重定义错误,这个问题同样会发生在动态链接时。头文件的麻烦在于我们并不能时时意识到头文件只是.c的嵌入部分而已,经常会不小心在头文件里面多放东西。
对于库的编写者来说,编写良好的库的要求就很多了:
- 头文件应该是纯粹的接口,没有任何多余的、不需要暴露给用户的东西
- 库文件暴露的符号应该仅仅是头文件暴露出去的,没有暴露任何内部使用的符号
为了实现上面的目标,我们需要专门屏蔽和暴露符号。
二、实际操作
导出一般不是个问题,因为默认情况下是全部导出的,但是,主程序、多个动态库之间存在同名符号怎么办?一般是使用已经存在的,也就是主程序的或已经加载的动态库的。
这就产生了问题:此种情形大部分情况下我们希望动态库用的是动态库里面自己的,而不是主程序或别的动态库的(因为我们写动态库的时候根本不知道别人有什么)。
另外,如果两个库有同名变量或函数,程序是无法链接的,链接器会报告符号冲突,那么由谁来修改?可能两个库都惹不起,傻眼了。
三、目标
因此必须做到:
- 优先调用动态库自己的符号
- 只导出必要的符号
3.1 优先调用动态库自己的符号
如果自己有却调用到别人的符号,你看到的值明明是这样,为什么程序却是那样?一头雾水,完全不知所措。
所以要优先调用自己的符号。这一点通过编译参数实现(具体见后)。
3.2 只导出必要的符号
这本来是写公共代码的基本原则,但是绝大部分人都没有遵守。
提供纯粹的接口需要额外一些编程工作,需要遵守一些专门的规则,增加不少工作量。
通过编译参数和代码里面的编译器指令可以屏蔽所有内部符号,除非明确指出是外部接口(具体见后)。
三、方法
以下几个方法的正确组合能保证正确的符号导出和使用:
3.1 生成动态库时
-Wl,-Bsymbolic 优先使用内部符号,避免调用了其它模块的同名符号
3.2 编译.o时
-fvisibility=hidden 默认不导出符号,避免暴露内部符号
3.3 代码控制
3.3.1 批量控制
#pragma GCC visibility push(hidden) 隐藏下面的所有符号
#pragma GCC visibility push(default) 导出下面的所有符号
#pragma GCC visibility pop 还原符号
3.3.2 单独控制
__attribute__ ((visibility ("default"))) 用在函数和变量前,导出一个符号,也可用于类和成员
四、辅助命令
如何查看动态库的符号:
readelf -a libXXXX.so | grep OBJECT
(这里是结束)