在 运行 时间加载动态库会产生不一致和意外的结果、缺少符号和空 PLT 条目。为什么?

Loading a dynamic library at run-time yields inconsistent and unexpected results, missing symbols and empty PLT entries. Why?

我已经和这个问题斗争了很长一段时间,但一直找不到解决方案,甚至找不到解释。很抱歉,如果问题很长,但请耐心等待,因为我只是想 100% 清楚地说明问题,希望比我更有经验的人能够弄明白。

我为所有片段保留了 C 语法高亮显示,因为它使它们更清晰一点,即使不是真的正确。

我想做什么

我有一个 C 程序,它使用动态库 (libzip) 中的一些函数。在这里它被归结为一个最小的可重现示例(它基本上什么都不做,但它工作得很好):

#include <zip.h>

int main(void) {
    int err;
    zip_t *myzip;

    myzip = zip_open("myzip.zip", ZIP_CREATE | ZIP_TRUNCATE, &err);
    if (myzip == NULL)
        return 1;

    zip_close(myzip);

    return 0;
}

通常,要编译它,我会简单地做:

gcc -c prog.c
gcc -o prog prog.o -lzip

正如预期的那样,这将创建一个需要 libzip 到 运行 的 ELF:

$ ldd prog
linux-vdso.so.1 (0x00007ffdafb53000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f81eedc7000)
/lib64/ld-linux-x86-64.so.2 (0x00007f81ef780000)
libzip.so.4 => /usr/lib/x86_64-linux-gnu/libzip.so.4 (0x00007f81ef166000)
libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f81eebad000)

libz 只是 libzip 的依赖项)

真正想做的是自己使用dlopen()加载库。很简单的任务,不是吗?嗯,是的,或者至少我是这么认为的。

要实现这一点,我只需要调用 dlopen 并让加载器完成它的工作:

#include <zip.h>
#include <dlfcn.h>

int main(void) {
    void *lib;
    int err;
    zip_t *myzip;

    lib = dlopen("libzip.so", RTLD_LAZY | RTLD_GLOBAL);
    if (lib == NULL)
        return 1;

    myzip = zip_open("myzip.zip", ZIP_CREATE | ZIP_TRUNCATE, &err);
    if (myzip == NULL)
        return 1;

    zip_close(myzip);

    return 0;
}

当然,既然要自己手动加载库,这次就不link了:

# Create prog.o
gcc -c prog.c

# Do a dry-run just to make sure all symbols are resolved
gcc -o /dev/null prog.o -ldl -lzip

# Now recompile only with libdl
gcc -o prog prog.o -ldl -Wl,--unresolved-symbols=ignore-in-object-files

标志 --unresolved-symbols=ignore-in-object-files 告诉 ld 不要担心我的 prog.o 在 link 时间有未解析的符号(我想在 运行时间).

问题

上面的应该可以工作™,确实它看起来确实......但是我有两台机器,作为一个迂腐的书呆子我只是想"well, better make sure and compile it on both of them".

第一台机器

x86-64,Linux 4.9,Debian 9,gcc 6.3.0,ld 2.28。这里 一切都按预期工作

我可以清楚地看到符号在那里:

$ readelf --dyn-syms prog

Symbol table '.dynsym' contains 15 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
     3: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
===> 4: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND zip_close
     5: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND dlopen@GLIBC_2.2.5 (3)
===> 6: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND zip_open
     7: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _Jv_RegisterClasses
     8: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
     9: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@GLIBC_2.2.5 (2)
    10: 0000000000201040     0 NOTYPE  GLOBAL DEFAULT   25 _edata
    11: 0000000000201048     0 NOTYPE  GLOBAL DEFAULT   26 _end
    12: 0000000000201040     0 NOTYPE  GLOBAL DEFAULT   26 __bss_start
    13: 00000000000006a0     0 FUNC    GLOBAL DEFAULT   11 _init
    14: 0000000000000924     0 FUNC    GLOBAL DEFAULT   15 _fini

PLT 条目也如预期的那样存在并且看起来不错:

$ objdump -j .plt -M intel -d prog

Disassembly of section .plt:

