博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
linux动态库的种种要点
阅读量:6920 次
发布时间:2019-06-27

本文共 4501 字,大约阅读时间需要 15 分钟。

linux下使用动态库,基本用起来还是非常easy。但假设我们的程序中大量使用动态库来实现各种框架/插件,那么就会遇到一些坑,掌握这些坑才有利于程序更稳健地执行。

本篇先谈谈动态库符号方面的问题。

測试代码能够在

符号查找

一个应用程序test会链接一个动态库libdy.so,假设一个符号。比如函数callfn定义于libdy.so中,test要使用该函数,简单地声明就可以:

// dy.cpp libdy.sovoid callfn() {    ...}// main.cpp testextern void callfn();callfn();

在链接test的时候,链接器会统一进行检查。

相同,在libdy.so中有相同的规则,它能够使用一个外部的符号,在它被链接/加载进一个可运行程序时才会进行符号存在与否的检查。这个符号甚至能够定义在test中,形成一种双向依赖,或定义在其它动态库中:

// dy.cpp libdy.soextern void mfunc();mfunc();// main.cpp testvoid mfunc() {    ...}

在生成libdy.so时mfunc能够找不到,此时mfunc为没有定义:

$ nm libdy.so | grep mfunU _Z5mfuncv

但在libdy.so被链接进test时则会进行检查,试着把mfunc函数的定义去掉,就会得到一个链接错误:

