Ответ 1
Это способ получить исправления кода (корректируя адреса в зависимости от места расположения кода в виртуальной памяти, которые могут различаться в разных процессах) без необходимости поддерживать отдельную копию кода для каждого процесса. PLT - это таблица связывания процедур, одна из структур, которая упрощает использование динамической загрузки и связывания.
[email protected]
- это на самом деле небольшая заглушка, которая (в конце концов) вызывает настоящую функцию printf
, изменяя способ ускорения последующих вызовов.
Реальная функция printf
может быть отображена в любое место в данном процессе (виртуальное адресное пространство), как и код, который пытается ее вызвать.
Таким образом, чтобы разрешить правильное совместное использование кода вызывающего кода (слева внизу) и вызываемого кода (справа внизу), вы не хотите применять какие-либо исправления непосредственно к вызывающему коду, поскольку это ограничит его расположение в другие процессы.
Таким образом, PLT
- это меньшая область, специфичная для процесса, с надежно рассчитанным адресом во время выполнения, который не распределяется между процессами, поэтому любой конкретный процесс может изменить его по своему усмотрению, без негативных последствий.
Изучите следующую диаграмму, которая показывает и ваш код, и код библиотеки, сопоставленный с разными виртуальными адресами в двух разных процессах, ProcA
и ProcB
:
Address: 0x1234 0x9000 0x8888
+-------------+ +---------+ +---------+
| | | Private | | |
ProcA | | | PLT/GOT | | |
| Shared | +---------+ | Shared |
========| application |=============| library |==
| code | +---------+ | code |
| | | Private | | |
ProcB | | | PLT/GOT | | |
+-------------+ +---------+ +---------+
Address: 0x2020 0x9000 0x6666
Этот конкретный пример показывает простой случай, когда PLT сопоставляется с фиксированным местоположением. В вашем сценарии он расположен относительно текущего счетчика программы, о чем свидетельствует ваш поиск относительно счетчика программ:
<[email protected]+0>: jmpq *0x2004c2(%rip) ; 0x600860 <_GOT_+24>
Я просто использовал фиксированную адресацию, чтобы упростить пример.
Первоначальный способ совместного использования кода означал, что они должны быть загружены в одну и ту же область памяти в каждом виртуальном адресном пространстве каждого процесса, который его использовал. Либо это, либо он не может быть разделен, так как сам процесс исправления одной общей копии для одного процесса полностью заполняет другие процессы, в которых он был сопоставлен с другим местоположением.
Используя независимый от позиции код вместе с PLT и глобальной таблицей смещений (GOT), первый вызов функции [email protected]
(в PLT) является многоступенчатой операцией, в которой выполняются следующие действия:
- Вы вызываете
[email protected]
в PLT. - Он вызывает версию GOT (через указатель), которая первоначально указывает на некоторый код настройки в PLT.
- Этот установочный код загружает соответствующую разделяемую библиотеку, если это еще не сделано, затем модифицирует указатель GOT так, чтобы последующие обращения непосредственно к реальному
printf
а не к установочному коду PLT. - Затем он вызывает загруженный код
printf
по правильному адресу для этого процесса.
При последующих вызовах, поскольку указатель GOT был изменен, многоэтапный подход упрощен:
- Вы вызываете
[email protected]
в PLT. - Он вызывает версию GOT (через указатель), которая теперь указывает на реальный
printf
.
Хорошую статью можно найти здесь, подробно описывая, как glibc
загружается во время выполнения.