2013-04-09 12 views
8

Chciałbym zrozumieć, jak wygenerowany zespół i środowisko wykonawcze współpracują ze sobą, i natknąłem się na pytanie podczas przechodzenia przez niektóre generowane kody zespołu.Co naprawdę zrobił kompilator i system operacyjny w moim wygenerowanym zespole?

Źródło Przykład

Tutaj trzy linie Celem-C działa w XCode 4,5:

// Line 1: 
NSObject *obj1 = [[NSObject alloc] init]; 

// Line 2: 
[obj1 release]; 

// Line 3: 
NSObject *obj2; 

Porównanie wygenerowany zespołu

Zwiększenie przez wygenerowanego zestawu, to dokonałem kilku obserwacji.

przed linią 1, adres obj1 się w następujący sposób:

obj1 (NSObject*) 0x00003604 

Po linii 1, zmienia:

obj1 NSObject * 0x08122110 

Obserwacje

1) adres obj1 został zmieniony. Kiedy kod źródłowy jest kompilowany, kompilator przydziela tymczasowo pamięć dla obj1. Następnie (po linii 1) kompilator najwidoczniej ponownie przydziela, więc adres obiektu zmienia się.

2) Po linii 2 adres obj2 jest wciąż taki sam (0x08122110)! Kiedy dzwonię pod numer [obj1 release], mówię kompilatorowi: "Już tego nie potrzebuję, proszę, zabierz to". Ale system faktycznie robi to w pewnym momencie w przyszłości i nie mogę wydawać się kontrolować go bezpośrednio.

3) Debugger nie może przekroczyć linii 3. Nie rozumiem, dlaczego tak się nie stanie!

Pytanie

w zakresie tworzenia i niszczenia obiektów, co jest kompilator faktycznie robi z tych linii kodu (konkretnie o „Alloc-init”, uwolnienia i deklaracji wskaźnika NSObject bez przydziału)? Poza tym, dlaczego debugger nie pozwoli mi przekroczyć trzeciej linii? Czy debugger go nie widzi?

Wraz z odpowiedzią, jeśli możesz polecić niektóre dokumenty lub książkę o tym, co naprawdę robią kompilator i system operacyjny, byłbym wdzięczny. Dziękuję Ci bardzo!

+8

Nie wiem, dlaczego został zamknięty. "Dlaczego debugger nie pozwoli mi przekroczyć tej linii" jest sam w sobie punktem zwrotnym dla wielu nowych programistów. Zrozumienie "ekspresji, która coś robi", w przeciwieństwie do "tej linii jest przeznaczona wyłącznie dla wygody [programisty] i nie robi zbyt wiele" jest całkiem przydatne. I ogólne "co do cholery robi kompilator z tym i jak wchodzi w interakcję z runtime" jest również dość interesujące, choć szczegóły zazwyczaj nie są potrzebne przez większość twórców przez większość czasu. – bbum

+0

Nie mogę jeszcze zrozumieć, dlaczego nie mogę przekroczyć linii 3 ^^ – DungProton

+2

Ponieważ wiersz 3 nie jest instrukcją ("zrób to"), jest to deklaracja ("istnieje"). Przechodzenie między instrukcjami. Deklaracja z inicjatorem, podobnie jak linia 1, jest wystarczająco blisko, aby wykonać krok, ponieważ inicjator jest czymś do zrobienia - co najmniej wstawia coś do zmiennej, a twój inicjator w linii 1 również tworzy obiekt, który stawia tam. –

Odpowiedz

12
  1. Wskaźnik o nazwie obj1 jest tworzony na stosie. Nie jest zainicjowany, co oznacza, że ​​będzie zawierał wszystko, co było w tej lokalizacji pamięci. Jest to stałe źródło błędów, ponieważ użycie niezainicjowanego wskaźnika może prowadzić do nieokreślonego zachowania. Po przydzieleniu obiektu wskaźnik zostaje zainicjalizowany jego adresem.

  2. Adres nie zmienia się, ponieważ wskaźnik nie jest aktualizowany. Kiedy wiadomość jest wysyłana do obiektu, licznik zatrzymania jest zwykle zmniejszany o jeden. Jeśli licznik zatrzymania jest już w jednym, wywoływana jest metoda -dealloc, a pamięć jest oznaczana jako wolna. Tylko pamięć wskazywana przez wskaźnik jest oznaczana jako wolna, ale wskaźnik pozostaje taki sam.Dlatego niektórzy wolą ustawić swoje wskaźniki na nil, gdy już ich nie potrzebują.

  3. Tworzysz niezainicjowany wskaźnik. Ponieważ nie jest on zainicjowany, będzie ponownie wykorzystywał dane, które znajdowały się już w pamięci, w której przechowywany jest wskaźnik.

O zaleceniu dotyczącym książki. Polecam Kompilatory: zasady, techniki i narzędzia.

+0

Przykro mi, chciałem napisać 'NSObject * obj2' w linii 3 – DungProton

+0

I chciałem wspomnieć o kompilatorze i systemie uruchomieniowym, którego używa Apple (XCode)! – DungProton

+1

