C/C++程序编译与链接(四) 创建动态库

创建动态库

在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兼容

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::stringstd::list的命令空间进行了更改,相应的符号也变化了。从std::string变为了std::__cx11:string,从std::list变为了std::__cx11::string
所以如果依赖库所使用的编译器是gcc5.1之前的版本,而客户程序使用的是gcc5.1版本,那么在链接时,std::stringstd::list的因为两个版本的编译器生成的符号不一样,而会导致链接失败。有两个解决方法:

  1. 都用同一版本的gcc编译。
  2. 使用_GLIBCXX_USE_CXX11_ABI

加入编译选项 -D_GLIBCXX_USE_CXX11_ABI=1/0

所以容易遇到ABI兼容问题的场景:

  • 客户程序与所依赖的库,使用的是不同的编译器编译

比如在Linux下客户程序用的gcc,而所依赖的库使用clang编译。

  • 客户程序与所依赖的库,使用同一编译器的不同版本

比如我们上面所举的GCC的例子。

当然我们也并不需要掌握ABI兼容问题的细节,只要我们做到以下几点就可以避免绝大多数ABI兼容问题:

  • 客户程序与所依赖的库,尽量用同一个编译器同一版本编译
  • 以C语言风格设计接口

在C语言中并没有符号修饰,对调用约定,内存布局,编译器也做到了统一。
通过extern "C"让编译器生成不带修辞的符号名。

#ifdef __cplusplus
extern "C"
{
    
    
    int Function(int x,int y);
}
#endif //__cplusplus

接口中的数据类型也使用C语言的类型。比如将std::string替换为char[]。如果是自定义类则改为结构体。在C++中是兼容C语言的类型内存布局的。在接口实现时,可以使用C++实现。

动态符号的可见性

动态库应该只向客户程序暴露所需求的接口。所以需要控制接口对外的可见性。
**在Linux中所有符号默认都是外部可见的,**可以通过以下几种方法来控制Linux下符号的可见性:

  1. -fvisibility=hidden

在编译动态库时加入该编译选项,那么所有的动态符号都置为对外不可见,任何尝试链接该动态库的客户程序将无法访问这些符号。

  1. __attribute__((visibility("<default | hidden>")))

通过在函数前面使用编译属性修饰,可以指示链接器运行或禁止对外提供该符号。

  1. strip工具

直接通过strip工具抹除动态库中的某个符号。

示例

如下是两个简单的示例代码文件:

void function1(void);
void function2(void);
void function3(void);
#include <stdio.h>
#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 <stdio.h>
#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

猜你喜欢

转载自blog.csdn.net/mo4776/article/details/129459581