记一个链接库导出函数被覆盖的问题

链接库的一个问题

前些天遇到这样一个问题:libD.so需要用到libS.a提供的方法,于是静态链接了libS.a。而libS.a和libD.so又都会被可执行文件bin所链接。(因为libD.so还提供给其他可执行程序使用,所以链接libS.a是必须的。而libD.so对于bin来说是可选的,所以bin也必须链接libS.a。)这就形成下面一种情况:
libD.so  <----  bin
    |                 |
libS.a   <--------
那么,如果今后libS.a有所修改(成了libS_new.a),bin重新编译了(重新链接了libS_new.a),而libD.so不变(还是链接的libS.a)。libD.so会受(libS_new.a的)影响吗?需求是libD.so不要受影响。

我之前的理解是:不会受影响。因为旧的libS.a已经静态链接到libD.so里面了,libD.so调用到的libS.a接口已经被链接上。只剩下函数的地址,而符号(函数名)已经不需要了。虽然说libD.so的代码是位置无关的,libS.a被静态链接在其中之后也需要位置无关,但是对于libS.a里面的接口的调用应该是可以使用相对地址寻址来实现的。
libD.so  <----  bin
     |                 |
libS.a        libS_new.a

但是实际情况却不是这样,尽管libD.so没有重新链接libS_new.a,但是它依然会调用到libS_new.a里面。
     libD.so  <----  bin
      |     |              |
libS.a     ----> libS_new.a

测试过程如下:

S.cpp
#include <stdio.h>
namespace S {
    int testS() {
        printf("it's old\n");
        return 0;
    }
}
g++ -fPIC -c S.cpp
ar r libS.a S.o

D.cpp
#include <stdio.h>
namespace S {void testS();}
namespace D {
    int test() {
        return 1;
    }
    void testD() {
        S::testS();
        printf("%s,%d, test=%d\n",__FILE__,__LINE__, test());
    }
}
(注意,这里的D::test()后面另有用处。)
g++ -g -fPIC -shared D.cpp libS.a -o libD.so

bin.cpp
namespace S {void testS();}
namespace D {void testD();}
int main(int argc, char *argv[]) {
    S::testS();
    D::testD();
    return 0;
}
g++ bin.cpp libS.a libD.so -o bin

执行bin程序之后,输出:
it's old
it's old
D.cpp,9, test=1
这没问题。

之后S.cpp的代码发生了变化(变成S_new.cpp):
S_new.cpp
#include <stdio.h>
namespace S {
    int testS() {
        printf("it's new\n");
        return 0;
    }
}
g++ -fPIC -c S_new.cpp
ar r libS_new.a S_new.o

主程序bin重新编译(重新静态链接libS_new.a),而libD.so保持原样(还是静态链接libS.a):
g++ bin.cpp libS_new.a libD.so -o bin

执行bin程序之后,输出:
it's new
it's new
D.cpp,9, test=1
尽管libD.so没有重新编译(它链接的依然是libS.a),它里面调用的S::testS()还是调用到了libS_new.a。

查找原因

objdump看一下libD.so的代码:
objdump -d libD.so
 ......
0000000000000730 <_ZN1S5testSEv@plt>:
730:   ff 25 5a 04 10 00       jmpq   *1049690(%rip)        # 100b90 <_GLOBAL_OFFSET_TABLE_+0x18>
736:   68 00 00 00 00          pushq  $0x0
73b:   e9 e0 ff ff ff          jmpq   720 <_init+0x18>
......
0000000000000818 <_ZN1D5testDEv>:
818:   55                      push   %rbp
819:   48 89 e5                mov    %rsp,%rbp
81c:   e8 0f ff ff ff          callq  730 <_ZN1S5testSEv@plt>
821:   e8 3a ff ff ff          callq  760 <_ZN1D4testEv@plt>
826:   89 c1                   mov    %eax,%ecx
......
0000000000000848 <_ZN1S5testSEv>:
848:   55                      push   %rbp
849:   48 89 e5                mov    %rsp,%rbp
84c:   48 8d 3d 79 00 00 00    lea    121(%rip),%rdi        # 8cc <_fini+0x24>
853:   b8 00 00 00 00          mov    $0x0,%eax
858:   e8 e3 fe ff ff          callq  740 <printf@plt>
......

可以发现尽管libD.so静态链接了libS.a,libS.a里面的S::testS()也确实被包含在了libD.so里面,但是对S::testS()函数的调用却依然是走的GOT符号表(GLOBAL_OFFSET_TABLE)。这与我原先的理解是不一致的。

再objdump看一下bin的代码:
objdump -d bin
 ......
