О необходимости структур GOT & PLT в заголовке ELF - PullRequest
1 голос
/ 15 мая 2019

В некоторых архитектурах (например, x86_64), где можно ссылаться на данные (например, mov) и код (jmp, call), используя режим относительной адресации ПК (RIP), действительно ли существует техническая причина, оправдывающая необходимость таких структур (получил, плт)?

Я имею в виду, если я хочу переместить глобальные данные (например) в регистр, я мог бы сделать следующую инструкцию (стандартный PIE):

mov rax, QWORD PTR [rip + 0x2009db]

mov eax, DWORD PTR [rax]

(где 0x2009db - смещение между rip и правой записью в блоке, содержащем адрес символа)

И почему мы не можем сделать что-то подобное:

mov rax, rip + 0xYYYYYY

mov eax, DWORD PTR [rax]

(0xYYYYYYY - прямая дельта между значением RIP и символом (например, глобальной переменной))

Я не привык делать ASM, поэтому мой пример, возможно, неверен. Но моя идея такова: почему бы просто не вычислить абсолютный адрес символа на основе RIP, поместить его в EAX и затем получить доступ к его содержимому. Если набор инструкций позволяет делать что угодно с относительной адресацией, зачем использовать такие структуры (got, plt)?

Тот же вопрос будет применяться для инструкций call / jmp.

Это потому, что набор инструкций этого не позволяет? Это потому, что значение смещения не может охватить все адресное пространство? Но .. это важно? Поскольку структура раздела поддерживается, он отображается в виртуальном адресном пространстве процесса (например, раздел .dat, за которым следует .got или что-то подобное). Я имею в виду, почему смещение было бы больше при обращении непосредственно к адресу символа вместо адреса входа в полученном? Другая причина?

Спасибо!

1 Ответ

0 голосов
/ 17 мая 2019

По сути, причина этих структур заключается в дополнительном уровне косвенности.

Таким образом, вы можете вставлять символы в динамические библиотеки с помощью LD_PRELOAD. И даже без этого правила динамического связывания таковы, что символ, определенный в исполняемом файле, переопределяет символ, определенный в общей библиотеке, даже для вызовов из этой библиотеки (см. this ).

Также рассмотрите эти моменты.

  1. Адрес, по которому загружается разделяемая библиотека, содержащая реализацию вызываемой функции, заранее неизвестен (это сделано специально, в частности это сделано специально: это функция ld.so, известная как ASLR). ), поэтому динамический загрузчик должен применить перемещения как минимум ко всем сайтам вызовов, которые выполняются во время выполнения.
  2. Если бы не PLT, это убило бы преимущество совместного использования сегментов кода библиотек, отображаемых в память разными образами процессов, поскольку в разных процессах одна и та же библиотека может иметь разные адреса, что приводит к разным исправлениям код. PLT - это относительно небольшой фрагмент данных, который не передается. Смотрите этот пост .
  3. PLT позволяет лениво связывать функции при первом вызове. Слоты PLT изначально содержат адрес резольвера. После завершения разрешения результат кэшируется в стоте PLT.

Механизм перемещения для GOT / PLT покрыт здесь . В общем, в Интернете достаточно информации о том, как (и почему) работают PLT и GOT.

Кроме того, проверьте опцию GCC -fno-plt. Это оптимизация, но учтите, что GOT все еще необходим и что отложенное связывание не поддерживается для функций без записей PLT.

...