浅谈动态库符号的私有化与全局化

之前一篇《记一个链接库导出函数被覆盖的问题》中,描述了这样一个问题:动态库若想使用自己的符号,不受可执行程序(或其他动态库的影响),可以使用-Wl,-Bsymbolic链接参数或version_script来让动态库的符号自我解决,而不必通过全局符号表来动态解决。
之前的文章也提到,使用-Wl,-Bsymbolic这样的方法是存在隐患的。最近又遇到这样的例子,动态库使用了私有的符号导致dynamic_cast、throw可能达不到程序预想的效果。

另外,除了显式的使用-Wl,-Bsymbolic、version_script这样的方法之外,通过dlopen打开的动态库也可能会使用到自己私有的符号。这种情况的发生,有如下条件:
1、可执行程序在链接的时候没有使用-Wl,-E选项。则链接器在生成可执行程序的时候默认不会将其符号导出到全局符号表;
2、在链接生成可执行程序的目标文件中没有列举将要dlopen的动态库(通常都是这样,静态链接的时候并不知道运行时需要dlopen什么样的动态库)。否则的话,就算没有-Wl,-E选项,可执行程序也会将目标文件中的动态库所需要的符号导出到全局符号表;
满足这两个条件的话,动态库在通过dlopen加载的时候,可能就看不到可执行程序中的相同符号,而使用自己私有的符号。

例:
[class.h]
class AAA {
public:
virtual void test();
int nnn;
};
class BBB : public AAA {
public:
virtual void test();
virtual void tttest();
};

[class.cpp]=> libclass.a
#include <stdio.h>
#include "class.h"
void AAA::test() { printf("AAA\n"); }
void BBB::test() { printf("BBB\n"); }
void BBB::tttest() { printf("OK!\n"); }

[test.cpp]=> libtest.so
#include <stdio.h>
#include "class.h"
void test(AAA *p) {
BBB *b = dynamic_cast<BBB*>(p);
if (!b) printf("ERROR: dynamic_cast failed!\n");
else b->tttest();
throw AAA();
}

[main.cpp]=> main
#include <dlfcn.h>
#include <stdio.h>
#include "class.h"
typedef void (*test_f)(AAA*);
int main() {
void *h = dlopen("libtest.so", RTLD_NOW|RTLD_GLOBAL);
if (!h) { printf("ERROR: dlopen %s\n", dlerror()); return 1; }
test_f f = (test_f)dlsym(h, "_Z4testP3AAA");
if (!f) { printf("ERROR: dlsym %s\n", dlerror()); return 2; }
BBB b;
try { f(&b); }
catch (AAA e) { printf("AAA catched!\n"); }
catch (...) { printf("NULL catched!\n"); }
return 0;
}

main和libtest.so都依赖于libclass.a。main会将libclass.a静态链接、而main通过dlopen的方式在运行时加载libtest.so。
对于不同的链接方式,会有如下几种现象:

1、main导出符号,而libtest.so并不使用私有的符号。结果正常:
$ g++ main.cpp libclass.a -ldl -Wl,-E
$ g++ test.cpp -o libtest.so -shared -fPIC
$ ./a.out
OK!
AAA catched!

2、main不导出符号,libtest.so也不链接libclass.a。结果是dlopen无法打开libtest.so,因为libtest.so需要libclass.a中的符号,但它去未链接libclass.a,并且main中虽然有这些符号,却未被导出:
$ g++ main.cpp libclass.a -ldl
$ g++ test.cpp -o libtest.so -shared -fPIC
$ ./a.out
ERROR: dlopen libtest.so: undefined symbol: _ZTI3AAA
$ c++filt _ZTI3AAA
typeinfo for AAA

3、main不导出符号,libtest.so自己去链接libclass.a。结果是libtest.so使用了私有的符号,dynamic_cast和throw都无法正常工作:
$ g++ main.cpp libclass.a -ldl
$ g++ test.cpp libclass.a -o libtest.so -shared -fPIC
$ ./a.out
ERROR: dynamic_cast failed!
NULL catched!

4、main导出符号,libtest.so自己去链接libclass.a并使用了-Wl,-Bsymbolic参数。结果同上:
$ g++ main.cpp libclass.a -ldl -Wl,-E
$ g++ test.cpp libclass.a -o libtest.so -shared -fPIC -Wl,-Bsymbolic
$ ./a.out
ERROR: dynamic_cast failed!
NULL catched!