Należy zauważyć, że ARC zapewnia, że ​​wszystkie zmienne referencyjne obiektu są inicjowane do zera. – bbum

13

Odpowiedź Marcusa jest całkiem dobra, ale tutaj jest trochę więcej szczegółów (chciałbym odświeżyć odczyt wygenerowanego zespołu, a właściwie próbować i wyjaśnić, to jest najlepszy sposób).

NSObject *obj1 = [[NSObject alloc] init]; // Line 1 

kompilator kompiluje dwa wywołania funkcji objc_msgSend(). Pierwszy wywołuje metodę +alloc na klasie NSObject. Wynikiem tego wywołania funkcji staje się pierwszy argument - obiekt docelowy - drugiego wywołania funkcji, który wywołuje metodę -init.

Wynikiem wywoływania init są następnie przechowywane na stosie w fragmencie pamięci, która została zadeklarowana jako nazwany obj1 która ma typ wskaźnik do instancji NSObject.

Możesz przejść przez tę linię w debuggerze , ponieważ jest wykonywane wyrażenie na linii. Jeśli kod został napisany jako:

NSObject *obj1; // declaration 
obj1 = [[NSObject alloc] init]; 

Wtedy okazałoby się, że nie można przekroczyć deklaracji.

Przed obj1 = [[NSObject alloc] init];, the value of obj1 is *undefined* under Manual Retain Release, but **will be automatically set to nil` (0) pod ARC ** (eliminując w ten sposób źródło błędów wskazanych przez Marcusa).

[obj1 release]; // Line 2 

Linia ta wywołuje metodę release na wystąpienie NSObject wskazał przez obj1.

NSObject *obj2; // Line 3 

Ta linia skutecznie nic nie robi. Jeśli optymalizator kompilatora został włączony, w ogóle nie byłoby generowanego kodu. Bez optymalizatora kompilator może podnieść wskaźnik stosu o sizeof(NSObject*), aby zarezerwować miejsce na stosie o nazwie obj2.

I znowu nie można tego zrobić w debugerze, ponieważ nie ma żadnego wyrażenia do wykonania w tym wierszu.


Warto zauważyć, że można przepisać kod jako:

[[[NSObject alloc] init] release]; 

które byłyby skutecznie identyczny z oryginalnym kodem napisałeś o ile egzekucja dotyczy. Bez optymalizatora będzie trochę inaczej, ponieważ nie będzie niczego przechowywać na stosie. Za pomocą optymalizatora prawdopodobnie wygeneruje identyczny kod jak w oryginalnym kodzie.Optymalizator całkiem dobrze eliminuje zmienne lokalne, gdy nie są one potrzebne (co również częściowo tłumaczy, dlaczego debugowanie zoptymalizowanego kodu jest tak trudne).


Biorąc pod uwagę to:

(11) void f() 
(12) { 
(13) NSObject *obj1 = [[NSObject alloc] init]; // Line 1 
(14)  
(15) [obj1 release]; // Line 2 
(16)  
(17) NSObject *obj2; // Line 3 
(18)} 

To unoptimized montaż x86_64. Zignoruj ​​"poprawkę". Spójrz na linie callq; są to rzeczywiste wywołania objc_msgSend(), jak opisano powyżej. Na x86_64% rdi - register - jest argumentem 0 dla wszystkich wywołań funkcji. Tak więc% rdi jest tam, gdzie cel wywołań metod idzie. % rax jest rejestrem używanym do zwracania wartości.

Tak, gdy pojawi się callq, a następnie movq %rax, %rdi, a następnie innym callq, który mówi „weź wartość zwracaną pierwszego callq i przekazać go jako pierwszy argument do następnej callq.

Co do twoje zmienne, po callq zobaczysz takie rzeczy, jak movq %rax, -8(%rbp). Mówi "weź wszystko, co zostało zwrócone przez callq, zapisz je do bieżącego miejsca na stosie, a następnie przesuń wskaźnik stosu w dół o 8 lokalizacji (stos rośnie") Niestety, w zestawie nie są wyświetlane nazwy zmiennych:

_f:          ## @f 
    .cfi_startproc 
Lfunc_begin0: 
    .loc 1 12 0     ## /tmp/asdfafsd/asdfafsd/main.m:12:0 
## BB#0: 
    pushq %rbp 
Ltmp2: 
    .cfi_def_cfa_offset 16 
Ltmp3: 
    .cfi_offset %rbp, -16 
    movq %rsp, %rbp 
Ltmp4: 
    .cfi_def_cfa_register %rbp 
    subq $32, %rsp 
    leaq l_objc_msgSend_fixup_release(%rip), %rax 
    leaq l_objc_msgSend_fixup_alloc(%rip), %rcx 
    .loc 1 13 0 prologue_end  ## /tmp/asdfafsd/asdfafsd/main.m:13:0 
Ltmp5: 
    movq L_OBJC_CLASSLIST_REFERENCES_$_(%rip), %rdx 
    movq %rdx, %rdi 
    movq %rcx, %rsi 
    movq %rax, -24(%rbp)   ## 8-byte Spill 
    callq *l_objc_msgSend_fixup_alloc(%rip) 
    movq L_OBJC_SELECTOR_REFERENCES_(%rip), %rsi 
    movq %rax, %rdi 
    callq _objc_msgSend 
    movq %rax, -8(%rbp) 
    .loc 1 15 0     ## /tmp/asdfafsd/asdfafsd/main.m:15:0 
    movq -8(%rbp), %rax 
    movq %rax, %rdi 
    movq -24(%rbp), %rsi   ## 8-byte Reload 
    callq *l_objc_msgSend_fixup_release(%rip) 
    .loc 1 18 0     ## /tmp/asdfafsd/asdfafsd/main.m:18:0 
    addq $32, %rsp 
    popq %rbp 
    ret 
Ltmp6: 
Lfunc_end0: 

Dla chichotów, zajrzyj na zgromadzeniu generowanego z optymalizatora włączony (-os - najszybsze, najmniejsze, domyślny dla kodu wdrożonej):

Pierwszą rzeczą, aby pamiętać - i to powraca do pytania (3) - czy to nie ma manipulacji %rbp poza pierwszą i ostatnią instrukcją. Oznacza to, że nic nie jest popychane lub wyciągane ze stosu; dosłownie, nie ma dowodów, że kiedykolwiek zostały zadeklarowane, ponieważ kompilator nie potrzebował ich do wygenerowania równoważnego kodu.

Wszystko odbywa się za pośrednictwem rejestrów, a należy pamiętać, że istnieją dwie wartości: move %rax, %rdi. Pierwszym z nich jest „wziąć wynik +alloc i używać go jako pierwszy argument na wezwanie do -init”, a drugi „take wynik -init i używać go jako argumentu do -release

Poza;. %rsi gdzie drugi argument do funkcjonowania połączeń znajduje się na x86_64 dla wywołań metod. - w odniesieniu do wywołań funkcji objc_msgSend() - argument ten będzie zawsze zawierać nazwę metody (wybierak), aby nazwać

Lfunc_begin0: 
    .loc 1 12 0     ## /tmp/asdfafsd/asdfafsd/main.m:12:0 
## BB#0: 
    pushq %rbp 
Ltmp2: 
    .cfi_def_cfa_offset 16 
Ltmp3: 
    .cfi_offset %rbp, -16 
    movq %rsp, %rbp 
Ltmp4: 
    .cfi_def_cfa_register %rbp 
    .loc 1 13 0 prologue_end  ## /tmp/asdfafsd/asdfafsd/main.m:13:0 
Ltmp5: 
    movq L_OBJC_CLASSLIST_REFERENCES_$_(%rip), %rdi 
    leaq l_objc_msgSend_fixup_alloc(%rip), %rsi 
    callq *l_objc_msgSend_fixup_alloc(%rip) 
    movq L_OBJC_SELECTOR_REFERENCES_(%rip), %rsi 
    movq %rax, %rdi 
    callq *[email protected](%rip) 
    .loc 1 15 0     ## /tmp/asdfafsd/asdfafsd/main.m:15:0 
    leaq l_objc_msgSend_fixup_release(%rip), %rsi 
    movq l_objc_msgSend_fixup_release(%rip), %rcx 
    movq %rax, %rdi 
    popq %rbp 
    jmpq *%rcx # TAILCALL 
Ltmp6: 
Lfunc_end0: 

.

Jeśli chcesz dowiedzieć się więcej o metoda wysyłki, napisałem bit of a guide. Jest to kilka wersji objc_msgSend() nieaktualnych, ale nadal istotnych.

Zauważ, że kod ARM działa w taki sam sposób filozoficznie, ale wygenerowany zespół będzie nieco inny i trochę go więcej.


nie mogę jeszcze zrozumieć, dlaczego nie mogę przechodzić nad linią 3 ^^

Jeśli spojrzeć na wygenerowanym montażu, nie ma nic generowane dla zmiennej deklaracji. Przynajmniej nie bezpośrednio. Najbliższy będzie movq %rax, -8(%rbp) , który przenosi wynik init na, ale to jest po dwóch wywołaniach funkcji.

Dla NSObject *obj2; kompilator nie generuje żadnego kodu. Nawet przy wyłączonym optymalizatorze.

Dzieje się tak dlatego, że deklaracja zmiennej nie jest wyrażeniem; w rzeczywistości nie robi nic poza dostarczeniem etykiety - deweloperowi - do przechowywania wartości. Dopiero wtedy, gdy faktycznie używasz zmiennej, która generuje kod.

Tak więc, gdy wkracza się do debuggera, pomija tę linię, ponieważ nie ma nic do zrobienia.

+0

"Możesz przejść przez tę linię w debugerze, ponieważ w wierszu znajduje się wykonane wyrażenie" Nie rozumiem tego! Co to jest wykonane wyrażenie? – DungProton

+0

Dodano wyjaśnienie na końcu. – bbum

+0

Powiedziałeś, że "pomija tę linię, ponieważ nie ma nic do zrobienia". Tak więc debugger wciąż ustępował, gdybym napisał "{}". Myślę, że '{}' również tak naprawdę nic nie robi – DungProton