Что происходит в сборке 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).

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

Другие вопросы по тегам