在Linux中创建动态库通过如下过程完成:
gcc -fPIC -c first.c second.c
gcc -shared first.o second.o -o libdynamiclib.so
按照Linux的惯例,动态以lib
作为前缀,并以.so
作为文件扩展名。
需要两个必要的选项
fPIC
,告诉编译器使用位置无关代码技术。fPIC
本质上是用于辅助加载器加载程序,可以使多个进程可以无缝映射到已加载动态库的内存映射中。
有一点还需要注意的是:如果静态库是链接到动态库中,那么就在编译静态库时就必须使用**-fPIC**
选项编译。否则链接器会报错。
shared
,告诉编译器生成动态库。动态库本质上是一种代码复用的技术,动态库提供功能接口给客户程序,而使客户程序不必关心内部实现细节。
对动态库的,需要特别注意ABI兼容和接口的可见效性。
ABI的全称是Application Binary Interfance
,应用程序二进制接口。
ABI与API相同的概念,但是两者的侧重不一样。API是指代码层面的接口,在代码层面客户程序关注所依赖库,提供的接口定义,返回值等特征。而ABI是指客户程序和所依赖的库被编译成二进制文件后,在链接器,加载器层面对应的符号是否匹配。
其实站在开发者的角度,认为API与ABI是应该等价的,代码里的接口匹配,难道编译后,二进制文件中的接口对应的符号还不匹配吗?是的,对C++来说是要需要关注这些问题。
ABI兼容也是C++饱受诟病的问题,它是C++的复杂性造成,并且因为各个编译器厂商没有统一ABI标准而加剧。同一套代码,使用不同的编译器而导致无法链接到客户程序。
最被广泛认识的造成C++ ABI兼容的问题是:符号修饰。
C++引入的命名空间,类,私有,公有等一些面向对象的语法。在编码层面这些机制很好的保证了模块的划分及命名冲突。但是在编译层面也引入了符号命名的复杂性,因为在变成二进制符号时,可没这些概念,但是编译器也需要保证符号唯一,所以会有一套符号命令规则。
这套规则并没有标准化,各个厂家间,甚至同一厂家不同的版本间也可能不兼容。
gcc中有个比较典型的ABI兼容情况,也是我们比较容易遇到的,从GCC 5.1开始为了兼容C++ 11标准,对std::string
和 std::list
的命令空间进行了更改,相应的符号也变化了。从std::string
变为了std::__cx11:string
,从std::list
变为了std::__cx11::string
。
所以如果依赖库所使用的编译器是gcc5.1之前的版本,而客户程序使用的是gcc5.1版本,那么在链接时,std::string
和std::list
的因为两个版本的编译器生成的符号不一样,而会导致链接失败。有两个解决方法:
_GLIBCXX_USE_CXX11_ABI
宏加入编译选项 -D_GLIBCXX_USE_CXX11_ABI=1/0
所以容易遇到ABI兼容问题的场景:
比如在Linux下客户程序用的gcc,而所依赖的库使用clang编译。
比如我们上面所举的GCC的例子。
当然我们也并不需要掌握ABI兼容问题的细节,只要我们做到以下几点就可以避免绝大多数ABI兼容问题:
在C语言中并没有符号修饰,对调用约定,内存布局,编译器也做到了统一。
通过extern "C"
让编译器生成不带修辞的符号名。
#ifdef __cplusplus
extern "C"
{
int Function(int x,int y);
}
#endif //__cplusplus
接口中的数据类型也使用C语言的类型。比如将std::string
替换为char[]
。如果是自定义类则改为结构体。在C++中是兼容C语言的类型内存布局的。在接口实现时,可以使用C++实现。
动态库应该只向客户程序暴露所需求的接口。所以需要控制接口对外的可见性。
**在Linux中所有符号默认都是外部可见的,**可以通过以下几种方法来控制Linux下符号的可见性:
-fvisibility=hidden
在编译动态库时加入该编译选项,那么所有的动态符号都置为对外不可见,任何尝试链接该动态库的客户程序将无法访问这些符号。
__attribute__((visibility("")))
通过在函数前面使用编译属性修饰,可以指示链接器运行或禁止对外提供该符号。
strip
工具直接通过strip
工具抹除动态库中的某个符号。
如下是两个简单的示例代码文件:
void function1(void);
void function2(void);
void function3(void);
#include
#include "sharedLib.h"
void function1(void) {
printf("function1\n");
}
void function2(void) {
printf("function2\n");
}
void function3(void) {
printf("function3\n");
}
#include "sharedLib.h"
int main() {
function1();
function2();
function3();
}
gcc -fPIC -shared sharedLib.c -o libsharedLib.so
生成动态libsharedLib.so
通过nm libsharedLib.so
查看它的符号,如下:
00000000000006f5 T function1
0000000000000707 T function2
0000000000000719 T function3
符号的类型都是T
类型,表示位于代码区的符号,对外可见。
链接动态库,生成test
可执行程序
gcc main.c -L. -lsharedLib -o test
在运行test
程序时,需要设置LD_LIBRARY_PATH
环境变量,用于指定libsharedLib.so
的路径。使加载器能找到它。
-fvisibility
-fvisibility
编译选项,让所有符号默认不可见-fvisibility=hidden -fvisibility-inlines-hidden
再通过该编译选项将libsharedLib.so
的接口都改为不可见。
gcc -fPIC -shared sharedLib.c -fvisibility=hidden -fvisibility-inlines-hidden -o libsharedLib.so
通过nm libsharedLib.so
查看符号,如下
0000000000000675 t function1
0000000000000687 t function2
0000000000000699 t function3
符号类型变为t
,表示为内部符号,对外不可见。
那么链接libsharedLib.so
库时就会报错,如下:
gcc main.c -L. -lsharedLib -o test
main.c:(.text+0x5):对‘function1’未定义的引用
main.c:(.text+0xa):对‘function2’未定义的引用
main.c:(.text+0xf):对‘function3’未定义的引用
__attribute__((visibility("default")))
修饰需要导出的符号将符号改为不可见,同时通过改属性,设置某个方法为可见。设置function1
为可见。
#include
#include "sharedLib.h"
#define FUNC_EXPORT __attribute__((visibility("default")))
void FUNC_EXPORT function1(void) {
printf("function1\n");
}
void function2(void) {
printf("function2\n");
}
void function3(void) {
printf("function3\n");
}
将function1
设置为了可见,重新编译。
gcc sharedLib.c -fPIC -shared -fvisibility=hidden -fvisibility-inlines-hidden -o libsharedLib.so
通过nm libsharedLib.so
查看其符号,如下:
0000000000000675 T function1
0000000000000687 t function2
0000000000000699 t function3
function1
变为了可见,其它两个都是不可见。
strip
工具删除指定符号strip --strip-symbol 符号名
,直接通过strip
命令简单粗暴的删除动态库中的符号。当然它也可以删除静态库中的符号。
strip --strip-symbol function1 libsharedLib.so
,删除libsharedLib.so
中的符号function1
。