2013-03-08 11 views
7

Chcę się dowiedzieć więcej na temat montażu x86/x86_64. Niestety, jestem na Macu. Nie ma problemu, prawda?Co się dzieje w zestawie Apple LLVM-gcc x86?

$ 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. 

Napisałem prosty "Hello World" w C, aby uzyskać informację o tym, jakiego rodzaju kodu będę musiał napisać. Zrobiłem trochę x86 Powrót na studiach, i spojrzał liczne tutoriale, ale żaden z nich nie wyglądał na wyjściu szalonym widzę tutaj:

.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 

... może teraz wiele się zmieniło trochę, ale to nie jest dokładnie przyjazne, nawet dla kodu zespołu. Ciężko mi o tym opowiadać ... Czy ktoś pomógłby zepsuć to, co się dzieje w tym kodzie i dlaczego to wszystko jest potrzebne?

Wiele, wielkie dzięki z góry.

+0

To nie jest dziwaczne. A jeśli włączysz optymalizację kodu (np. Z '-O2'), prawdopodobnie będzie to miało więcej sensu. –

+1

Witamy w zestawie x86_64. Spróbuj skompilować z opcją '-m32'. To może dać ci bardziej znane wyniki. –

+0

@AlexeyFrunze Właśnie skompilowałem próbkę C Witaj, świecie, który napisałem z -O2, na wszelki wypadek, gdyby nie był już domyślny (byłem przekonany, że gcc używa -O2 domyślnie, jakoś ... może byłem w trakcie moich dni w Gentoo). Kod źródłowy zespołu nie wygląda inaczej niż powyższy kod: –

Odpowiedz

10

Ponieważ pytanie jest tak naprawdę o tych dziwnych etykiet i danych, a nie tak naprawdę o samym kodzie, zamierzam tylko rzucić trochę światła na nie.

Jeśli instrukcja programu powoduje błąd wykonania (taki jak podział przez 0 lub dostęp do niedostępnego regionu pamięci lub próba wykonania uprzywilejowanej instrukcji), powoduje to wyjątek (nie jest to wyjątek C++, a raczej rodzaj przerwań) i zmusza procesor do wykonania odpowiedniego programu obsługi wyjątku w jądro systemu operacyjnego. Gdybyśmy całkowicie odrzucili te wyjątki, historia byłaby bardzo krótka, system operacyjny po prostu zakończyłby program.

Istnieją jednak zalety pozwalające programom obsługiwać własnych wyjątków, więc główna procedura obsługi wyjątków w procedurze obsługi systemu operacyjnego odzwierciedla niektóre wyjątki z powrotem do programu do obsługi. Na przykład program może próbować odzyskać dane z wyjątku lub może zapisać znaczący raport awarii przed zakończeniem.

W każdym przypadku warto wiedzieć, co następuje:

  • funkcja, gdzie wystąpił wyjątek, a nie tylko dyspozycja popełniła w nim
  • funkcja, która nazywa tę funkcję, funkcja który nazywa się, że jeden i tak dalej

i ewentualnie (głównie do debugowania):

  • linia pliku kodu źródłowego, z których ta instrukcja została wygenerowana
  • linie gdzie zostały wykonane te wywołania funkcji
  • funkcja Parametry

Dlaczego musimy wiedzieć drzewo połączeń?

Dobrze, jeśli program rejestruje swoje własne procedury obsługi wyjątków, to zwykle robi to coś jak C++ try i catch bloków:

fxn() 
{ 
    try 
    { 
    // do something potentially harmful 
    } 
    catch() 
    { 
    // catch and handle attempts to do something harmful 
    } 
    catch() 
    { 
    // catch and handle attempts to do something harmful 
    } 
} 

Jeśli żadna z tych catches połowów wyjątek propaguje do wywołującego fxn i potencjalnie dla osoby dzwoniącej osoby dzwoniącej pod numer fxn, dopóki nie pojawi się catch, który przechwytuje wyjątek lub do domyślnego programu obsługi wyjątku, który po prostu kończy program.

Tak, trzeba wiedzieć regiony kod, który każdy try normą i trzeba wiedzieć, jak dostać się do następnego najbliżej try (w wywołującego fxn, na przykład), jeżeli natychmiastowe try/catch nie złapać wyjątek i musi się upaść.

Zakresy dla try oraz lokalizacje bloków catch są łatwe do zakodowania w specjalnej sekcji pliku wykonywalnego i są łatwe w użyciu (wystarczy wykonać wyszukiwanie binarne w celu obezwładnienia adresów instrukcji w tych zakresach). Ale znalezienie następnego zewnętrznego bloku jest trudniejsze, ponieważ może zajść potrzeba znalezienia adresu zwrotnego z funkcji, w której wystąpił wyjątek.

I nie zawsze można polegać na rbp+8 wskazującym na adres zwrotny na stosie, ponieważ kompilator może zoptymalizować kod w taki sposób, że rbp nie jest już zaangażowany w uzyskiwanie dostępu do parametrów funkcji i zmiennych lokalnych. Możesz uzyskać do nich dostęp poprzez rsp+something i zapisać rejestr oraz kilka instrukcji, ale biorąc pod uwagę fakt, że różne funkcje przydzielają różną liczbę bajtów na stosie dla locals i parametry przekazywane innym funkcjom i zmieniają rsp inaczej, po prostu wartość z rsp nie wystarczy, aby znaleźć adres zwrotny i funkcję dzwoniącą. rsp może być dowolną liczbą bajtów poza miejscem, w którym adres powrotu znajduje się na stosie.

