Зачем использовать глобальную таблицу смещений для символов, определенных в самой общей библиотеке? - PullRequest
4 голосов
/ 09 апреля 2019

Рассмотрим следующий простой исходный код общей библиотеки:

library.cpp:

static int global = 10;

int foo()
{
    return global;
}

Скомпилировано с опцией -fPIC в clang, это приводит к следующемусборка объекта (x86-64):

foo(): # @foo()
  push rbp
  mov rbp, rsp
  mov eax, dword ptr [rip + global]
  pop rbp
  ret
global:
  .long 10 # 0xa

Поскольку символ определен внутри библиотеки, компилятор использует относительную адресацию ПК, как и ожидалось: mov eax, dword ptr [rip + global]

Однако, если мы изменимstatic int global = 10; до int global = 10;, делая его символом с внешней связью, в результате получается сборка:

foo(): # @foo()
  push rbp
  mov rbp, rsp
  mov rax, qword ptr [rip + global@GOTPCREL]
  mov eax, dword ptr [rax]
  pop rbp
  ret
global:
  .long 10 # 0xa

Как вы можете видеть, компилятор добавил слой косвенного обращения с Global Offset Table, который кажется совершенно ненужнымв этом случае, поскольку символ все еще определен внутри той же библиотеки (и исходного файла).

Если символ был определен в другой общей библиотеке , GOT будет необходим, но в этомна случай, если это кажется излишним.Почему компилятор все еще добавляет этот символ в GOT?

Примечание: я считаю, этот вопрос подобен этому, однако ответ не был уместен, возможно, из-за недостатка деталей.

Ответы [ 2 ]

2 голосов
/ 09 апреля 2019

Глобальная таблица смещений служит двум целям.Один из них - позволить динамическому компоновщику «вставлять» другое определение переменной из исполняемого или другого общего объекта.Во-вторых, это позволяет генерировать независимый от позиции код для ссылок на переменные в определенных архитектурах процессора.

Динамическое связывание 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 будет содержать указатель на переменную, как в случае с глобальными переменными обычной видимости.

0 голосов
/ 09 апреля 2019

В дополнение к подробностям в ответе Росс-Риджа.

Это внешняя или внутренняя связь.Без static эта переменная имеет внешнюю связь и, следовательно, доступна из любой другой единицы перевода.Любая другая единица перевода может объявить ее как extern int global; и получить к ней доступ.

Связь :

Внешняя связь. Имя можетобращаться из областей в других единицах перевода.Переменные и функции с внешней связью также имеют языковую связь, которая позволяет связывать единицы перевода, написанные на разных языках программирования.

Любое из следующих имен, объявленных в области пространства имен, имеет внешнюю связь, если пространство имен не является безымянным или не являетсясодержится в безымянном пространстве имен (начиная с C ++ 11):

  • переменные и функции, не перечисленные выше (то есть функции, не объявленные как статические, неконстантные переменные в области имен, не объявленные как статические, и любыепеременные объявлены extern);
...