Возвращаемое значение get_pc_thunk не используется - PullRequest
1 голос
/ 09 июля 2020

У меня есть эта программа:

static int aux() {
    return 1;
}
int _start(){
    int a = aux();
    return a;
}

Когда я компилирую ее с использованием G CC с флагами -nostdlib -m32 -fpie и генерирую двоичный файл ELF, я получаю следующий код сборки:

00001000 <aux>:
    1000:   f3 0f 1e fb             endbr32 
    1004:   55                      push   %ebp
    1005:   89 e5                   mov    %esp,%ebp
    1007:   e8 2d 00 00 00          call   1039 <__x86.get_pc_thunk.ax>
    100c:   05 e8 2f 00 00          add    $0x2fe8,%eax
    1011:   b8 01 00 00 00          mov    $0x1,%eax
    1016:   5d                      pop    %ebp
    1017:   c3                      ret    

00001018 <_start>:
    1018:   f3 0f 1e fb             endbr32 
    101c:   55                      push   %ebp
    101d:   89 e5                   mov    %esp,%ebp
    101f:   83 ec 10                sub    $0x10,%esp
    1022:   e8 12 00 00 00          call   1039 <__x86.get_pc_thunk.ax>
    1027:   05 cd 2f 00 00          add    $0x2fcd,%eax
    102c:   e8 cf ff ff ff          call   1000 <aux>
    1031:   89 45 fc                mov    %eax,-0x4(%ebp)
    1034:   8b 45 fc                mov    -0x4(%ebp),%eax
    1037:   c9                      leave  
    1038:   c3                      ret    

00001039 <__x86.get_pc_thunk.ax>:
    1039:   8b 04 24                mov    (%esp),%eax
    103c:   c3                      ret

Я знаю, что функция get_pc_thunk используется для реализации позиционно-независимого кода в x86, но в этом случае я не могу понять, почему она используется. Мои вопросы:

  1. Функция возвращает адрес следующей инструкции в регистре eax, и в обоих случаях используется инструкция add, чтобы eax указывал на GOT. Обычно (по крайней мере, при доступе к глобальным переменным) этот регистр eax будет немедленно использоваться для доступа к глобальной переменной в таблице. Однако в этом случае eax полностью игнорируется. Что происходит?
  2. Я также не понимаю, почему get_pc_thunk вообще присутствует в коде, поскольку обе инструкции call используют относительные адреса. Поскольку адреса являются относительными, разве они не должны быть независимыми от позиции прямо из коробки?

Спасибо!

Ответы [ 2 ]

3 голосов
/ 09 июля 2020

Вы не включили оптимизацию, поэтому G CC генерирует прологи функций безотносительно того, есть ли полезные в рассматриваемой функции.

Чтобы увидеть результат get_pc_thunk, используется доступ к глобальной переменной .

Чтобы удалить бесполезные вызовы get_pc_thunk, включите оптимизацию, например, добавив -O2 в командную строку G CC.

1 голос
/ 10 июля 2020

Однако, если я перенесу функцию aux () в другой модуль компиляции, функция get_pc_thunk останется вызываемой даже с -O2, и, опять же, ее возвращаемое значение игнорируется.

IIR C, точка EBX = GOT предполагается / требуется самим PLT, и вызов должен быть go через PLT, потому что известно при компиляции этого модуля компиляции, что aux определение будет статически связано с ним. (https://godbolt.org/z/Yere9o показывает этот эффект для main только с прототипом для aux(), а не с определением, которое он может встроить.)

С атрибутом видимости "hidden" ELF мы можем получить это на go, потому что компилятор знает, что ему не нужно косвенно через PLT, потому что call rel32 будет известен во время связи c без необходимости перемещения времени выполнения: https://godbolt.org/z/73dGKq

__attribute__((visibility("hidden"))) int aux(void);
int _start(){
    int a = aux();
    return a;
}

gcc10.1 -O2 -m32 -fpie

_start:
        jmp     aux

IMO имеет смысл иметь вызов в объектных файлах, сгенерированных для модулей компиляции, которые вызывают внешние функции, но я не понимаю, почему компоновщик (или «поток») не удаляет их в финальном двоичный.

@ felipeek: Хороший вопрос. Компоновщик не знает, когда он может ослабить вызов foo@plt для вызова foo, потому что это также отключает взаимное расположение символов. Даже если в этом разделяемом объекте ELF есть определение foo, определение в одном из загруженных ранее может переопределить его / иметь приоритет. Я думаю, что эта «проблема» связана с тем, что исполняемые файлы P IE возникли в результате своего рода взлома: поместите точку входа в общий объект, и компоновщик Dynami c захочет запустить его. т.е. на уровне ELF исполняемые файлы P IE такие же, как .so, а -fpie и -fPIC выглядят одинаково для компоновщика.

Компоновщик может go наоборот, хотя : при создании обычного исполняемого файла, отличного от P IE (тип ELF = EXE C), он может превратить вызов foo в вызов foo@plt, но сам PLT не обязательно должен быть PIE / PI C, поэтому ему не требуется EBX = GOT.

Мы говорим, что все вызовы других модулей компиляции будут вызывать совершенно ненужный вызов в конечном двоичном файле, когда требуется P IE?

Нет, только в 32-битном коде P IE, где вы не можете сообщить компилятору, что это «внутренний» символ, используя «скрытую» видимость ELF. У вас даже может быть два имени для одного и того же символа, одно со скрытой видимостью, поэтому вы можете создать функцию, которую библиотеки могут разрешать по имени, но которую вы все равно можете вызывать из исполняемого файла, используя простой call rel32 вместо неуклюжих непрямых вызовов через PLT.

Это один из недостатков P IE. Даже в 64-битном коде без атрибута вы получите jmp aux@PLT. (Или с -fno-plt, непрямым вызовом с использованием относительной адресации RIP для записи GOT.)

32-битный P IE действительно отстой для производительности, например, в среднем 15% (измерено некоторое время a go на процессорах в то время, возможно, несколько отличался.) Намного меньший эффект на x86-64, где доступна относительная адресация RIP, например, пара%. 32-битные абсолютные адреса больше не разрешены в x86-64 Linux? содержит ссылки на более подробную информацию.

...