解决函数名冲突问题(dlopen,dlsym,dlclose)

您所在的位置:网站首页 外部函数与内部函数的调用方法相同吗 解决函数名冲突问题(dlopen,dlsym,dlclose)

解决函数名冲突问题(dlopen,dlsym,dlclose)

2024-07-13 23:59| 来源: 网络整理| 查看: 265

背景

最近在加载动态库的时候有遇到一个问题,如图1所示,多个动态库同时提供同名函数但是具体实现却不同的情况,那么有个问题,当我们调用的时候怎么确认调用的是某个静态库中的函数?其实,当我们链接所有的静态库的时候,如果遇到多个静态库中符号名冲突的问题,先装载的符号会优先,那么后续相同符号就无法载入。也就是说,如果libA.a的GetMemSize先加载,那么libB.a中的GetMemSize就会无法加载进去,这时候调用libB.a中GetMemSize就无法得到正确的结果。 在这里插入图片描述

图1 接口相同但实现不通的函数

这种情况按道理应该不会出现才对,但是有可能是libA.a和libB.a是不同的人维护,所以就会出现这种情况。

如果出现这种情况,可以使用显式运行时链接的方式去加载各个库文件。前面介绍如果碰到许多共享模块中符号名冲突的问题,结论是当多个同名符号冲突时,先装入的符号优先,这种优先级方式称为装载序列。但是当我们通过dlopen()装入的共享对象时,并且参数filename不是NULL时,这时候dlsym()对符号的查找优先级是采用依赖序列,依赖序列的意思是它以被dlopen()打开的那个共享对象为根节点,对它所有依赖的共享对象进行广度优先遍历,直到找到符号为止。所以,使用dlopen()打开 libB.a,并且以dlsym()去查找符号GetMemSize,就不会和libA.a中的GetMemSize发生冲突。提到的dlopen()和dlsym()下面会具体介绍。

但需要注意的是,dlopen()打开的是动态库,因此需要将静态库libB.a转换成动态库libB.so,具体的转换方式是:

#不同的平台使用各自的编译工具链 ar -x libB.a gcc -shared *.o -o libB.so 显式运行时链接

支持动态链接的系统往往都支持一种更加灵活的模块加载方式,叫做显式运行时链接(Explicit Run-time Linking),有时候也叫做运行时加载。也就是让程序自己在运行时控制加载指定的模块,并且可以在不需要该模块时将其卸载。这种运行时加载使得程序的模块组织变得很灵活,可以用来实现一些诸如插件、驱动等功能。当程序需要用到某个插件或者驱动的时候,才将相应的模块装载进来,而不需要从一开始就将他们全部装载进来,从而减少了程序启动时间和内存使用。并且程序可以在运行时的时候重新加载某个模块,这样使得程序本身不必重新启动而实现模块的增加、删除、更新等,这对于很多需要长期运行的程序来说是很大的优势。

动态库的装载通过4个函数完成:打开动态库(dlopen)、查找符号(dlsym)、错误处理(dlerror)以及关闭动态库(dlclose),程序可以通过这几个API对动态库进行操作。这几个API的实现是在/lib/libdl.so.2里面,它们的声明和相关常量在系统标准头文件。

dlopen()

dlopen()函数用来打开一个动态库,并将其加载到进程的地址空间,完成初始化过程,它的原型定义为:

void *dlopen(const char *filename, int flags);

第一个参数是被加载动态库的路径,如果这个路径是绝对路径(以”/”开始的路径),则该函数会将尝试直接打开动态库;如果是相对路径,那么dlopen()会尝试在以一定的顺序查找该动态库文件。一般直接使用绝对路径就可以了。

如果filename为NULL,则返回的句柄是main函数的句柄,也就是全局符号表的句柄,也就是说我们可以在运行时找到全局符号表里面的任何一个符号,并且可以执行它们。全局符号表包括了程序的可执行文件本身、被动态链接库加载到进程中的所有共享符号以及在运行时通过dlopen打开并且使用了RTLD_GLOBAL方式的模块中的符号。

