动态链接库的隐式加载和显示加载

1.静态库和动态库、动态库的显式调用和隐式调用

静态库:静态链接库在链接时,编译器会将目标文件和静态库文件组织成一个可执行文件,程序运行时,将全部数据加载到内存。
如果程序体积较大,功能较为复杂,那么加载到内存中的时间就会比较长,最直接的一个例子就是双击打开一个软件,要很久才能看到界面。这是静态链接库的一个弊端。
动态库相比静态库的区别是:静态库是编译时就加载到可执行文件中的,而动态库是在程序运行时完成加载的,所以使用动态库的程序的体积要比使用静态库程序的体积小,并且使用动态库的程序在运行时必须依赖所使用的动态库文件(.so文件),而使用静态库的程序一旦编译好,就不再需要依赖的静态库文件了(.a文件)。

动态链接库有两种加载方式:隐式加载和显示加载:
隐式加载又叫载入时加载,指在主程序载入内存时搜索动态库,并将动态库载入内存。隐式加载也会有静态链接库的问题,如果程序稍大,加载时间就会过长,用户不能接受。
显式加载又叫运行时加载,指主程序在运行过程中需要动态库中的函数时再加载。显式加载是将较大的程序分开加载的,程序运行时只需要将主程序载入内存,软件打开速度快,用户体验好。

2.extern "C"的作用

C++程序(或库、目标文件)中,所有非静态(non-static)函数在二进制文件中都是以“符号(symbol)”形式出现的。这些符号都是唯一的字符串,从而把各个函数在程序、库、目标文件中区分开来。在C中,符号名正是函数名,两者完全一样。而C++允许重载(不同的函数有相同的名字但不同的参数,甚至const重载),并且有很多C所没有的特性──比如类、成员函数、异常说明──几乎不可能直接用函数名作符号名。为了解决这个问题,C++采用了所谓的name mangling。它把函数名和一些信息(如参数数量和大小)杂糅在一起,改造成奇形怪状,只有编译器才懂的符号名。例如,被mangle后的foo可能看起来像foo@4%6^,或者,符号名里头甚至不包括“foo”。

其中一个问题是,C++标准并没有定义名字必须如何被mangle,所以每个编译器都按自己的方式来进行name mangling。有些编译器甚至在不同版本间更换mangling算法(尤其是g++ 2.x和3.x)。说过,在显示调用动态库中的函数时,需要指明调用的函数名,即使您搞清楚了您的编译器到底怎么进行mangling的,从而知道调用的函数名被C++编译器转换为了什么形式,但可能仅仅限于您手头的这个编译器而已,而无法在下一版编译器下工作。

extern "C"即可以解决这个问题。用 extern "C"声明的函数将使用函数名作符号名,就像C函数一样。因此,只有非成员函数才能被声明为extern "C",并且不能被重载。尽管限制多多,extern "C"函数还是非常有用,因为它们可以象C函数一样被dlopen动态加载。冠以extern "C"限定符后,并不意味着函数中无法使用C++代码了,相反,它仍然是一个完全的C++函数,可以使用任何C++特性和各种类型的参数。所以extern "C" 只是告诉编译器编译和链接的时候都用c的方式的函数名字,函数里的内容可以为c的代码也可以为c++的。

3.显式调用

建立so1.h、so1.cc、so2.h、so2.cc、test.cc五个文件,内容如下:

// so1.h:
extern "C" void fcn();

// so1.cc:
#include <iostream>
#include "so1.h"
void fcn()
{
    std::cout << "this is fcn in so1" << std::endl;
}


// so2.h:
extern "C" void fcn();

// so2.cc:
#include <iostream>
#include "so2.h"
void fcn()
{
    std::cout << "this is fcn in so2" << std::endl;
}


// test.cc:
#include <iostream>
#include <cstdlib>
#include <dlfcn.h>

using namespace std;

int main(int argc, char **argv)
{
    if (argc != 2)
    {
        cout << "argument error!" << endl;
        exit(1);
    }

    // pointer to function
    typedef void (*pf_t)();

    char *err = NULL;
    // open the lib
    void *handle = dlopen(argv[1], RTLD_NOW); // 将库装载到内存
                                              // RTLD_NOW表示马上检查是否存在未定义的函数,若存在,则dlopen以失败告终。

    if (!handle)
    {
        cout << "load " << argv[1] << "failed! " << dlerror() << endl;
        exit(1);
    }

    // clear error info
    dlerror();

    pf_t pf = (pf_t)dlsym(handle, "fcn");  // 获得函数fcn在内存中的位置
    err = dlerror();
    if (err)
    {
        cout << "can't find symbol fcn! " << err << endl;
        exit(1);
    }

    // call function by pointer
    pf();

    dlclose(handle);

    return 0;
}

将so1.cc和so2.cc编译成动态库,编译命令如下:

g++ so1.cc -shared -fPIC -o libso1.so
g++ so2.cc -shared -fPIC -o libso2.so

