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

Теперь... возможно, все немного изменилось, но это не совсем удобно, даже для ассемблерного кода. Мне трудно обдумать это... Кто-нибудь поможет разобраться, что происходит в этом коде и зачем все это нужно?

Большое, большое спасибо заранее.


person Mike Bell    schedule 08.03.2013    source источник
comment
Это не причудливо. И если вы включите оптимизацию кода (например, с помощью -O2), это, вероятно, будет иметь больше смысла.   -  person Alexey Frunze    schedule 08.03.2013
comment
Добро пожаловать в сборку x86_64. Попробуйте скомпилировать с опцией -m32. Это может дать вам более знакомый результат.   -  person Martin Green    schedule 08.03.2013
comment
@AlexeyFrunze Я только что скомпилировал пример C Hello world, который я написал с -O2, на тот случай, если он уже не был по умолчанию (мне сказали, что gcc использует -O2 по умолчанию, каким-то образом ... может быть) был во времена моего Gentoo). Исходный код сборки не сильно отличается от приведенного выше кода:   -  person Mike Bell    schedule 09.03.2013
comment
Как насчет ненужных сохранений в память с последующим чтением оттуда? Они должны были пойти с -O2. Кроме того, если вы используете параметры отладки (было ли это -g?), вы должны отказаться от них, так как они влияют на оптимизацию.   -  person Alexey Frunze    schedule 09.03.2013
comment
@AlexeyFrunze Извините, я не закончил свой комментарий выше: ... Я должен был быть более конкретным в том, что я имел в виду под «причудливым», что я имел в виду только в шутку. Система маркировки мне сейчас незнакома, в отличие от моего короткого периода работы с x86 в колледже. Я понимаю концепцию оптимизации инструкций и выполнения странных арифметических операций при сборке с помощью компилятора, но меня больше интересует маркировка разделов и то, что делает каждый раздел, а не инструкция для инструкции, а в целом.   -  person Mike Bell    schedule 09.03.2013
comment
Ага! Но это следовало уточнить в вопросе! :)   -  person Alexey Frunze    schedule 09.03.2013


Ответы (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, так как не знаю формата этих данных. Но, как правило, эти подробности знать не нужно, если только они не являются разработчиками компилятора или отладчика.

Я надеюсь, что это дало вам представление о том, для чего нужны эти метки и константы.

person Alexey Frunze    schedule 09.03.2013
comment
Отличный ответ. Спасибо. Это дает мне достаточно хорошее представление о том, что происходит за кулисами, не запутываясь. Любопытство возникает из-за желания написать от руки современный x86_64 (Бог знает почему, правда?). - person Mike Bell; 09.03.2013

Хорошо, давайте попробуем

// Первый раздел кода, объявляющий основную функцию, которая должна быть выровнена по 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:

Поместите аргументы в стек и вызовите puts()

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!"

Остальное мне неизвестно, могут быть данные, используемые как код запуска c или информация об отладке.

.section    __TEXT,__eh_frame,coalesced,no_toc+strip_static_syms+live_support
...

ОБНОВЛЕНИЕ: Документация по директиве .align из: http://sourceware.org/binutils/docs-2.23.1/as/Align.html#Align

«Способ указания требуемого выравнивания зависит от системы. Для дуг, hppa, i386 с использованием ELF, i860, iq2000, m68k или 32, 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)».

//jk

person j.karlsson    schedule 08.03.2013

Вы можете найти ответы практически на любые вопросы, связанные с директивами здесь и здесь.

Например:

.section    __TEXT,__text,regular,pure_instructions

Объявляет раздел с именем __TEXT,__text с типом раздела по умолчанию и указывает, что этот раздел будет содержать только машинный код (т. е. без данных).


.globl _main
Делает метку (символ) _main глобальной, чтобы она была видна компоновщику.


.align 4, 0x90
Выравнивает счетчик местоположения по границе следующего 2^4 (==16) байта. Пространство между ними будет заполнено значением 0x90 (==NOP).

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

person Michael    schedule 08.03.2013