Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

На главную -> MyLDP -> Программирование и алгоритмические языки


Ulrich Drepper "Как писать разделяемые библиотеки"
Назад Оглавление Вперед

1.5.5. Таблицы GOT и PLT

Глобальная таблица смещений (Global Offset Table - GOT) и таблица компоновки процедур (Procedure Linkage Table — PLT) являются двумя центральными структурами данных времени выполнения ELF. Давайте теперь рассмотрим, почему они используются и какие из этого вытекают последствия.

Перемещения создаются для таких конструкций исходного кода, как, например

   extern int foo;
   extern int bar (int);
   int call_bar (void) {
   return bar (foo);
   }

Вызов функции bar требует двух перемещений: одно для загрузки значения foo и еще одно для того, чтобы найти адрес функции bar. Если для кода, который должен быть сгенерирован, известны адреса переменной и функции, то ассемблерные инструкции будут напрямую выполнять загрузку или выполнять переход по адресу. Для архитектуры IA-32 код будет выглядеть следующим образом:

   pushl   foo
   call    bar

Здесь адреса foo и bar должны быть закодированы в текстовом сегменте как часть инструкции. Если известен только адрес динамического компоновщика, то текстовый сегмент должен быть изменен во время выполнения. Согласно тому, что мы узнали выше, этого следует избегать.

Поэтому код, сгенерированный для объектов DSO, т.е. использующий -fpic или -fPIC, выглядит следующим образом:

   movl	foo@GOT(%ebx), %eax
   pushl	(%eax)
   call 	bar@PLT

Адрес переменной foo теперь не является частью инструкции. Вместо этого он загружается из таблицы GOT. Адрес места в таблице GOT, вычисляемый относительно значения регистра PIC (%ebx) известен, как адрес времени компоновки. Поэтому текстовый сегмент изменять не потребуется, изменяется только таблица GOT {Примечание 7}.

Примечание 7: Есть еще одно преимущество использования такой схемы. Если инструкция должна быть изменена, нам потребуется по одному перемещению для каждой инструкции load/store. Если адрес сохраняется в таблице GOT, то потребуется только одно перемещение.

Ситуация для вызова функции аналогичная. Функция bar не вызывается напрямую. Вместо этого управление передается заглушке фунции bar, которая находится в таблице PLT (указано с помощью bar@PLT). В архитектуре IA-32 сама таблица PLT модифицироваться не должна, и ее можно поместить в сегмент, в котором разрешено только чтение; размер каждой записи в ней равен 16 байтов. Модифицироваться может только таблица GOT, причем размер записи в ней равен 4 байта. Структура PLT для объекта DSO архитектуры IA-32 выглядит следующим образом:

    .PLT0:pushl 4(%ebx)
		jmp *8(%ebx)
		nop; nop
                nop; nop
    .PLT1:jmp *name1@GOT(%ebx)
                pushl $offset1
                jmp .PLT0@PC
    .PLT2:jmp *name2@GOT(%ebx)
                pushl $offset2
                jmp .PLT0@PC

Здесь показаны три записи, именно столько, сколько необходимо, причем все они имеют одинаковый размер. Первая запись, помеченная как .PLT0, специальная. Как мы увидим, она используется внутри таблицы. Все последующие записи принадлежат к точно одному символу функции. Первая инструкция является косвенным переходом, адрес для которого берется из хэш-блока в таблице GOT. Каждая запись таблицы PLT имеет один хэш-блок в таблице GOT. Во время запуска динамический компоновщик заполняет хэш-блок GOT адресом, указывающим на вторую инструкцию соответствующей записи в PLT. То есть, когда запись PLT используется в первый раз, переход завершается следующей инструкцией pushl. Значение, помещаемое в стек, также представляет собой конкретное значение хэш-блока PLT, является записью смещения, используемого при перемещении, и это смещение будет использоваться функцией, которая должна быть вызвана. Затем управление передается на специальную первую запись PLT, которая помещает в стек еще несколько значений и, наконец, выполняется переход в динамический компоновщик. Динамический компоновщик должен убедиться, что в третьем хэш-блоке GOT (смещение 8) находится адрес точки входа в динамический компоновщик. Как только динамический компоновщик определит адрес функции, он прежде, чем переходить к найденной функции, сохраняет результат в записи GOT, которая была использована в инструкции jmp в начале записи PLT. В результате во всех следующих обращениях к записи PLT переход к динамическому компоновщику происходить не будет, а вместо этого будет выполняться переход напрямую в функцию. Следовательно затраты на все вызовы кроме первого будут равны "только" одному косвенному переходу.

Если не гарантируется, что функция определена в объекте, в котором на нее ссылаются, то всегда используется заглушка PLT. Обратите внимание, что простого определения в объекте, в котором делается ссылка, недостаточно для того, чтобы избежать обращения к записи PLT. Если рассмотреть процесс поиска символов, то будет понятно, что если поиск определения должен происходить в другом объекте (интерпозиция), то тогда запись PLT необходима. Позже мы точно объясним, когда и как избегать использование записи PLT.

Какую именно структуру имеют таблицы GOT и PLT, зависит от архитектуры, что указано в соответствующих спецификациях psABI. То, что было сказано здесь об архитектуре IA-32, в некоторой форме применимо к некоторым другим, но не всем вариантам архитектуры. Например, для архитектуры IA-32 таблица PLT доступна только для чтения, для других вариантов архитектуры она должна быть доступна и для записи, поскольку вместо того, чтобы воспользоваться косвенным переходом с использованием значения из таблицы GOT, изменение записей в таблице PLT должно осуществляться напрямую. Читатель может подумать, что разработчики спецификации IA-32 ABI совершили ошибку, потребовав вместо прямого вызова использовать косвенный и, следовательно, более медленный вызов. Все же, это не ошибка. Наличие исполняемого сегмента, в котором можно делать запись, является огромной проблемой безопасности, поскольку злоумышленники могут просто записать в таблицу PLT произвольный код и перехватить управление программой. Так или иначе, мы можем поытожить затраты на использование таблиц GOT и PLT следующим образом:

  • для каждого использования экспортируемой глобальной переменной потребуется использовать запись GOT и косвенно выполнять загрузку значений переменных;
  • для каждой вызываемой функции (в отличие от ссылки на переменную), для которой не гарантируется наличие определения в вызывающем объекте, требуется запись PLT. Вызов функции выполняется косвенно с помощью передачи управления первый раз в код в таблице PLT, который, в свою очередь, вызывает функцию.
  • В некоторых вариантах архитектуры для каждой записи PLT потребуется, по крайней мере, одна запись GOT.

Если не пользоваться переходом через таблицу PLT, то для архитектуры IA-32 можно сэкономить 16 байтов текста и 4 байта данных. Если не пользоваться таблицей GOT при доступе к глобальной переменной, то можно сэкономить 4 байта данных и одну инструкцию загрузки (то есть, по крайней мере 3 байта кода и циклы времени выполнения ). Кроме того, для каждой записи GOT требуется перемещение, связанное с затратами, о которых рассказывалось выше.


Предыдущий раздел:   Следующий раздел:
Назад Оглавление Вперед