c动态加载c/c++ so并调用其中的函数或者子类实现
在不少服务器应用中,会采用插件化或者模块化的体系实现具体的业务功能,比如mysql支持插件化体系,nginx采用模块化体系。总得来说,很多时候,因为扩展性,系统会采用动态加载so的方式扩展业务功能,而主框架不需要每次新增功能就不得不重新编译,很多时候,对于二进制发行的应用来说,不可能这么做。
最近抽空研究了下,Linux提供了一套dlXXX的API来动态装载库。
- dlopen,打开一个库,并为使用该库做些准备。
void *dlopen(const char *filename, int flag);
dlopen打开模式如下:
RTLD_LAZY 暂缓决定,等有需要时再解出符号
RTLD_NOW 立即决定,返回前解除所有未决定的符号。
- dlsym,在打开的库中查找符号的值。
void *dlsym(void *handle, const char *symbol);
- dlclose,关闭库。
- dlerror,返回一个描述最后一次调用dlopen、dlsym,或dlclose的错误信息的字符串。
C/C++语言用户需要包含头文件dlfcn.h(该头文件实际上是c语言编写的,不是c++,所以下面会提到,so中的函数需要增加链接指示extern "C",否则在加载so的时候,会提示找不到符号表Undefined symbols when loading shared library with dlopen())才能使用上述API。glibc还增加了两个POSIX标准中没有的API:
- dladdr,从函数指针解析符号名称和所在的文件。
- dlvsym,与dlsym类似,只是多了一个版本字符串参数。
在Linux上,使用动态链接的主应用程序需要和库libdl.so一起链接,也就是使用选项-ldl。首先看个例子:
dynso.cpp
//申明结构体类型 typedef struct __test { int i; void(*echo_fun)(struct __test *p); }Test_struct; static void __printf(Test_struct *p) { printf("i = %dn", p->i); } //动态库申请一个全局变量空间 //这种 ".成员"的赋值方式为c99标准 static Test_struct config = { .i = 0, .echo_fun = __printf, }; extern "C" { int dyn_so(char* dest) { strcat(dest, "abc"); return 1; } int object_cpp(); //申明注册函数原型 void __register(Test_struct *p); //加载动态库的自动初始化函数 void _init(void) { printf("init dynso.cpp\n"); //调用主程序的注册函数 __register(&config); } }
使用g++ -fpic -shared选项编译。因为__register只有声明,没有定义,因此正常编译的时候会报undefined reference to `__register(__test*)'。要解决这个问题,就要给链接器加上参数-E将主程序中所有全局符号放到动态符号表中即可, 由于生成可执行文件一般都是gcc直接生成, 因此可以使用gcc -Wl,-E来将-E参数传给ld来完成创建一个可以被动态链接的可执行文件,参见下面主程序的编译部分。TODO待解决
参考http://www.cppblog.com/markqian86/archive/2017/09/27/215269.html。
再看主程序:
#include <dlfcn.h>
//申明结构体 typedef struct __test { int i; void(*echo_fun)(struct __test *p); }Test; //供动态库使用的注册函数 void __register(Test *p) { p->i = 1; p->echo_fun(p); }
int main(int argc, const char ** argv) { // 动态so加载 void *handle = dlopen("/root/projects/dynso/bin/x64/Debug/libdynso.so.1.0", RTLD_NOW); if (!handle) { printf("open libdynso error ,dlerror=%s\n", dlerror()); return -1; } // call func typedef int(*fnc_ptr)(char* dest); fnc_ptr v_fnc_ptr = (fnc_ptr)dlsym(handle, "dyn_so"); if (!v_fnc_ptr) { printf("not found dyn_so function ,dlerror= %s \n", dlerror()); dlclose(handle); return -1; } char dest[100] = {0}; int ret_code = v_fnc_ptr(dest); cout << "call dyn loaded so,result=" << dest << endl; }
使用g++ -ldl -rdynamic编译。
-rdynamic类似于-g选项,只不过相比-g选项, -rdynamic 却是一个 连接选项 ,它将指示连接器把所有符号(而不仅仅只是程序已使用到的外部符号)都添加到动态符号表(即.dynsym表)里,以便那些通过 dlopen() 或 backtrace() (这一系列函数使用.dynsym表内符号)这样的函数使用。
当一个库通过dlopen()动态打开或以共享库的形式打开时,如果_init在该库中存在且被输出出来,则_init函数(如果使用g++编译,需要使用extern "C"使得对外可见)会被调用。如果一个库通过dlclose()动态关闭或因为没有应用程序引用其符号而被卸载时,_fini函数会在库卸载前被调用。当使用你自己的_init和_fini函数时,需要注意不要与系统启动文件一起链接。可以使用GCC选项 -nostartfiles 做到这一点。
上面是动态调用so中函数的方式,还有一种典型的用法是主框架定义了接口,具体的so实现特定的接口以扩展主程序的功能,也就是插件化体系。
动态模块包含c++ 11特性
g++ -std=c++11 -g -I./include -fPIC -shared -nostartfiles -o libdynso_cpp.so dynso.cpp
/tmp/ccoMSNmQ.o: In function `__static_initialization_and_destruction_0(int, int)':
/usr/include/c++/4.8.2/iostream:74: undefined reference to `__dso_handle'
/usr/bin/ld: /tmp/ccoMSNmQ.o: relocation R_X86_64_PC32 against undefined hidden symbol `__dso_handle' can not be used when making a shared object
/usr/bin/ld: final link failed: Bad value
collect2: error: ld returned 1 exit status
make: *** [all] Error 1
https://blog.csdn.net/weixin_30882895/article/details/95461913
真正解决方法:在源文件前面加上 extern "C"{ void * __dso_handle = 0 ;}
如果不生效,那就去掉 -nostartfiles,原因待查,参见https://gcc.gnu.org/onlinedocs/gcc/gcc-command-options/options-for-linking.html。
如果包含c++ 14特性,还得加个编译选项-fno-use-cxa-atexit。如下:
g++ -std=c++14 -g -Wall -fno-use-cxa-atexit -I./include -fPIC -shared -nostartfiles -o libdiff_two_dir.so diff_two_dir.cpp
参考:
http://www.tuicool.com/articles/EvIzUn
https://www.cnblogs.com/pcdack/p/16041098.html c函数本身调用的代价
ld本身是不能基于c主程序链接c++ object文件的,原因可以参见:https://jingyan.baidu.com/article/3c343ff7e9f1840d377963ea.html。
https://www.jb51.net/article/101744.htm
https://www.cnblogs.com/Anker/p/3746802.html
https://blog.csdn.net/tgdzsjh/article/details/41695881
https://blog.csdn.net/haifengid/article/details/51732600
http://blog.sina.com.cn/s/blog_664ffc6b01014ctj.html(特别感谢,运行的时候报找不到符号表就是通过该帖子的提示解决)
http://pubs.opengroup.org/onlinepubs/009695399/functions/dlopen.html
https://linux.die.net/man/3/dladdr
https://blog.csdn.net/xqhrs232/article/details/51305421
http://www.itkeyword.com/doc/0427885252816876x572/c-undefined-symbols-when-loading-shared-library-with-dlopen(使用基类指针方式调用)
https://my.oschina.net/saly/blog/130920
https://www.169it.com/tech-qa-linux/article-11063969113097593705.html c++中_init未被调用