Загрузка динамической библиотеки во время выполнения дает противоречивые и неожиданные результаты, пропущенные символы и пустые записи PLT. Почему? - PullRequest
1 голос
/ 06 ноября 2019

Я уже давно борюсь с этой проблемой и не могу найти решения или даже объяснения для нее. Извините, если вопрос длинный, но потерпите меня, поскольку я просто хочу прояснить его на 100% в надежде, что кто-то более опытный, чем я, сможет это выяснить.

I 'm выделение синтаксиса 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

Это создает, как и ожидалось, ELF, для выполнения которого требуется libzip:

$ 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;
}

Конечно, так как яхочу вручную загрузить библиотеку сам, я не буду связывать ее на этот раз:

# 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 не решенсимволы во время соединения (я хочу сам позаботиться об этом во время выполнения).

Проблема

Выше Just Just Work ™ , и действительно, похоже,... но у меня есть две машины, и, будучи педантичным ботаником, я просто подумал: «Ну, лучше убедитесь и скомпилируйте их на обеих».

Первая машина

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, который содержит неверный адрес и делает программу segfault и die:

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

Questions

  1. Итак, прежде всего ... почему это происходит?
  2. Я думал, что это должно сработать, не так ли? Если нет, то почему? И почему только на одной из двух машин?
  3. Но самое главное: как я могу это исправить ?

Для вопроса 3 я хочу подчеркнуть, чтоВесь смысл в том, что я хочу загрузить библиотеку самостоятельно, не связывая ее, поэтому, пожалуйста, не комментируйте, что это плохая практика, или что-то еще.

1 Ответ

3 голосов
/ 06 ноября 2019

Вышесказанное должно просто работать ™, и действительно, похоже, оно ...

Нет, не должно, и если это кажется, это скорее несчастный случай. В общем, использование --unresolved-symbols=... - это действительно плохая идея ™, и она почти никогда не будет делать то, что вы хотите.

Решение тривиально: вам просто нужно посмотреть zip_open и zip_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;
}
...