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

偶尔善良 提交于 2020-03-04 03:35:37
之前一篇《记一个链接库导出函数被覆盖的问题》中,描述了这样一个问题:动态库若想使用自己的符号,不受可执行程序(或其他动态库的影响),可以使用-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,或者链接时不导出符号的办法来解决问题。
但是有时候,对于那些真正是全局对象的全局对象,就应该从代码上来规避其被重复定义的可能。问题留到链接阶段就已经不可治愈了。
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!