./libdy.so: undefined reference to `mfunc()'

相同。假设我们动态加载libdy.so,此时当然能够链接通过,可是在加载时相同得到找不到符号的错误:

#ifdef DY_LOAD    void *dp = dlopen("./libdy.so", RTLD_LAZY);    typedef void (*callfn)();    callfn f = (callfn) dlsym(dp, "callfn");    f();    dlclose(dp);#else    callfn();#endif

得到错误:

./test: symbol lookup error: ./libdy.so: undefined symbol: _Z5mfuncv

结论:基于以上,我们知道,假设一个动态库依赖了一些外部符号,这些外部符号能够位于其它动态库甚至应用程序中。

我们能够再链接这个动态库的时候就把依赖的其它库也链接上,或者推迟到链接应用程序时再链接。而动态载入的库,则要保证在载入该库时,进程中载入的其它动态库里已经存在该符号。

比如。通过LD_PRELOAD环境变量能够让一个进程先载入指定的动态库,上面那个动态载入启动失败的样例,能够通过预先载入包括mfunc符号的动态库解决:

$ LD_PRELOAD=libmfun.so ./test...

可是假设这个符号存在于可运行程序中则不行:

$ nm test | grep mfunc0000000000400a00 T _Z5mfuncv$ nm test | grep mfunc0000000000400a00 T _Z5mfuncv$ ./test..../test: symbol lookup error: ./libdy.so: undefined symbol: _Z5mfuncv

符号覆盖

前面主要讲的是符号缺少的情况。假设同一个符号存在多分。则更能引发问题。这里谈到的符号都是全局符号。一个进程中某个全局符号始终是全局唯一的。为了保证这一点。在链接或动态加载动态库时。就会出现忽略反复符号的情况。

这里就不提同一个链接单位(如可运行程序、动态库)里符号反复的问题了

函数

当动态库和libdy.so可运行程序test中包括同名的函数时会如何?依据是否动态载入情况还有所不同。

当直接链接动态库时,libdy.so和test都会链接包括func函数的fun.o。为了区分。我把func依照条件编译得到不同的版本号:

// fun.cpp#ifdef V2extern "C" void func() {    printf("func v2\n");}#elseextern "C" void func() {    printf("func v1\n");}#endif// Makefiletest: libdy obj.o mainfn    g++ -g -Wall -c fun.cpp -o fun.o # 编译为fun.o    g++ -g -Wall -c main.cpp #-DDY_LOAD    g++ -g -Wall -o test main.o obj.o fun.o -ldl mfun.o -ldy -L.libdy: obj    g++ -Wall -fPIC -c fun.cpp -DV2 -o fun-dy.o  # 定义V2宏,编译为fun-dy.o    g++ -Wall -fPIC -shared -o libdy.so dy.cpp -g obj.o fun-dy.o

这样,test中的func就会输出func v1;libdy.so中的func就会输出func v2

test和libdy.o确实都有func符号:

$ nm libdy.so | grep func0000000000000a60 T func$nm test | grep func0000000000400a80 T func

在test和libdy.so中都会调用func函数:

// main.cpp testint main(int argc, char **argv) {    func();    ...    callfn(); // 调用libdy.so中的函数    ...}// dy.cpp libdy.soextern "C" void callfn() {    ...     printf("callfn\n");    func();    ...}

执行后发现。都调用的是同一个func

$ ./test...func v1...callfnfunc v1

结论,直接链接动态库时,整个程序执行的时候符号会发生覆盖,仅仅有一个符号被使用。在实践中,假设程序和链接的动态库都依赖了一个静态库,而后他们链接的这个静态库版本号不同,则非常有可能由于符号发生了覆盖而导致问题。(静态库同普通的.o性质一样,參考)

更复杂的情况中。多个动态库和程序都有同样的符号。情况也是一样。会发生符号覆盖。假设程序里没有这个符号,而多个动态库里有同样的符号,也会覆盖。

可是对于动态加载的情况则不同。相同的libdy.so我们在test中不链接,而是动态加载:

int main(int argc, char **argv) {    func();#ifdef DY_LOAD    void *dp = dlopen("./libdy.so", RTLD_LAZY);    typedef void (*callfn)();    callfn f = (callfn) dlsym(dp, "callfn");    f();    func();    dlclose(dp);#else    callfn();#endif    return 0;}

执行得到:

$ ./testfunc v1...callfnfunc v2func v1

都正确地调用到各自链接的func

结论。实践中,动态加载的动态库通常会作为插件使用,那么其同程序链接不同版本号的静态库(同样符号不同实现),是没有问题的。

变量

变量本质上也是符号(symbol),但其处理规则和函数还有点不一样(是不是有点想吐槽了)。

// object.hclass Object {public:    Object() {#ifdef DF        s = malloc(32);        printf("s addr %p\n", s);#endif        printf("ctor %p\n", this);    }    ~Object() {        printf("dtor %p\n", this);#ifdef DF        printf("s addr %p\n", s);        free(s);#endif    }    void *s;};extern Object g_obj;

我们的程序test和动态库libdy.so都会链接object.o。

首先測试test链接libdy.so,test和libdy.so中都会有g_obj这个符号:

// B g_obj 表示g_obj位于BSS段,未初始化段$ nm test | grep g_obj0000000000400a14 t _GLOBAL__I_g_obj00000000006012c8 B g_obj$ nm libdy.so | grep g_obj000000000000097c t _GLOBAL__I_g_obj0000000000200f30 B g_obj

执行:

$ ./testctor 0x6012c8ctor 0x6012c8...dtor 0x6012c8dtor 0x6012c8

g_obj被构造了两次,但地址一样。全局变量仅仅有一个实例,似乎在情理之中。

动态加载libdy.so,变量地址还是同样的:

$ ./testctor 0x6012a8...ctor 0x6012a8...dtor 0x6012a8dtor 0x6012a8

结论,不同于函数,全局变量符号反复时。不论动态库是动态加载还是直接链接,变量始终仅仅有一个。

但诡异的情况是,对象被构造和析构了两次。构造两次倒无所谓,浪费点空间,可是析构两次就有问题。由于析构时都操作的是同一个对象。那么假设这个对象内部有分配的内存,那就会对这块内存造成double free,由于指针同样。打开DF宏实验下:

$ ./tests addr 0x20de010ctor 0x6012b8s addr 0x20de040ctor 0x6012b8...dtor 0x6012b8s addr 0x20de040dtor 0x6012b8s addr 0x20de040

由于析构的两次都是同一个对象,所以其成员s指向的内存被释放了两次。从而产生了double free,让程序coredump了。

总结。全局变量符号反复时。始终会仅仅使用一个,而且会被初始化/释放两次,是一种较危急的情况,应当避免在使用动态库的过程中使用全局变量。

written by  posted at

转载地址:http://yfecl.baihongyu.com/

你可能感兴趣的文章
webpack.optimize.CommonsChunkPlugin的minChunks解析
查看>>
java9系列(九)Make G1 the Default Garbage Collector
查看>>
Javascript 项目常用的一些配置文件
查看>>
小白文,关于vuejs中的vuex计数小示例
查看>>
ZStack源码剖析之设计模式鉴赏——三驾马车
查看>>
学习笔记:util
查看>>
记一次Nodejs安全工单的处理过程_20171226
查看>>
Cloudera(CDH) 简介和图解在线安装
查看>>
Apache本地服务器配置
查看>>
重论JavaScript伪数组的种种
查看>>
入门到放弃node系列之网络模块(一)
查看>>
gitlab-runner-maven卡死的情况
查看>>
初学vue整理
查看>>
threejs中矩阵旋转原理
查看>>
Spring事务管理
查看>>
初学Vue
查看>>
Android Studio (一. 安装)
查看>>
20170625-bind方法的实现
查看>>
重拾css(8)——盒子模型
查看>>
Web图片资源的加载与渲染时机
查看>>