00000000004006f8 <main>:
4006f8:       55                      push   %rbp
4006f9:       48 89 e5                mov    %rsp,%rbp
4006fc:       48 83 ec 10             sub    $0x10,%rsp
400700:       89 7d fc                mov    %edi,0xfffffffffffffffc(%rbp)
400703:       48 89 75 f0             mov    %rsi,0xfffffffffffffff0(%rbp)
400707:       e8 0c 00 00 00          callq  400718 <_ZN1S5testSEv>
40070c:       e8 07 ff ff ff          callq  400618 <_ZN1D5testDEv@plt>
......
0000000000400718 <_ZN1S5testSEv>:
400718:       55                      push   %rbp
......

可以发现,bin对于S::testS()的调用是静态的(直接call的相对地址)。但是,libD.so对S::testS()的调用为什么会调用到bin里面来呢?
readelf看一下bin的符号表:
readelf bin -s
Symbol table '.dynsym' contains 15 entries:
Num:    Value          Size Type    Bind   Vis      Ndx Name
......
2: 0000000000400718    28 FUNC    GLOBAL DEFAULT   12 _ZN1S5testSEv
3: 0000000000500978     0 OBJECT  GLOBAL DEFAULT  ABS _DYNAMIC
......
S::testS()的符号被bin导出了,随着bin被加载,S::testS()符号也被加载到了符号表。这件事情是发生在libD.so加载之前的,等到libD.so被加载的时候,S::testS()符号已经存在,所以libD.so就调用到了bin里面的S::testS()(也就是libS_new.a里面)。

其实不光是libD.so调用libS.a的函数会发生这样的情况,libD.so调用自己的函数都是这样:
objdump -d libD.so
 ......
0000000000000818 <_ZN1D5testDEv>:
818:   55                      push   %rbp
819:   48 89 e5                mov    %rsp,%rbp
81c:   e8 0f ff ff ff          callq  730 <_ZN1S5testSEv@plt>
821:   e8 3a ff ff ff          callq  760 <_ZN1D4testEv@plt>
......
调用libD.so内部的test()函数都是走的符号表!

修改一下bin.cpp,也来实现一个D::test():
bin.cpp
namespace S {void testS();}
namespace D {
    void testD();
    int test() {
        return 999;
    }

}
int main(int argc, char *argv[]) {
    S::testS();
    D::testD();
    return 0;
}
g++ bin.cpp libS_new.a libD.so -o bin

执行bin程序之后,输出:
it's new
it's new
D.cpp,9, test=999

没错吧,D::test()被覆盖了。

一些插曲

使用下面的方式编译bin:
g++ bin.cpp -c
ld -r libS_new.a bin.o -o bin.lo      
gcc bin.lo -o bin libD.so 

输出是:
it's old
it's old
D.cpp,9, test=999

居然D::test()是取的bin里面的,而S::testS()却是libS.a的!
为什么呢?还是objdump看一下bin:
 ......
0000000000400744 <main>:
400744:       55                      push   %rbp
400745:       48 89 e5                mov    %rsp,%rbp
400748:       48 83 ec 10             sub    $0x10,%rsp
40074c:       89 7d fc                mov    %edi,0xfffffffffffffffc(%rbp)
40074f:       48 89 75 f0             mov    %rsi,0xfffffffffffffff0(%rbp)
400753:       e8 e0 fe ff ff          callq  400638 <_ZN1S5testSEv@plt>
400758:       e8 fb fe ff ff          callq  400658 <_ZN1D5testDEv@plt>
......
对S::testS()的调用走的是符号表,而bin里面根本就没有S::testS()。

这其实是跟ld的参数顺序有关的,libS_new.a bin.o,ld试图用bin.o导出的符号去解决libS_new.a的未决符号。而现在我们需要的是用libS_new.a导出的符号(S::testS())去解决bin.o的未决符号。

重新编译一下,把libS_new.a和bin.o的位置换一换:
g++ bin.cpp -c
ld -r bin.o libS_new.a -o bin.lo
gcc bin.lo -o bin libD.so 

执行结果就对了:
it's new
it's new
D.cpp,9, test=999

解决办法

那么,怎样才能让bin调用到libS_new.a、而libD.so调用到libS.a,使它们互不影响呢?一开始想了两种办法:
一、bin以dlopen的方式去打开libD.so,并dlsym查找D::testD():
libD.so  <-(dlopen)-  bin
    |                            |
libS.a                 libS_new.a
这个方法并不可行,D::testD()去调用S::testS()时,同样走GOT,同样找到了bin里面的S::testS();