为什么动态库使用私有符号会导致dynamic_cast和throw不正常呢?
因为这两个东西都是基于RTTI(Run Time TypeInfo)来实现的。也就是,C++程序在运行时,内存中会存有TypeInfo,即类的信息。(针对与多态有关的类,因为多态是运行时才能确定的。)
dynamic_cast怎么知道AAA*指针指向的对象是否是一个BBB的实例?通过AAA*指向的对象的vptr指针(虚函数指针,有多态就必有虚函数)可以找到该对象的TypeInfo;再通过BBB这个符号找到class BBB对应的TypeInfo;如果两个TypeInfo相同,或者后者是前者的祖先的话,那么cast成功;
throw时怎么知道丢过来的AAA是否就是我想要catch的AAA呢?同样是找到两者的TypeInfo,然后做个比较;
注意,两个用于比较的TypeInfo,其中之一是从内存对象中得来的(通过其vptr指针),另一个是通过类的符号得来的。
那么,如果动态库使用了私有的符号,则在动态库中通过类的符号得到的TypeInfo其实就已经不是主程序中的那个TypeInfo了,自然就与那个由对象的vptr得来的TypeInfo比不到一块去(因为对象是在主程序中创建的)。

动态库符号私有化会存在隐患。那么,如果动态库符号统统全局化,是不是一定就高枕无忧了呢?倒也未必。
一方面,动态库有时候的确会有私有化符号的需求(《记一个链接库导出函数被覆盖的问题》一文就是由这样一个需求而引出的)。
另一方面,符号全局化也可能会存在隐患。比如全局对象构造析构的问题。

动态库和可执行程序一样,如果其代码中定义了全局对象,那么在它们的_init和_fini代码中就会对自己定义过的全局对象做构造和析构(_init和_fini是由编译器生成的)。_init和_fini分别在动态库(或可执行程序)load和unload时被调用。
那么,如果可执行程序和动态库中定义了同名的全局对象会发生什么事呢?如果符号是全局化的,全局符号表中只会保留一个符号,也就是先来的那个符号。但是多个_init和_fini都会被调用。其结果就是同一个全局对象被多次构造和析构了。

例:
[D.cpp]=> libD.a
#include <stdio.h>
class D {
public:
D() { printf("inited D\n"); num = new int; }
~D() { delete num; printf("finished D\n"); }
void print() { printf("my num is %d\n", *num); }
void set(int n) { *num = n; }
private:
int *num;
};

D g_D;
void testD(int n) {
g_D.set(n);
g_D.print();
}

[Ds.cpp]=> libDs.so
void testD(int n);
void doTest() {
testD(100);
}

[bin.cpp]=> bin
#include <stdio.h>
void doTest();
void testD(int n);
int main(int argc, char *argv[]) {
printf("in main\n");
doTest();
testD(1);
printf("out main\n");
return 0;
}

可执行程序bin和动态库libDs.so分别都链接了静态库libD.a,从而都定义了全局变量g_D。
运行程序可以发现g_D将被构造和析构两次。

$ ./bin
inited D
inited D
in main
my num is 100
my num is 1
out main
finished D
*** glibc detected *** ./bin: double free or corruption (fasttop): 0x00000000101ee030 ***
......

像这个例子这样,全局对象被重复定义,经常就是源于一个定义了全局对象的静态库被到处引用。
在本例中,D.cpp定义的全局对象g_D从程序逻辑上来说其实应该是私有的。可以通过将其修饰成static,或者链接时不导出符号的办法来解决问题。
但是有时候,对于那些真正是全局对象的全局对象,就应该从代码上来规避其被重复定义的可能。问题留到链接阶段就已经不可治愈了。

时间: 2024-10-01 12:07:11

浅谈动态库符号的私有化与全局化的相关文章

Linux下C编程:浅谈动态内存

使用动态内存时需要用户自己去申请资源和释放资源.用户可以随时的分配所需空间,根据需要分配空间大小,并在最后释放申请内存. 动态内存也存在隐患:在大型的项目当中管理申请的动态内存是很复杂的,以及释放申请的内存有难想起的.在释放动态内存时可能不止一个指针指向了该内存,所以释放的时候是很容易出错的.内存无法释放就会造成内存泄露,这也就是为什么服务器要经常的每个一段时间重启的原因. 内存管理操作: 分配内存函数: #include <stdlib.h> void *malloc(size_t size

浅谈MVC+EF easyui dataGrid 动态加载分页表格_jquery

