Глобальная таблица смещений служит двум целям.Один из них - позволить динамическому компоновщику «вставлять» другое определение переменной из исполняемого или другого общего объекта.Во-вторых, это позволяет генерировать независимый от позиции код для ссылок на переменные в определенных архитектурах процессора.
Динамическое связывание ELF рассматривает весь процесс, исполняемый файл и все общие объекты (динамические библиотеки) как разделяемый.единое глобальное пространство имен.Если несколько компонентов (исполняемые или совместно используемые объекты) определяют один и тот же глобальный символ, то динамический компоновщик обычно выбирает одно определение этого символа, и все ссылки на этот символ во всех компонентах ссылаются на это одно определение.(Однако динамическое разрешение символов в формате ELF является сложным, и по разным причинам разные компоненты могут в конечном итоге использовать разные определения одного и того же глобального символа.)
Для реализации этого при создании разделяемой библиотеки компилятор будет обращаться к глобальномупеременные косвенно через GOT.Для каждой переменной будет создана запись в GOT, содержащая указатель на переменную.Как показывает ваш пример кода, компилятор будет использовать эту запись для получения адреса переменной, а не пытаться получить к ней доступ напрямую.Когда общий объект загружается в процесс, динамический компоновщик определяет, была ли какая-либо из глобальных переменных заменена определениями переменных в другом компоненте.Если так, то у этих глобальных переменных будут обновлены записи GOT, чтобы они указывали на заменяющую переменную.
Используя "скрытые" или "защищенные" атрибуты видимости ELF, можно предотвратить замену глобального определенного символа определением.в другом компоненте, что исключает необходимость использования GOT на определенных архитектурах.Например:
extern int global_visible;
extern int global_hidden __attribute__((visibility("hidden")));
static volatile int local; // volatile, so it's not optimized away
int
foo() {
return global_visible + global_hidden + local;
}
при компиляции с -O3 -fPIC
с портом x86_64 GCC генерирует:
foo():
mov rcx, QWORD PTR global_visible@GOTPCREL[rip]
mov edx, DWORD PTR local[rip]
mov eax, DWORD PTR global_hidden[rip]
add eax, DWORD PTR [rcx]
add eax, edx
ret
Как видите, только global_visible
использует GOT, global_hidden
и local
не используйте его.«Защищенная» видимость работает аналогично, она предотвращает замену определения, но делает его по-прежнему видимым для динамического компоновщика, чтобы к нему могли обращаться другие компоненты.«Скрытая» видимость полностью скрывает символ от динамического компоновщика.
Необходимость перераспределения кода для того, чтобы совместно используемые объекты могли загружаться по разным адресам в разных процессах, означает, что статически распределенные переменные, независимо от того, имеют ли они глобальныеили локальная область, не может быть доступна напрямую с помощью одной инструкции на большинстве архитектур.Единственное исключение, о котором я знаю, - это 64-битная архитектура x86, как вы видите выше.Он поддерживает операнды памяти, которые являются относительными к ПК и имеют большие 32-битные смещения, которые могут достигать любой переменной, определенной в том же компоненте.
На всех других архитектурах, с которыми я знаком, доступ к переменным в зависимости от положения требует нескольких инструкций.Как именно сильно зависит от архитектуры, но это часто связано с использованием GOT.Например, если вы скомпилировали приведенный выше пример C-кода с портом x86_64 GCC, используя опции -m32 -O3 -fPIC
, вы получите:
foo():
call __x86.get_pc_thunk.dx
add edx, OFFSET FLAT:_GLOBAL_OFFSET_TABLE_
push ebx
mov ebx, DWORD PTR global_visible@GOT[edx]
mov ecx, DWORD PTR local@GOTOFF[edx]
mov eax, DWORD PTR global_hidden@GOTOFF[edx]
add eax, DWORD PTR [ebx]
pop ebx
add eax, ecx
ret
__x86.get_pc_thunk.dx:
mov edx, DWORD PTR [esp]
ret
GOT используется для всех трех переменных доступа, но если вы присмотритесь global_hidden
и local
обрабатываются иначе, чем global_visible
.С последним указатель на переменную доступен через GOT, а с двумя первыми переменными они обращаются напрямую через GOT.Это довольно распространенный трюк среди архитектур, где GOT используется для всех ссылок на независимые от позиции переменные.
32-битная архитектура x86 здесь исключительна, так как имеет большие 32-битные смещения и 32-битное адресное пространство. Это означает, что к любой части памяти можно получить доступ через базу GOT, а не только через GOT. Большинство других архитектур поддерживают только гораздо меньшие смещения, что делает максимальное расстояние, которое может быть чем-то от базы GOT, намного меньше. Другие архитектуры, использующие этот трюк, будут помещать только маленькие (локальные / скрытые / защищенные) переменные в сам GOT, большие переменные хранятся вне GOT, а GOT будет содержать указатель на переменную, как в случае с глобальными переменными обычной видимости.