二、将libS.a打包成动态库libS_d.so,libD.so以dlopen的方式去打开libS_d.so,并dlsym查找S::testS():
libD.so  <--------  bin
    |(dlopen)          |
libS_d.so(libS.a)  libS_new.a
这个方法可行,因为dlsym指明了是在libS_d.so查找符号,所以能正确找到libS_d.so(也就是原来的libS.a)里面的S::testS(),而不是bin里面的S::testS();

不过,继续之前的分析思路想一想。只要链接顺序不错,bin里面已经静态链接了libS_new.a里面的S::testS(),这个地方是不会调错的。而libD.so之所以不能调用到libS.a里面的S::testS(),是因为libD.so通过符号表去调用S::testS()、并且bin先把S::testS()这个符号给导出了。
如果能让bin不导出S::testS()符号呢?或者让libD.so不通过符号表去调用S::testS()呢?

尝试了很多编译选项,似乎都没法阻止bin导出S::testS()符号……
-s (Remove all symbol table and relocation information from the executable.)
这个选项实际上是删除了.symtab段的符号,而不是.dynsym段。前者是链接过程中使用的符号、后者是运行时动态链接使用的符号,前者是后者的超集。而.symtab段在运行时根本就不会mmap到进程的地址空间;

-fvisibility=hidden
这个选项可以阻止动态链接库导出符号,配合__attribute__ ((visibility("default")))语法可以实现类似windows下的dll可以指定导出哪些函数的功能。但是好像对可执行程序无效;

-Wl,--no-export-dynamic
-Wl,--exclude-symbols=_ZN1S5testSEv
-Wl,--exclude-libs=libS_new.a
均无效;

那么能不能让libD.so不通过符号表去调用S::testS()呢?
-Wl,-Bsymbolic(When creating a shared library, bind references to global symbols to the definition within the shared library, if any.)选项能够做到这一点。

重新编译libD.so
g++ -g -fPIC -shared -Wl,-Bsymbolic D.cpp libS.a -o libD.so

执行程序,输出:
it's new
it's old
D.cpp,9, test=1

再objdump看看libD.so:
 ......
00000000000007c8 <_ZN1D5testDEv>:
7c8:   55                      push   %rbp
7c9:   48 89 e5                mov    %rsp,%rbp
7cc:   e8 27 00 00 00          callq  7f8 <_ZN1S5testSEv>
7d1:   e8 e6 ff ff ff          callq  7bc <_ZN1D4testEv>
7d6:   89 c1                   mov    %eax,%ecx
......
00000000000007f8 <_ZN1S5testSEv>:
7f8:   55                      push   %rbp
7f9:   48 89 e5                mov    %rsp,%rbp
......

test()和S::testS()都是直接调用的了,没有再走符号表。
加了-Wl,-Bsymbolic选项,生成的libD.so的确如最初所想的那样,在静态链接时就已经把符号给解决了(只剩下函数的相对地址,而没有符号)。

不过不幸的是,-Wl,-Bsymbolic选项是存在隐患的。因为它有些暴力的使动态链接库内部的符号都本地化了,使得一些需要共享的东西得不到共享。详见《Bsymbolic can cause dangerous side effects》的描述。

那篇文章也给出了解决办法,就是使用version_script来明确指定哪些符号要使用本地的(完成静态链接),哪些不限定本地(走符号表)。(尽管这种做法增加了维护成本,并且据说可移植性不佳。)
写一个version_script,指定S::testS()要使用本地的(D::test()故意不管):
D.vs
VERS_1.1 {local: _ZN1S5testSEv;};

重新编译libD.so:
g++ -g -fPIC -shared D.cpp libS.a -o libD.so -Xlinker --version-script -Xlinker D.vs

执行程序:
it's new
it's old
D.cpp,9, test=999
果然,S::testS()没被覆盖,而D::test()却还是被覆盖了。

version_script这一招对于bin文件似乎照样无效。

一个偏方

最后,对于libD.so调用libS.a的S::testS()函数会被覆盖的情况,还是有利用价值的。
比如,libS.a是一个第三方库,我们编写了libD.so,并用到了这个第三方库。现在,想为libD.so做一下单元测试。
由于libS.a这个第三方库的习性可能不是很容易掌握,或是是受到环境限制,使得我们很难确切构造libS.a接口的返回值。但是,对libD.so的单元测试又需要构造各种各样的返回值。这下怎么办呢?
如本文所述,我们可以山寨一个libS_new.a出来,然后就能随心所欲的构造我们需要的返回值了。

posted on 2013-05-12 00:27  胡永光  阅读(186)  评论(0编辑  收藏  举报

导航