在不少服务器应用中,会采用插件化或者模块化的体系实现具体的业务功能,比如mysql支持插件化体系,nginx采用模块化体系。总得来说,很多时候,因为扩展性,系统会采用动态加载so的方式扩展业务功能,而主框架不需要每次新增功能就不得不重新编译,很多时候,对于二进制发行的应用来说,不可能这么做。

  最近抽空研究了下,Linux提供了一套dlXXX的API来动态装载库。

- dlopen,打开一个库,并为使用该库做些准备。

void *dlopen(const char *filename, int flag);
  • 1.

dlopen打开模式如下:

  RTLD_LAZY 暂缓决定,等有需要时再解出符号
  RTLD_NOW 立即决定,返回前解除所有未决定的符号。

- dlsym,在打开的库中查找符号的值。

void *dlsym(void *handle, const char *symbol);
  • 1.

- 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);
    }

}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.

  使用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>
  • 1.
//申明结构体
typedef struct __test {
    int i;
    void(*echo_fun)(struct __test *p);
}Test;

//供动态库使用的注册函数
void __register(Test *p) {
    p->i = 1;
    p->echo_fun(p);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
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;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

  使用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://stackoverflow.com/questions/57957080/relocation-r-x86-64-pc32-against-undefined-hidden-symbol-dso-handle-can-not


真正解决方法:在源文件前面加上 extern "C"{ void * __dso_handle = 0 ;}

如果不生效,那就去掉 -nostartfiles,原因待查,参见https://gcc.gnu.org/onlinedocs/gcc/gcc-command-options/options-for-linking.html。

 ldd,  nm, readelf, ld

如果包含c++ 14特性,还得加个编译选项-fno-use-cxa-atexit。如下:

g++ -std=c++14 -g -Wall -fno-use-cxa-atexit -I./include -fPIC -shared -nostartfiles

参考:

 http://www.tuicool.com/articles/EvIzUn

 c函数本身调用的代价

ld本身是不能基于c主程序链接c++ object文件的,原因可以参见:https://jingyan.baidu.com/article/3c343ff7e9f1840d377963ea.html。

 https://www.jb51.net/article/101744.htm

 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

 http://www.itkeyword.com/doc/0427885252816876x572/c-undefined-symbols-when-loading-shared-library-with-dlopen(使用基类指针方式调用)

 https://www.169it.com/tech-qa-linux/article-11063969113097593705.html c++中_init未被调用