zoukankan      html  css  js  c++  java
  • 记一个链接库导出函数被覆盖的问题

    链接库的一个问题

    前些天遇到这样一个问题: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出来,然后就能随心所欲的构造我们需要的返回值了。
  • 相关阅读:
    Python+Flask使用蓝图
    Python+selenium实现自动登录
    Python+Flask做个简单的表单提交程序
    第一个Flask程序
    PHP读取IIS网站列表
    在IIS7上导出所有应用程序池的方法 批量域名绑定
    Delphi判断一个字符串是否全是相同的数字
    WeTest六周年 | 匠心不改 初心不变
    WeTest压测大师链路性能监控 | 一站式压测、监控解决方案,开放免费体验预约
    WeTest自助压测1折起,最低1分钱参与Q币抽奖
  • 原文地址:https://www.cnblogs.com/wangfengju/p/6173081.html
Copyright © 2011-2022 走看看