Какво се случва в сглобяването на 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

„Начинът, по който е указано необходимото подравняване, варира от система до система. За 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)."

//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