一、C++动态库编译
采用g++编译C++动态库,命令如下:
g++ -fPIC -shared -o 动态库名 cpp文件名
1.1 关于fPIC选项
首先了解动态库的载入时重定位。
一般linux的可执行文件都是elf格式(一种二进制文件格式),在可执行文件的头部包含了文件格式、加载地址、符号表等信息。当连接器链接生成可执行文件时,会将程序的加载地址写入到可执行文件的头中。在程序运行时,动态加载器将可执行文件载入文件头指定的加载地址位置,并加载该地址,开始从该地址处运行。由此可见,可执行文件的起始地址是在编译时就决定的。
#define EI_NIDENT 16
typedef struct{unsigned char e_ident[EI_NIDENT];Elf32_Half e_type;Elf32_Half e_machine;Elf32_Word e_version;Elf32_Addr e_entry;Elf32_Off e_phoff;Elf32_Off e_shoff;Elf32_Word e_flags;Elf32_Half e_ehsize;Elf32_Half e_phentsize;Elf32_Half e_phnum;Elf32_Half e_shentsize;Elf32_Half e_shnum;Elf32_Half e_shstrndx;
} Elf32_Ehdr;
最开头是16个字节的e_ident, 其中包含用以表示ELF文件的字符,以及其他一些与机器无关的信息。开头的4个字节值固定不变,为0x7f和ELF三个字符。
e_type 它标识的是该文件的类型。
e_machine 表明运行该程序需要的体系结构。
e_version 表示文件的版本。
e_entry 程序的入口地址。
e_phoff 表示Program header table 在文件中的偏移量(以字节计数)。
e_shoff 表示Section header table 在文件中的偏移量(以字节计数)。
e_flags 对IA32而言,此项为0。
e_ehsize 表示ELF header大小(以字节计数)。
e_phentsize 表示Program header table中每一个条目的大小。
e_phnum 表示Program header table中有多少个条目。
e_shentsize 表示Section header table中的每一个条目的大小。
e_shnum 表示Section header table中有多少个条目。
e_shstrndx 包含节名称的字符串是第几个节(从零开始计数)。
elf的依赖库查看
readelf -d main1 | grep NEEDED
elf各个section的header信息
readelf -S --wide main
载入时重定位的缺点:
1、动态库的代码段不能在进程间共享:多个进程加载同一个动态库到各自不同的地址空间,导致代码段需要不同的重定位,所以最终每个引用该动态库的进程拥有一份该动态库代码段的不同拷贝。
2、代码段必须是可写的,增加了被攻击风险。
为了解决载入时重定位的问题,引入了PIC的概念,即位置无关代码。
1.2 关于shared选项
-shared用来创建动态库
1.3 测试demo
#include <iostream>
using namespace std;void Get123Info()
{cout << "get info call success" << endl;
}
1.3.1 C++名字改编问题
这里涉及一个问题需要注意
名字改编(Name Mangling,或Name Decoration):在C++中,有函数重载的特性,所以编译器在编译时会出现符号名称相同的问题,为了解决这个问题,就有了名字改编。它将一些函数的额外信息加入到符号名中。
1.3.2 常规的动态库接口处理手段
如果我们在动态库的制作中,接受了这个名字改编,那对方调用加载这个符号时,就需要根据提供的so的实际符号名进行加载,那将很麻烦,所以一般都是采用C语言的规则来解决这个问题,即采用extern "C"的方式。
修改后的代码如下
#include <iostream>
using namespace std;#ifdef __cplusplus
extern "C"__attribute__((visibility("default"))) void Get123Info(){cout << "get info call success" <<endl;}
#endif
二、C++动态库的调用
2.1 系统显式调用库
其中包括系统函数如下:
2.1.1 dlopen()
函数定义如下:
void * dlopen( const char * pathname, int mode);
第二个参数是加载库的模式:
RTLD_LAZY:暂缓决定,在dlopen返回前,对于动态库中的未定义的符号不执行解析(只对函数引用有效,对于变量引用总是立即解析)
RTLD_NOW:立即决定,在dlopen返回前,解析出所有未定义的符号,如果解析不出来,在dlopen会返回NULL,错误为 undefined symbol:XXX...
作用范围:
RTLD_GLOBAL: 动态库中定义的符号可被其后打开的其他库重定位
RTLD_LOCAL: 与RTLD_GLOBAL作用相反,动态库中定义的符号不能被其后打开的其他库重定位。如果没有指明是RTLD_GLOBAL还是RTLD_LOCAL,那么
默认是RTLD_LOCAL。
返回值:
成功返回库引用的handle,失败返回NULL
2.1.2 dlsym()
函数功能:从动态库中获取符号(全局变量与函数符号)地址,通常用于获取函数符号地
址。
函数定义:
void *dlsym(void *handle, const char *symbol);
返回值:
成功返回函数符号地址,失败返回NULL
2.1.3 dlclose()
函数功能:关闭动态库句柄,只有当此动态库的使用计数为0时,才会被真正的卸载。
函数定义:
int dlclose(void *handle);
返回值:
成功返回0,失败返回非0
2.1.4 dlerror()
函数功能:当动态链接库操作函数执行失败时,dlerror可以返回出错信息,返回值为 NULL时表示操作函数执行成功。
函数定义:
char *dlerror(void);
2.2 测试demo
g++编译命令如下:
g++ -o main1 main1.cpp -ldl -g
2.2.1 关于ldl选项
-ldl 是 g++ 编译器链接选项,它会将动态链接库 libdl.so 链接进可执行文件中,以便程序 可以调用 libdl 中定义的函数。使用该命令即可
2.2.2 demo代码
#include <iostream>
#include <dlfcn.h>
using namespace std;
typedef void (*Getinfo)();
int main()
{void* handle = dlopen("./libtest1.so", RTLD_LAZY);if (!handle){return 0;}void* temp = dlsym(handle, "Get1234Info");if (temp){Getinfo getinfo = reinterpret_cast<Getinfo>(temp);(*getinfo)();}else{cout << "dlsym error: " << dlerror() << endl;}dlclose(handle);return 0;
}
代码测试