Как правило, нельзя ожидать, что два разных компилятора сгенерируют один и тот же код сборки для одного и того же ввода, даже если они имеют одинаковый номер версии; они могут иметь любое количество дополнительных «патчей» для генерации кода. Пока наблюдаемое поведение одинаково, все идет.
Вы также должны знать, что GCC в режиме по умолчанию -O0
генерирует намеренно плохой код. Он настроен для простоты отладки и скорости компиляции, а не для ясности или эффективности сгенерированного кода. Часто код, генерируемый gcc -O1
, легче понять, чем код, генерируемый gcc -O0
.
Вы также должны знать, что функция main
часто требует дополнительных настроек и демонтажа, которые не нужны другим функциям. Инструкция leal 4(%esp),%ecx
является частью этой дополнительной настройки. Если вы хотите понять только машинный код, соответствующий коду , который вы написали, а не подробности ABI , назовите вашу тестовую функцию иначе, чем main
.
(Как отмечено в комментариях, этот установочный код не так тщательно настроен, как мог бы, но обычно это не имеет значения, потому что он выполняется только один раз за время существования программы.)
Теперь, чтобы ответить на вопрос, который был буквально задан, причина появления
call __x86.get_pc_thunk.ax
потому что ваш компилятор по умолчанию генерирует «независимые от позиции» исполняемые файлы. Независимо от позиции означает, что операционная система может загрузить машинный код программы по любому адресу в (виртуальной) памяти, и она все еще будет работать. Это допускает такие вещи, как рандомизация макета адресного пространства , но чтобы это работало, вам нужно предпринять специальные шаги для установки «глобального указателя» в начале каждой функции, которая обращается к глобальным переменным или вызывает другую функцию ( с некоторыми исключениями). На самом деле проще объяснить сгенерированный код, если включить оптимизацию:
main:
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ebx
pushl %ecx
Это всего лишь настройка стека фрейма main
и сохранение регистров, которые необходимо сохранить. Вы можете игнорировать это.
call __x86.get_pc_thunk.bx
addl $_GLOBAL_OFFSET_TABLE_, %ebx
Специальная функция __x86.get_pc_thunk.bx
загружает свой обратный адрес, который является адресом следующей сразу инструкции addl
, в регистр EBX. Затем мы добавляем к этому адресу значение магической константы _GLOBAL_OFFSET_TABLE_
, которая в позиционно-независимом коде представляет собой разницу между адресом инструкции, которая использует _GLOBAL_OFFSET_TABLE_
, и адресом глобальная таблица смещений . Таким образом, EBX теперь указывает на глобальную таблицу смещений.
call add@PLT
Теперь мы вызываем add@PLT
, что означает вызов add
, но прыгаем через "таблицу связей процедур", чтобы сделать это. PLT заботится о том, что add
определяется в общей библиотеке, а не в основном исполняемом файле. Код в PLT использует глобальную таблицу смещений и предполагает, что вы уже установили EBX, чтобы указывать на него, перед вызовом символа @PLT. Вот почему main
должен настроить EBX, хотя, кажется, ничто его не использует. Если бы вы вместо этого написали что-то вроде
extern int number;
int main(void) { return number; }
тогда вы увидите прямое использование GOT, что-то вроде
call __x86.get_pc_thunk.bx
addl $_GLOBAL_OFFSET_TABLE_, %ebx
movl number@GOT(%ebx), %eax
movl (%eax), %eax
Мы загружаем EBX с адресом GOT, затем мы можем загрузить адрес глобальной переменной number
из GOT, и затем мы фактически разыменовываем адрес, чтобы получить значение number
.
Если вместо этого вы скомпилируете 64-битный код, вы увидите что-то другое и гораздо более простое:
movl number(%rip), %eax
Вместо того, чтобы возиться с GOT, мы можем просто загрузить number
с фиксированного смещения от счетчика программы. Добавлена относительная к ПК адресация, а также 64-разрядные расширения архитектуры x86. Точно так же ваша исходная программа в 64-битном независимом от позиции режиме просто скажет
call add@PLT
без предварительной настройки EBX. Вызов все еще должен проходить через PLT, но PLT использует саму относительную к ПК адресацию и не нуждается в помощи своего вызывающего абонента.
Единственная разница между __x86.get_pc_thunk.bx
и __x86.get_pc_thunk.ax
заключается в том, в каком регистре они хранят свой обратный адрес: EBX для .bx
, EAX для .ax
. Я также видел, как GCC генерирует .cx
и .dx
вариантов. Вопрос только в том, какой регистр он хочет использовать для глобального указателя - это должен быть EBX, если будут вызовы через PLT, но если их нет, он может использовать любой регистр, поэтому он пытается выберите тот, который не нужен ни для чего другого.
Почему она вызывает функцию для получения обратного адреса? Старые компиляторы сделали бы это вместо этого:
call 1f
1: pop %ebx
но это облажает предсказание обратного адреса , поэтому в настоящее время компилятору приходится сталкиваться с некоторыми дополнительными проблемами, чтобы убедиться, что каждый call
связан с ret
.