00000000000006c0 <.plt>:
 6c0:   ff 35 42 09 20 00       push   QWORD PTR [rip+0x200942]        # 201008 <_GLOBAL_OFFSET_TABLE_+0x8>
 6c6:   ff 25 44 09 20 00       jmp    QWORD PTR [rip+0x200944]        # 201010 <_GLOBAL_OFFSET_TABLE_+0x10>
 6cc:   0f 1f 40 00             nop    DWORD PTR [rax+0x0]

00000000000006d0 <zip_close@plt>:
 6d0:   ff 25 42 09 20 00       jmp    QWORD PTR [rip+0x200942]        # 201018 <zip_close>
 6d6:   68 00 00 00 00          push   0x0
 6db:   e9 e0 ff ff ff          jmp    6c0 <.plt>

00000000000006e0 <dlopen@plt>:
 6e0:   ff 25 3a 09 20 00       jmp    QWORD PTR [rip+0x20093a]        # 201020 <dlopen@GLIBC_2.2.5>
 6e6:   68 01 00 00 00          push   0x1
 6eb:   e9 d0 ff ff ff          jmp    6c0 <.plt>

00000000000006f0 <zip_open@plt>:
 6f0:   ff 25 32 09 20 00       jmp    QWORD PTR [rip+0x200932]        # 201028 <zip_open>
 6f6:   68 02 00 00 00          push   0x2
 6fb:   e9 c0 ff ff ff          jmp    6c0 <.plt>

程序运行没有任何问题:

$ ./prog
$ echo $?
0

即使使用调试器查看它的内部,我也可以清楚地看到符号像任何普通动态符号一样得到正确解析:

0x55555555479b <main+43>                       lea    rax, [rbp - 0x14]
0x55555555479f <main+47>                       mov    rdx, rax
0x5555555547a2 <main+50>                       mov    esi, 9
0x5555555547a7 <main+55>                       lea    rdi, [rip + 0xc0] <0x7ffff7ffd948>
0x5555555547ae <main+62>                       call   zip_open@plt <0x555555554620>
 |
 v ### PLT entry:
0x555555554620 <zip_open@plt>                  jmp    qword ptr [rip + 0x200a02] <0x555555755028>
 |
 v 
0x555555554626 <zip_open@plt+6>                push   2
0x55555555462b <zip_open@plt+11>               jmp    0x5555555545f0
 |
 v ### PLT stub:
0x5555555545f0                                 push   qword ptr [rip + 0x200a12] <0x555555755008>
0x5555555545f6                                 jmp    qword ptr [rip + 0x200a14] <0x7ffff7def0d0>
 |
 v ### Symbol gets correctly resolved
0x7ffff7def0d0 <_dl_runtime_resolve_fxsave>    push   rbx
0x7ffff7def0d1 <_dl_runtime_resolve_fxsave+1>  mov    rbx, rsp
0x7ffff7def0d4 <_dl_runtime_resolve_fxsave+4>  and    rsp, 0xfffffffffffffff0
0x7ffff7def0d8 <_dl_runtime_resolve_fxsave+8>  sub    rsp, 0x240

第二台机器

x86-64,Linux 4.15,Ubuntu 18.04,gcc 7.4,ld 2.30。这里,发生了一些非常奇怪的事情

编译没有产生任何警告或错误,但是我没有看到符号:

$ readelf --dyn-syms prog

Symbol table '.dynsym' contains 7 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
     3: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     4: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND dlopen@GLIBC_2.2.5 (3)
     5: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
     6: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@GLIBC_2.2.5 (2)

那里有 PLT 条目 ,但它们被零填充,甚至无法被 objdump 识别:

$ objdump -j .plt -M intel -d prog

Disassembly of section .plt:

0000000000000560 <.plt>:
 560:   ff 35 4a 0a 20 00       push   QWORD PTR [rip+0x200a4a]        # 200fb0 <_GLOBAL_OFFSET_TABLE_+0x8>
 566:   ff 25 4c 0a 20 00       jmp    QWORD PTR [rip+0x200a4c]        # 200fb8 <_GLOBAL_OFFSET_TABLE_+0x10>
 56c:   0f 1f 40 00             nop    DWORD PTR [rax+0x0]
    ...

#   ^^^
# Here, these three dots are actually hiding another 0x10+ bytes filled of 0x0
# zip_close@plt should be here instead...

