文档章节

linux动态库的种种要点

cJeek
 cJeek
发布于 2015/04/14 10:57
字数 1895
阅读 3
收藏 0

linux动态库的种种要点

NOV 4TH2014 12:00 AM

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

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

测试代码可以在github上找到

符号查找

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

// dy.cpp libdy.so void callfn() {     ... } // main.cpp test extern void callfn(); callfn();

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

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

// dy.cpp libdy.so extern void mfunc(); mfunc(); // main.cpp test void mfunc() {     ... }

在生成libdy.so时mfunc可以找不到,此时mfunc为未定义:

$ nm libdy.so | grep mfun U _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 mfunc 0000000000400a00 T _Z5mfuncv $ nm test | grep mfunc 0000000000400a00 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 V2 extern "C" void func() {     printf("func v2\n"); } #else extern "C" void func() {     printf("func v1\n"); } #endif // Makefile test: 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 func 0000000000000a60 T func $nm test | grep func 0000000000400a80 T func

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

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

运行后发现,都调用的是同一个func

$ ./test ... func v1 ... callfn func 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; }

运行得到:

$ ./test func v1 ... callfn func v2 func v1

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

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

变量

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

// object.h class 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_obj 0000000000400a14 t _GLOBAL__I_g_obj 00000000006012c8 B g_obj $ nm libdy.so | grep g_obj 000000000000097c t _GLOBAL__I_g_obj 0000000000200f30 B g_obj

运行:

$ ./test ctor 0x6012c8 ctor 0x6012c8 ... dtor 0x6012c8 dtor 0x6012c8

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

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

$ ./test ctor 0x6012a8 ... ctor 0x6012a8 ... dtor 0x6012a8 dtor 0x6012a8

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

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

$ ./test s addr 0x20de010 ctor 0x6012b8 s addr 0x20de040 ctor 0x6012b8 ... dtor 0x6012b8 s addr 0x20de040 dtor 0x6012b8 s addr 0x20de040

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

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

原文地址: http://codemacro.com/2014/11/04/linux-dynamic-library/
written by Kevin Lynx  posted at http://codemacro.com

Posted by Kevin Lynx Nov 4th2014 12:00 am  c/c++


本文转载自:http://codemacro.com/2014/11/04/linux-dynamic-library/

cJeek
粉丝 0
博文 2
码字总数 37
作品 0
浦东
私信 提问
Linux的相关资源帖

http://www.kerneltravel.net/?page_id=8 2.6内核模块编程实例指导 内核模块编程之入门(一)-话说模块 内核模块编程之入门(二)—必备知识 内核模块编程之入门(三)-模块实用程序简介 内核...

AlphaJay
2010/05/21
221
0
[动态库]深入分析Windows和Linux动态库应用异同

摘要:动态链接库技术实现和设计程序常用的技术,在Windows和Linux系统中都有动态库的概念,采用动态库可以有效的减少程序大小,节省空间,提高效率,增加程序的可扩展性,便于模块化管理。 ...

龙宝宝
2011/08/05
167
0
深入分析Windows和Linux动态库应用异同

摘要:动态链接库技术实现和设计程序常用的技术,在Windows和Linux系统中都有动态库的概念,采用动态库可以有效的减少程序大小,节省空间,提高效率,增加程序的可扩展性,便于模块化管理。 ...

晨曦之光
2012/03/02
108
0
windows平台开发,在eclipse中使用java如何调用.so文件,试了半天还是不行:

windows平台开发,在eclipse中使用java如何调用.so文件,试了半天还是不行: 1、java工程项目不是安卓项目,是web项目, 2、.so是linux下生成的动态库,.dll是windows下生成的动态库,网上几...

hhl3136
2017/11/21
2.4K
7
svn 从1.6升级到1.7后遇到的种种问题

linux下的svn merge命令不太会用,便下载了smartsvn来做merge。当从smartsvn中import project后执行操作的时候,它提示我必须把我project升级到1.7才能执行操作。我也没多想就点了yes。结果之...

苏叶晚晚
2013/11/04
9.6K
0

没有更多内容

加载失败,请刷新页面

加载更多

OSChina 周六乱弹 —— 早上儿子问我他是怎么来的

Osc乱弹歌单(2019)请戳(这里) 【今日歌曲】 @凉小生 :#今日歌曲推荐# 少点戾气,愿你和这个世界温柔以待。中岛美嘉的单曲《僕が死のうと思ったのは (曾经我也想过一了百了)》 《僕が死の...

小小编辑
今天
2.1K
14
Excption与Error包结构,OOM 你遇到过哪些情况,SOF 你遇到过哪些情况

Throwable 是 Java 中所有错误与异常的超类,Throwable 包含两个子类,Error 与 Exception 。用于指示发生了异常情况。 Java 抛出的 Throwable 可以分成三种类型。 被检查异常(checked Exc...

Garphy
今天
38
0
计算机实现原理专题--二进制减法器(二)

在计算机实现原理专题--二进制减法器(一)中说明了基本原理,现准备说明如何来实现。 首先第一步255-b运算相当于对b进行按位取反,因此可将8个非门组成如下图的形式: 由于每次做减法时,我...

FAT_mt
昨天
40
0
好程序员大数据学习路线分享函数+map映射+元祖

好程序员大数据学习路线分享函数+map映射+元祖,大数据各个平台上的语言实现 hadoop 由java实现,2003年至今,三大块:数据处理,数据存储,数据计算 存储: hbase --> 数据成表 处理: hive --> 数...

好程序员官方
昨天
61
0
tabel 中含有复选框的列 数据理解

1、el-ui中实现某一列为复选框 实现多选非常简单: 手动添加一个el-table-column,设type属性为selction即可; 2、@selection-change事件:选项发生勾选状态变化时触发该事件 <el-table @sel...

everthing
昨天
21
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部