首先上javascript的代码 <script type="text/javascript"> $(function () { LoadGrid(); }) //加载表格!!! function LoadGrid() { $('#roleGrid').datagrid({ width: 900, striped: true, //交替条纹 fitColumns: true, //防止水平滚动 fit: true,//自动补全 iconCls: "icon-sav

浅谈Android Studio JNI生成so库

1.新建Android studio工程 2.新建class:AppKey.java.主要为了保存密钥 代码块 package com...adminapp.lib.utils.jni; /** * Created by seven on 16/9/8. */ public class AppKey { static { System.loadLibrary("AppKey"); } public static native String WechatId(); public stat

浅谈PHP5中垃圾回收算法(Garbage Collection)的演化

前言:PHP是一门托管型语言,在PHP编程中程序员不需要手工处理内存资源的分配与释放(使用C编写PHP或Zend扩展除外),这就意味着PHP本身实现了垃圾回收机制(Garbage Collection).现在如果去PHP官方网站(php.net)可以看到,目前PHP5的两个分支版本PHP5.2和PHP5.3是分别更新的,这是因为许多项目仍然使用5.2版本的PHP,而5.3版本对5.2并不是完全兼容.PHP5.3在PHP5.2的基础上做了诸多改进,其中垃圾回收算法就属于一个比较大的改变.本文将分别

浅谈关于JavaScript API设计的一些建议和准则

  这篇文章主要介绍了浅谈关于JavaScript API设计的一些建议和准则,文中列举了许多知名的JS API进行辅助说明,极力推荐!需要的朋友可以参考下 设计是一个很普遍的概念,一般是可以理解为为即将做的某件事先形成一个计划或框架. (牛津英语词典)中,设计是一种将艺术,体系,硬件或者更多的东西编织到一块的主线.软件设计,特别是作为软件设计的次类的API设计,也是一样的.但是API设计常常很少关注软件发展,因为为其他程序员写代码的重要性要次于应用UI设计和最终用户体验. 但是API设计,作为

浅谈如何有效建立权限管理体系(原创)

体系|原创 作者:BALLOONMAN2002  2004年6月26日 本文拟结合POWERBUILDER语言,简述如何在传统C/S应用系统当中有效建立权限管理体系. 何谓权限管理体系?就是如何控制操作使用者对软件功能和系统数据的访问权限的各个方面.传统的C/S应用系统,多是"前台应用程序+后台数据库表"两部分,这样就决定了我们考虑权限管理体系就必然要考虑两方面的内容: 1.用户在前台的功能权限:即该用户能够使用哪些菜单或窗口功能,例如:张三只能使用数据录入功能,不能使用管理审批功能:

总结Flash建站经验:浅谈flash web的结构

web 引言 记得我刚接触FLASH那会儿,应该是FLASH6末期吧,国内的flash web还是很少的,牛X的更是屈指可数,而且这个时候所谓的牛X,一般都是指效果很酷,技术强的基本没有.其实这是必然,国内早期的flash web开发者大都是由FLASH动画制作者或是网页设计师转变而来.他们非常热衷于片头和过渡效果,为此不惜牺牲浏览者的等待时间并吃掉浏览者的CPU.这就是为什么现在好多人一谈起flash web就觉得它体积大,效率低的根源了.当然如果是真对个人网站,这也无可厚非,个人网站信息量小

浅谈多态机制的意义及实现

标题:浅谈多态机制的意义及实现 作者:舒の随想日记 在面向对象编程(Object-Oriented Programming, OOP)中,多态机制无疑是其最具特色的功能,甚至可以说,不运用多态的编程不能称之为OOP.这也是为什么有人说,使用面向对象语言的编程和面向对象的编程是两码事. 多态并没有一个严格的定义,维基百科上给它下的定义比较宽松: Subtype polymorphism, almost universally called just polymorphism in the cont

浅谈iOS Crash(一)

本文讲的是浅谈iOS Crash(一),一.捕获iOS Crash 1.设置异常断点并运行 设置异常断点.png 说明:设置Xcode异常断点后运行程序,发生Crash时,断点会定位到出错的代码行,但仅适用于开发阶段.线上APP的Crash还需要通过收集Crash机制来捕获Crash并记录在日志中. 2.Mach异常 和 Unix信号 iOS Crash发生时,先产生Mach异常(最底层的内核级异常),然后Mach异常在host层被ux_exception转换为相应的Unix信号,并通过thre