Dla takich scenariuszy kompilator zawiera dodatkowe informacje o funkcjach i ich stosu w stosownej sekcji pliku wykonywalnego. Kod obsługi wyjątków sprawdza te informacje i prawidłowo rozwija stos, gdy wyjątki muszą być propagowane do funkcji wywołujących i ich bloków.

Dane następujące po _main.eh zawierają dodatkowe informacje. Zauważ, że jawnie koduje początek i rozmiar main(), odnosząc się do Leh_func_begin1 i Leh_func_end1-Leh_func_begin1. Ta informacja umożliwia kodowi obsługi wyjątków określenie main()'s instrukcji jako main()'s.

Wygląda również na to, że main() nie jest bardzo unikalny i niektóre z jego informacji o stosie/wyjątku są takie same jak w innych funkcjach i ma sens podzielenie się nimi między nimi. I tak jest odniesienie do Leh_frame_common.

Nie mogę dalej komentować struktury _main.eh i dokładnego znaczenia tych stałych, takich jak 144 i 13, ponieważ nie znam formatu tych danych. Ale generalnie nie trzeba znać tych szczegółów, chyba że są one programistami kompilatora lub debuggera.

Mam nadzieję, że dzięki temu dowiesz się, do czego służą te etykiety i stałe.

+0

Doskonała odpowiedź. Dziękuję Ci. To daje mi wystarczająco dobre pojęcie o tym, co dzieje się za kulisami, nie robiąc dużo brudniej. Ciekawość wynika z chęci ręcznego pisania współczesnego x86_64 (Bóg wie, dlaczego?). –

2

Możesz znaleźć odpowiedzi na prawie wszystkie pytania związane z dyrektywami: here i here.

Na przykład:

.section __TEXT,__text,regular,pure_instructions 

Deklaruje sekcję o nazwie __TEXT,__text z typem domyślnym sekcji i określić, że ta sekcja będzie zawierać tylko kod maszynowy (czyli żadnych danych).


.globl _main
Powoduje _main etykieta (symbol) globalny, tak że będzie ona widoczna dla łącznika.


.align 4, 0x90
wyrównanie licznika położenia do następnego 2^4 (== 16) granicy bajtu. Przestrzeń pomiędzy nimi zostanie wypełniona wartością 0x90 (== NOP).

Co do samego kodu, to oczywiście robi wiele zbędnych obciążeń pośrednich i sklepów. Spróbuj kompilować z włączonymi optymalizacjami, tak jak sugerował jeden z komentatorów, i powinieneś odkryć, że wynikowy kod będzie bardziej sensowny.

3

Ok pozwala spróbować

// pierwszej sekcji kodu, deklarując główną funkcję, którą należy wyrównać na granicy 32 bitów.

AKTUALIZACJA: Moje wyjaśnienie dyrektywy .align może być nieprawidłowe. Zobacz dokumentację gazową poniżej.

.section __TEXT,__text,regular,pure_instructions 
.globl _main 
.align 4, 0x90 
_main: 

Zapisz poprzedni wskaźnik bazowy i przydziel obszar stosu dla zmiennych lokalnych.

Leh_func_begin1: 
pushq %rbp 
Ltmp0: 
movq %rsp, %rbp 
Ltmp1: 
subq $32, %rsp 
Ltmp2: 

push argumentów na stosie i nazywają puts()

movl %edi, %eax 
movl %eax, -4(%rbp) 
movq %rsi, -16(%rbp) 
leaq L_.str(%rip), %rax 
movq %rax, %rdi 
callq _puts 

Put wartość powrotu na stosie, wolnej pamięci lokalnej, należy przywrócić wskaźnik bazowy i powrotu.

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: 

Następna sekcja, również sekcja kodu, zawierająca ciąg do wydrukowania.

.section __TEXT,__cstring,cstring_literals 
L_.str: 
.asciz "Hello, World!" 

Reszta jest dla mnie nieznana, może to być dane użyte w kodzie startowym oraz informacje o debugowaniu.

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

UPDATE: Dokumentacja na .align dyrektywy od: http://sourceware.org/binutils/docs-2.23.1/as/Align.html#Align

„Sposób, w jaki wymagany jest określony wyrównanie różni od systemu Na łuku hppa, i386, i860 zastosowaniem ELF,. iq2000, m68k, or32, s390, sparc, tic4x, tic80 i xtensa, pierwszym wyrażeniem jest żądanie wyrównania w bajtach, na przykład `.align 8 'przesuwa licznik położenia, aż stanie się wielokrotnością 8. Jeśli licznik lokalizacji jest już jest wielokrotnością liczby 8. Nie ma potrzeby zmiany.w przypadku tic54x, pierwszym wyrażeniem jest żądanie wyrównania w słowach:

W przypadku innych systemów, w tym ppc, i386 z użyciem formatu a.out, ramienia i broni sportowej, jest to liczba bitów zerowych niskiego rzędu, które licznik lokalizacji musi mieć po rozwinięciu. Na przykład `.align 3 'przesuwa licznik położenia, aż osiągnie wielokrotność 8. Jeśli licznik pozycji jest już wielokrotnością 8, zmiana nie jest potrzebna.

Ta niespójność wynika z różnych zachowań różnych natywnych asemblerów dla tych systemów, które GAS musi emulować. GAS zapewnia również .balign i .p2align dyrektyw, opisane poniżej, które mają spójne zachowanie we wszystkich architekturach (ale są specyficzne dla gazu).”

// jk

Powiązane problemy