0000000000000580 <dlopen@plt>:
 580:   ff 25 42 0a 20 00       jmp    QWORD PTR [rip+0x200a42]        # 200fc8 <dlopen@GLIBC_2.2.5>
 586:   68 00 00 00 00          push   0x0
 58b:   e9 d0 ff ff ff          jmp    560 <.plt>
    ...

#   ^^^
# Here, these three dots are actually hiding another 0x10+ bytes filled of 0x0
# zip_open@plt should be here instead...

当程序 运行 时,dlopen() 工作正常并将 libzip 加载到内存中,但是当 zip_open() 被调用时,它只会生成一个分段错误:

$ ./prog
Segmentation fault (code dumped)

使用调试器查看,问题更加明显(以防还不够明显)。填充为零的 PLT 条目最终解码为一堆 add 取消引用 rax 的指令,其中包含无效地址并使程序出现段错误并死亡:

0x5555555546e5 <main+43>               lea    rax, [rbp - 0x14]
0x5555555546e9 <main+47>               mov    rdx, rax
0x5555555546ec <main+50>               mov    esi, 9
0x5555555546f1 <main+55>               lea    rdi, [rip + 0xc6]
0x5555555546f8 <main+62>               call   dlopen@plt+16 <0x555555554590>
 |
 v ### Broken PLT enrty (all 0x0, will cause a segfault):
0x555555554590 <dlopen@plt+16>         add    byte ptr [rax], al
0x555555554592 <dlopen@plt+18>         add    byte ptr [rax], al
0x555555554594 <dlopen@plt+20>         add    byte ptr [rax], al
0x555555554596 <dlopen@plt+22>         add    byte ptr [rax], al
0x555555554598 <dlopen@plt+24>         add    byte ptr [rax], al
0x55555555459a <dlopen@plt+26>         add    byte ptr [rax], al
0x55555555459c <dlopen@plt+28>         add    byte ptr [rax], al
0x55555555459e <dlopen@plt+30>         add    byte ptr [rax], al
   ### Next PLT entry...
0x5555555545a0 <__cxa_finalize@plt>    jmp    qword ptr [rip + 0x200a52] <0x7ffff7823520>
 |
 v
0x7ffff7823520 <__cxa_finalize>        push   r15
0x7ffff7823522 <__cxa_finalize+2>      push   r14

问题

  1. 所以,首先...为什么会这样?
  2. 我认为这应该有效,不是吗?如果不是,为什么?为什么只在两台机器中的一台上?
  3. 但最重要的是:我该如何解决这个问题

对于问题 3,我想强调的是,我想自己加载库,而不是 linking 它,所以请不要仅仅评论这是不好的做法,或者其他什么。

The above Should Just Work™, and indeed it does seem to...

不,它不应该,如果它看起来像,那更像是一个意外。一般来说,使用 --unresolved-symbols=... 是一个非常糟糕的主意™,而且几乎永远不会如你所愿。

解决方案很简单:您只需要查找 zip_openzip_close,如下所示:

int main(void) {
    void *lib;
    zip_t *p_open(const char *, int, int *);
    void *p_close(zip_t*);
    int err;
    zip_t *myzip;

    lib = dlopen("libzip.so", RTLD_LAZY | RTLD_GLOBAL);
    if (lib == NULL)
        return 1;

    p_open = (zip_t(*)(const char *, int, int *))dlsym(lib, "zip_open");
    if (p_open == NULL)
        return 1;
    p_close = (void(*)(zip_t*))dlsym(lib, "zip_close");
    if (p_close == NULL)
        return 1;

    myzip = p_open("myzip.zip", ZIP_CREATE | ZIP_TRUNCATE, &err);
    if (myzip == NULL)
        return 1;

    p_close(myzip);

    return 0;
}

要添加到 EmployedRussian 的答案中,您可以借助 Implib.so 工具实现您需要的内容。它将为所有库符号(例如 zip_open)生成存根,这些存根将在内部调用 dlopen/dlsym 并将调用从您的程序转发到共享库:

$ gcc -c prog.c
$ implib-gen.py path/to/libzip.so
$ gcc -o prog prog.o libzip.tramp.S libzip.init.c -ldl

(请注意,您不再需要花哨的链接器标志和链接器空运行)。

附带说明一下,您尝试做的事情称为延迟加载,是 Windows DLLS 的 standard feature