Что такое @PLT

Table of Contents

И как libc линкуется в рантайме?

Каждый, кто своим любопытным отладчиком смотрел в собственноручно собранный helloworld, замечал там, неожиданную строчку:

0x000000000040c04b6 <main+30>:   callq  0x400398 <printf@plt>

и удивлялся тому, что его привычный и знакомый printf обрел какой-то странный и непонятный суффикс @plt. Посмотрим, почему это произошло.

ASLR

Сравнительно недавно динамические разделяемые библиотеки загружались в адресное пространство целевого процесса по стандартным, одинаковым для всех процессов, адресам, зависящим, разве что от версии операционной системы. Это весьма облегчало жизнь разного рода вредоносным программам, потому что они могли с некоторой долей уверенности предполагать расположение нужных им функций операционной системы, например для открытия и записи в файлы, чтобы заразить другие программы.

Поэтому через некоторое время была придумана и внедрена рандомизация размещения адресного пространства (address space layout randomization - ASLR). И теперь программам заранее неизвестны адреса вызовов библиотечных функций, когда библиотеки линкуются динамически.

Загрузка динамических библиотек

Итак, если код разделяемой библиотеки загружен неведомо куда, то как же программе его вызывать? Для этого придумали GOT и PLT.

GOT

Каждый исполняемый ELF-файл имеет раздел, называемый глобальной таблицей смещений (Global Offset Table, GOT). Эта таблица получает значения абсолютных адресов функций во время выполнения.

Мы можем взглянуть на нее:

rigidus@machine:~$ objdump -R ./hello_world

./hello_world:     file format elf32-i386

DYNAMIC RELOCATION RECORDS
OFFSET   TYPE              VALUE
08049564 R_386_GLOB_DAT    __gmon_start__
08049574 R_386_JUMP_SLOT   __gmon_start__
08049578 R_386_JUMP_SLOT   __libc_start_main
0804957c R_386_JUMP_SLOT   printf

PLT

Также каждый исполняемый файл имеет секцию, называемую таблицей связей процедур (Procedure Linkage Table, PLT). Когда вы читаете дизассемблированный код, вы видите, что вызовы функций, такие как printf указывают именно туда.

Если взглянуть внимательнее в эту секцию, то можно увидеть короткие однотипны цепочки инструкций:

rigidus@machine:~$ objdump -d -j .plt ./hello_world

./hello_world:     file format elf32-i386

Disassembly of section .plt:

08048270 <__gmon_start__@plt-0x10>:
8048270:       ff 35 6c 95 04 08       pushl  0x804956c
8048276:       ff 25 70 95 04 08       jmp    *0x8049570
804827c:       00 00                   add    %al,(%eax)

08048280 <__gmon_start__@plt>:
8048280:       ff 25 74 95 04 08       jmp    *0x8049574
8048286:       68 00 00 00 00          push   $0x0
804828b:       e9 e0 ff ff ff          jmp    8048270 <_init+0x18>

08048290 <__libc_start_main@plt>:
8048290:       ff 25 78 95 04 08       jmp    *0x8049578
8048296:       68 08 00 00 00          push   $0x8
804829b:       e9 d0 ff ff ff          jmp    8048270 <_init+0x18>

080482a0 <printf@plt>:
80482a0:       ff 25 7c 95 04 08       jmp    *0x804957c
80482a6:       68 10 00 00 00          push   $0x10
80482ab:       e9 c0 ff ff ff          jmp    8048270 <_init+0x18>

Как все эо работает?

Перейдем к первому вызову printf@plt, который на самом деле не printf а jmp на соответствующее место в PLT:

080482a0 <printf@plt>:
80482a0:       ff 25 7c 95 04 08       jmp    *0x804957c
80482a6:       68 10 00 00 00          push   $0x10
80482ab:       e9 c0 ff ff ff          jmp    8048270 <_init+0x18>

Обратите внимание, что это коссвенный переход по указателю 0x804957c, который находится в GOT. GOT в конечном итоге будет иметь абсолютный адрес для printf, однако при первом вызове произойдет возврат к инструкции после перехода в PLT - 0x80482a6. Мы можем увидеть это ниже:

(gdb) x/8x 0x804957c-20
0x8049568 <_GLOBAL_OFFSET_TABLE_>:      0x0804949c      0xb80016e0      0xb7ff92f0      0x08048286
0x8049578 <_GLOBAL_OFFSET_TABLE_+16>:   0xb7eafde0      0x080482a6      0x00000000      0x00000000

В коде внутри PLT смещение кладется в стек и выполняется другой jmp:

080482a0 <printf@plt>:
80482a0:       ff 25 7c 95 04 08       jmp    *0x804957c
80482a6:       68 10 00 00 00          push   $0x10
80482ab:       e9 c0 ff ff ff          jmp    8048270 <_init+0x18>

Этот переход - это переход к возможному компоновщику времени выполнения, который будет загружать разделяемую библиотеку, которая содержит printf. Смещение $0x10, которое было положено в стек, сообщает этому компоновщику код смещения символа в GOT (см. вывод objdump -R ./hello_world выше), printf в этом случае. Затем компоновщик будет записывать адрес printf в GOT по адресу 0x804957c. Мы можем увидеть это, если посмотрим на GOT после загрузки библиотеки:

(gdb) x/8x 0x804957c-20
0x8049568 <_GLOBAL_OFFSET_TABLE_>:      0x0804949c      0xb80016e0      0xb7ff92f0      0x08048286
0x8049578 <_GLOBAL_OFFSET_TABLE_+16>:   0xb7eafde0      0xb7edf620      0x00000000      0x00000000

Обратите внимание, что предыдущий адрес, 0x80482a6, был заменен компоновщиком на 0xb7edf620. Чтобы подтвердить, что это действительно адрес для printf, мы можем начать дизассемблировать по этому адресу:

(gdb) disassemble 0xb7edf620
Dump of assembler code for function printf:
...

Поскольку библиотека теперь загружена и GOT был перезаписан абсолютным адресом для printf, последующие вызовы функции printf@plt перейдут непосредственно к адресу printf.

Все это также имеет дополнительное преимущество в том, что общая библиотека не загружается до загрузки функции в ее библиотеку - другими словами, это "ленивая загрузка".

Яндекс.Метрика
Home