Что происходит в сборке Apple LLVM-gcc x86?
Мне интересно узнать больше о сборке x86/x86_64. Увы, я на Mac. Нет проблем, верно?
$ gcc --version
i686-apple-darwin11-llvm-gcc-4.2 (GCC) 4.2.1 (Based on Apple Inc. build
5658) (LLVM build 2336.11.00)
Copyright (C) 2007 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Я написал простой "Hello World" на C, чтобы понять, какой код мне нужно написать. Я сделал немного x86 еще в колледже и просмотрел множество уроков, но ни один из них не выглядел как странный вывод, который я вижу здесь:
.section __TEXT,__text,regular,pure_instructions
.globl _main
.align 4, 0x90
_main:
Leh_func_begin1:
pushq %rbp
Ltmp0:
movq %rsp, %rbp
Ltmp1:
subq $32, %rsp
Ltmp2:
movl %edi, %eax
movl %eax, -4(%rbp)
movq %rsi, -16(%rbp)
leaq L_.str(%rip), %rax
movq %rax, %rdi
callq _puts
movl $0, -24(%rbp)
movl -24(%rbp), %eax
movl %eax, -20(%rbp)
movl -20(%rbp), %eax
addq $32, %rsp
popq %rbp
ret
Leh_func_end1:
.section __TEXT,__cstring,cstring_literals
L_.str:
.asciz "Hello, World!"
.section __TEXT,__eh_frame,coalesced,no_toc+strip_static_syms+live_support
EH_frame0:
Lsection_eh_frame:
Leh_frame_common:
Lset0 = Leh_frame_common_end-Leh_frame_common_begin
.long Lset0
Leh_frame_common_begin:
.long 0
.byte 1
.asciz "zR"
.byte 1
.byte 120
.byte 16
.byte 1
.byte 16
.byte 12
.byte 7
.byte 8
.byte 144
.byte 1
.align 3
Leh_frame_common_end:
.globl _main.eh
_main.eh:
Lset1 = Leh_frame_end1-Leh_frame_begin1
.long Lset1
Leh_frame_begin1:
Lset2 = Leh_frame_begin1-Leh_frame_common
.long Lset2
Ltmp3:
.quad Leh_func_begin1-Ltmp3
Lset3 = Leh_func_end1-Leh_func_begin1
.quad Lset3
.byte 0
.byte 4
Lset4 = Ltmp0-Leh_func_begin1
.long Lset4
.byte 14
.byte 16
.byte 134
.byte 2
.byte 4
Lset5 = Ltmp1-Ltmp0
.long Lset5
.byte 13
.byte 6
.align 3
Leh_frame_end1:
.subsections_via_symbols
Теперь... может быть, все немного изменилось, но это не совсем удобно, даже для ассемблерного кода. Мне трудно обдумать это... Кто-нибудь поможет разобраться, что происходит в этом коде и зачем это все нужно?
Большое, большое спасибо заранее.
3 ответа
Поскольку вопрос действительно об этих нечетных метках и данных, а не о самом коде, я лишь собираюсь пролить на них немного света.
Если инструкция программы вызывает ошибку выполнения (например, деление на 0 или доступ к недоступной области памяти или попытку выполнить привилегированную инструкцию), это приводит к исключению (не исключение типа C++, а скорее тип прерывания). из него) и заставляет процессор выполнять соответствующий обработчик исключений в ядре ОС. Если бы мы полностью запретили эти исключения, история была бы очень короткой, ОС просто прекратила бы программу.
Тем не менее, есть преимущества, позволяющие программам обрабатывать свои собственные исключения, и поэтому основной обработчик исключений в обработчике ОС отражает некоторые исключения обратно в программу для обработки. Например, программа может попытаться восстановиться после исключения или сохранить значимый отчет о сбое перед завершением.
В любом случае полезно знать следующее:
- функция, в которой произошло исключение, а не просто оскорбительная инструкция в ней
- функция, которая вызвала эту функцию, функция, которая вызвала эту функцию и так далее
и возможно (в основном для отладки):
- строка файла исходного кода, из которой сгенерирована эта инструкция
- линии, где были сделаны эти вызовы функций
- параметры функции
Почему мы должны знать дерево вызовов?
Ну, если программа регистрирует свои собственные обработчики исключений, она обычно делает что-то вроде C++ try
а также catch
блоки:
fxn()
{
try
{
// do something potentially harmful
}
catch()
{
// catch and handle attempts to do something harmful
}
catch()
{
// catch and handle attempts to do something harmful
}
}
Если ни один из тех catches
ловит, исключение распространяется на вызывающего fxn
и потенциально для абонента вызывающего абонента fxn
до тех пор, пока catch
который перехватывает исключение или до обработчика исключений по умолчанию, который просто завершает программу.
Итак, вам нужно знать регионы кода, которые каждый try
чехлы и нужно знать как добраться до ближайшего ближайшего try
(в вызывающей fxn
Например, если try
/catch
не поймать исключение, и оно должно всплыть.
Диапазоны для try
и места catch
блоки легко кодируются в специальном разделе исполняемого файла и с ними легко работать (просто выполните бинарный поиск адресов инструкций в этих диапазонах). Но выяснить следующий внешний try
Блок сложнее, потому что вам может понадобиться узнать адрес возврата из функции, где произошло исключение.
И вы не всегда можете положиться на rbp+8
указывая на адрес возврата в стеке, потому что компилятор может оптимизировать код таким образом, чтобы rbp
больше не участвует в доступе к параметрам функции и локальным переменным. Вы можете получить к ним доступ через rsp+something
а также сохранить регистр и несколько инструкций, но учитывая тот факт, что разные функции выделяют разное количество байтов в стеке для локальных и параметров, передаваемых в другие функции, и настраивают rsp
иначе, просто значение rsp
недостаточно узнать адрес возврата и вызывающую функцию. rsp
может быть произвольным числом байтов от адреса возврата в стеке.
Для таких сценариев компилятор включает дополнительную информацию о функциях и их использовании в специальном разделе исполняемого файла. Код обработки исключений проверяет эту информацию и правильно раскручивает стек, когда исключения должны распространяться на вызывающие функции и их try
/catch
блоки.
Итак, данные следующие _main.eh
содержит эту дополнительную информацию. Обратите внимание, что он явно кодирует начало и размер main()
ссылаясь на Leh_func_begin1
а также Leh_func_end1-Leh_func_begin1
, Эта информация позволяет идентифицировать код обработки исключений main()'s
инструкции как main()'s
,
Также кажется, что main()
не очень уникален, и некоторая информация о его стеке / исключении такая же, как и в других функциях, и имеет смысл делиться ею между ними. И поэтому есть ссылка на Leh_frame_common
,
Я не могу комментировать структуру _main.eh
и точное значение этих констант, таких как 144
а также 13
поскольку я не знаю формат этих данных. Но обычно не нужно знать эти детали, если они не являются разработчиками компилятора или отладчика.
Я надеюсь, что это даст вам представление о том, для чего эти метки и константы.
Хорошо, давайте попробуем
// Первая часть кода, объявляющая основную функцию, которая должна быть выровнена по 32-битной границе.
ОБНОВЛЕНИЕ: моё объяснение директивы.align может быть неверным. См. Газовую документацию ниже.
.section __TEXT,__text,regular,pure_instructions
.globl _main
.align 4, 0x90
_main:
Сохраните предыдущий базовый указатель и выделите место в стеке для локальных переменных.
Leh_func_begin1:
pushq %rbp
Ltmp0:
movq %rsp, %rbp
Ltmp1:
subq $32, %rsp
Ltmp2:
Выдвиньте аргументы в стек и вызовите put ()
movl %edi, %eax
movl %eax, -4(%rbp)
movq %rsi, -16(%rbp)
leaq L_.str(%rip), %rax
movq %rax, %rdi
callq _puts
Поместите возвращаемое значение в стек, освободите локальную память, восстановите базовый указатель и вернитесь.
movl $0, -24(%rbp)
movl -24(%rbp), %eax
movl %eax, -20(%rbp)
movl -20(%rbp), %eax
addq $32, %rsp
popq %rbp
ret
Leh_func_end1:
Следующий раздел, также раздел кода, содержащий строку для печати.
.section __TEXT,__cstring,cstring_literals
L_.str:
.asciz "Hello, World!"
Остальное мне неизвестно, это могут быть данные, используемые при запуске, и / или отладочная информация.
.section __TEXT,__eh_frame,coalesced,no_toc+strip_static_syms+live_support
...
ОБНОВЛЕНИЕ: документация по директиве.align: http://sourceware.org/binutils/docs-2.23.1/as/Align.html
"Способ указания требуемого выравнивания варьируется от системы к системе. Для arc, hppa, i386 с использованием ELF, i860, iq2000, m68k, or32, s390, sparc, tic4x, tic80 и xtensa, первое выражение - это запрос на выравнивание в байт. Например, `.align 8'увеличивает счетчик местоположений до тех пор, пока он не будет кратен 8. Если счетчик местоположений уже кратен 8, никаких изменений не требуется. Для tic54x первое выражение - это запрос выравнивания в словах,
Для других систем, включая ppc, i386, использующих формат a.out, arm и strongarm, это число нулевых битов младшего разряда, которое должен иметь счетчик местоположения после продвижения. Например, `.align 3'увеличивает счетчик местоположений до кратного 8. Если счетчик местоположений уже кратен 8, никаких изменений не требуется.
Это несоответствие связано с различным поведением различных собственных ассемблеров для этих систем, которые GAS должен эмулировать. GAS также предоставляет директивы.balign и.p2align, описанные ниже, которые имеют согласованное поведение во всех архитектурах (но специфичны для GAS). "
// Дж.К.
Вы можете найти ответы практически на любые ваши вопросы, связанные с директивами, здесь и здесь.
Например:
.section __TEXT,__text,regular,pure_instructions
Объявляет раздел с именем __TEXT,__text
с типом раздела по умолчанию и укажите, что этот раздел будет содержать только машинный код (т.е. без данных).
.globl _main
Делает _main
метка (символ) глобальная, так что она будет видна компоновщику.
.align 4, 0x90
Выравнивает счетчик местоположения по следующей 2^4 (==16) байтовой границе. Промежуток между ними будет заполнен значением 0x90 (==NOP).
Что касается самого кода, он явно выполняет много избыточных промежуточных загрузок и хранилищ. Попробуйте выполнить компиляцию с включенной оптимизацией, как предложил один из комментаторов, и вы обнаружите, что полученный код будет иметь больше смысла.