第二个参数flag表示函数符号的解析方式,其中RTLD_LAZY和RTLD_NOW必须要设置其中一种。常量RTLD_LAZY表示使用延迟绑定,当函数第一次被用到时才进行绑定,即PLT机制;而RTLD_NOW表示当模块被加载时即完成所有的函数绑定工作,如果有任何未定义的符号引用的绑定工作没法完成,那么dlopen()就返回错误。上面的两种绑定方式必须选其一。

另外还有一些常量可以跟上面的两者中任意一个一起使用(通过常量的“或”操作)。

RTLD_GLOBAL 表示将加载的模块的全局符号合并到进程的全局符号表中,使得以后加载的模块可以使用这些符号。

RTLD_LOCAL RTLD_LOCAL则与RTLD_GLOBAL相反,使以后加载的模块不能使用这些符号,如果没有定义,默认就是RTLD_LOCAL。

RTLD_NODELETE 在dlclose()期间不要卸载共享对象。因此,如果稍后使用dlopen()重新加载对象,则共享对象的静态变量不会被重新初始化。

RTLD_DEEPBIND 将这个共享对象中符号的查找作用域放在全局作用域之前。这意味着包含对象将优先使用自己的符号,而不是已经加载的其他对象中包含的具有相同名称的全局符号。

dlopen的返回值是被加载的模块的句柄,这个句柄在后面使用dlsym或者dlclose时要用到。如果加载模块失败,则返回NULL。如果模块已经通过dlopen被加载过了,那么返回的是同一个句柄。另外如果被加载的模块之间有依赖关系,比如模块A依赖于模块B,那么程序员必须手工加载被依赖的模块,比如先加载B,再加载A。

dlsym()

dlsym函数就是运行时装载的核心,通过这个函数找到所需要运行的符号,函数原型如下:

void *dlsym(void *handle, const char *symbol);

有两个参数,第一个参数是有dlopen()返回的动态库的句柄;第二个参数即所查找的符号的名字,一个以\0结尾的C字符串。如果dlsym找到了相应的符号,则返回该符号的值;没有找到相应的符号,则返回NULL。dlsym()返回的值对于不同类型的符号,意义是不同的。如果查找的符号是个函数,那么它返回函数的地址;如果是个变量,它返回变量的地址;如果这个符号是个常量,那么它返回的是该常量的值。这里有个问题是:如果常量的值刚好是NULL或者0呢,我们如何判断dlsym()是否找到了该符号呢?这就需要用到dlerror()函数了。如果符号找到了,那么dlerror()返回NULL,如果没有找到,dlerror()就会返回相应的错误信息。

dlerror()

每次我们调用dlopen()、dlsym()或dlclose()以后,都可以调用dlerror()函数来判断上一次调用是否成功。dlerror()的返回值类型是char*,如果返回NULL,则表示上一次调用成功,如果不是,则返回相应的错误消息。

dlclose()

dlclose()的作用跟dlopen()刚好相反,它的作用是将上一个已经加载的模块卸载。系统会维持一个加载引用计数器,每次使用dlopen()加载某模块时,相应的计数器加一;每次使用dlclose()卸载某模块时,相应计数器减一。只有当计数器值减到0时,模块才被真正的卸载掉。

例程

下面是一个简单的例子,介绍显式运行时链接数学库,然后获取cos()函数符号地址,调用cos()并返回结果。

#include #include #include #include /* Defines LIBM_SO (which will be a string such as "libm.so.6") */ int main(void) { void *handle; double (*cosine)(double); char *error; handle = dlopen(LIBM_SO, RTLD_LAZY); if (!handle) { fprintf(stderr, "%s\n", dlerror()); exit(EXIT_FAILURE); } dlerror(); /* Clear any existing error */ cosine = (double (*)(double)) dlsym(handle, "cos"); error = dlerror(); if (error != NULL) { fprintf(stderr, "%s\n", error); exit(EXIT_FAILURE); } printf("%f\n", (*cosine)(2.0)); dlclose(handle); exit(EXIT_SUCCESS); } 总结

本文简单介绍使用运行时加载的方式解决符号名冲突问题,并介绍了运行时加载的几个函数打开动态库(dlopen)、查找符号(dlsym)、错误处理(dlerror)以及关闭动态库(dlclose)的使用方法。

参考资料《程序员的自我修养—链接、装载与库》和man手册。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3