test.cc中使用到了上面生成的动态库,编译命令如下:

g++ test.cc -l dl -Wl,-rpath=. -o test
等价于:g++ test.cc  -ldl -Wl,-rpath=. -o test

【注】在显式调用方式中,必须加入头文件dlfcn.h,makefile中的链接命令中要加入参数-ldl,否则报错。

运行test:

./test libso1.so 
或./test libso2.so 

可以看到这里,通过输入不同的参数,调用了不同的共享库中的fcn函数,是一种多态的表现,许多软件的不同插件就是这样实现的。
【注】多态:相同的代码,展现出不同的状态,或者说展现出不同的效果,就叫多态。

dlfcn.h中提供的API说明如下:
1.dlopen

函数原型:void *dlopen(const char *libname,int flag);
功能描述:dlopen必须在dlerror,dlsym和dlclose之前调用,表示要将库装载到内存,准备使用。如果要装载的库依赖于其它库,必须首先装载依赖库。如果dlopen操作失败,返回NULL值;如果库已经被装载过,则dlopen会返回同样的句柄。

参数中的libname一般是库的全路径,这样dlopen会直接装载该文件;如果只是指定了库名称,在dlopen会按照下面的机制去搜寻:
a.根据环境变量LD_LIBRARY_PATH查找
b.根据/etc/ld.so.cache查找
c.查找依次在/lib和/usr/lib目录查找。

flag参数表示处理未定义函数的方式,可以使用RTLD_LAZY或RTLD_NOW。RTLD_LAZY表示暂时不去处理未定义函数,先把库装载到内存,等用到没定义的函数再说;RTLD_NOW表示马上检查是否存在未定义的函数,若存在,则dlopen以失败告终。

2.dlerror

函数原型:char *dlerror(void);
功能描述:dlerror可以获得最近一次dlopen,dlsym或dlclose操作的错误信息,返回NULL表示无错误。dlerror在返回错误信息的同时,也会清除错误信息。

3.dlsym

函数原型:void *dlsym(void *handle,const char *symbol);
功能描述:在dlopen之后,库被装载到内存。dlsym可以获得指定函数(symbol)在内存中的位置(指针)。如果找不到指定函数,则dlsym会返回NULL值。但判断函数是否存在最好的方法是使用dlerror函数,

4.dlclose

函数原型:int dlclose(void *);
功能描述:将已经装载的库句柄减一,如果句柄减至零,则该库会被卸载。如果存在析构函数,则在dlclose之后,析构函数会被调用。

4.隐式调用

隐式调用不需要包含头文件dlfcn.h,只需要包含动态链接库中的头文件,使用动态库中的函数也不需要像显示调用那么复杂。
可以参考:静态库和动态库的制作。这个链接中调用动态库的方式就是隐式调用。

根据上面的显式调用和隐式调用的实例,可总结显示和隐式的区别如下:
1、 隐式调用需要调用者写的代码量少,调用起来和使用当前项目下的函数一样直接;而显式调用则要求程序员在调用时,指明要加载的动态库的名称和要调用的函数名称。
2、隐式调用由系统加载完成,对程序员透明;显式调用由程序员在需要使用时自己加载,不再使用时,自己负责卸载。
3、由于显式调用由程序员负责加载和卸载,好比动态申请内存空间,需要时就申请,不用时立即释放,因此显式调用对内存的使用更加合理, 大型项目中应使用显示调用。
4、当动态链接库中只提供函数接口,而该函数没有封装到类里面时,如果使用显式调用的方式,调用方甚至不许要包含动态链接库的头文件(需要调用的函数名是通过dlsym函数的参数指明的),而使用隐式调用时,则调用方必须要加上动态库中的头文件,g++编译时还需要要用参数-I指明包含的头文件的位置。需要注意的是,当动态链接库中的接口函数是作为成员函数封装在类里面时,即使使用显式调用的方式,调用方也必须包含动态库中的相应头文件(详见五、显示调用动态链接中的类成员函数)。
5、显式调用更加灵活,可以模拟多态效果。
6、显式调用的方式,必须加入头文件dlfcn.h,makefile中的链接命令中要加入参数-ldl,需要用dlopen加载库,dlsym取函数符号(函数名应用新定义的),dlclose卸载库。
7、隐式调用的方式,makefile中的链接命令中要加入参数-l加库名,直接用库里的函数名就可以。

5.显式调用动态链接中的类成员函数

显示调用动态链接库的类成员函数,有单独的写法,但比较少用。推荐的写法是为每个要被外部调用的类成员函数设计一个普通的接口函数,在接口函数内部使用类的成员函数。当然这就需要将类设计为单例模式,因为不可能在每个接口函数中都构造一个类的对象。【看不懂,以后再看】

参考:链接

posted @ 2022-06-26 22:13  好人~  阅读(1140)  评论(0编辑  收藏  举报