Was hat der Compiler und das Laufzeitsystem wirklich in meiner generierten Assembly gemacht?

8

Ich würde gern verstehen, wie die generierte Assembly und die Laufzeit zusammenarbeiten, und stieß auf eine Frage, während ich den generierten Assembler-Code durchblätterte.

Quellbeispiel

Hier sind drei Zeilen von Objective-C, die in XCode 4.5 laufen:

%Vor%

Vergleichen der generierten Assembly

Beim Durchlaufen der generierten Baugruppe habe ich ein paar Beobachtungen gemacht.

Vor Zeile 1 lautet die Adresse von obj1 wie folgt:

%Vor%

Nach Zeile 1 ändert sich:

%Vor%

Beobachtungen

1) Die Adresse von obj1 wurde geändert. Wenn der Quellcode kompiliert wird, reserviert der Compiler temporär Speicher für obj1 . Dann (nach Zeile 1) wird der Compiler scheinbar neu zugewiesen, so dass sich die Adresse des Objekts ändert.

2) Nach Zeile 2 ist die Adresse von obj2 immer noch gleich ( 0x08122110 )! Wenn ich [obj1 release] aufruft, sage ich dem Compiler: "Ich brauche das nicht mehr. Bitte nimm es mit." Aber das System macht tatsächlich die Veröffentlichung irgendwann in der Zukunft und ich kann nicht scheinen, es direkt zu kontrollieren.

3) Der Debugger kann Zeile 3 nicht überspringen. Ich verstehe nicht, warum das nicht geht!

Frage

In Bezug auf das Erstellen und Zerstören von Objekten, was macht der Compiler eigentlich mit diesen Codezeilen (speziell einem "alloc-init", einem Release und einer NSObject-Zeigerdeklaration ohne Zuweisung)? Warum lässt mich der Debugger nicht über die dritte Zeile gehen? Kann der Debugger es nicht sehen?

Zusammen mit einer Antwort, wenn Sie bitte einige Dokumente oder ein Buch darüber, was der Compiler und das Laufzeitsystem wirklich tun, empfehlen, würde ich es begrüßen. Vielen Dank!

    
DungProton 09.04.2013, 10:57
quelle

2 Antworten

13

Marcus 'Antwort ist ziemlich gut, aber hier sind ein paar weitere Details (ich hatte vor, die generierte Assembly aufzufrischen; ich muss versuchen, es zu erklären, ist der beste Weg).

%Vor%

Der Compiler kompiliert zwei Funktionsaufrufe an objc_msgSend() . Der erste ruft die Methode +alloc für die Klasse NSObject auf. Das Ergebnis dieses Funktionsaufrufs wird zum ersten Argument - dem Zielobjekt - des zweiten Funktionsaufrufs, der die Methode -init aufruft.

Das Ergebnis des Aufrufs von init wird dann auf dem Stack in einem Speicherbereich gespeichert, den Sie mit dem Namen obj1 deklariert haben und der einen Zeiger auf eine Instanz von NSObject .

Sie können diese Zeile im Debugger durchlaufen, da auf der Zeile ein Ausdruck ausgeführt wird. Wenn der Code wie folgt geschrieben wurde:

%Vor%

Dann würden Sie feststellen, dass Sie die Deklaration nicht durchgehen können.

Vorher obj1 = [[NSObject alloc] init];, the value of obj1 is *undefined* under Manual Retain Release, but **will be automatically set to nil '(0) unter ARC ** (wodurch die Fehlerquelle beseitigt wird, die Marcus angezeigt hat).

%Vor%

Diese Zeile ruft die release -Methode für die -Instanz von NSObject auf, auf die von obj1 zeigt.

%Vor%

Diese Zeile bewirkt effektiv nichts. Wenn der Optimierer des Compilers eingeschaltet wäre, würde überhaupt kein Code generiert werden. Ohne den Optimierer kann der Compiler den Stack-Zeiger um sizeof(NSObject*) anheben, um Speicherplatz auf dem Stack mit dem Namen obj2 zu reservieren.

Und Sie können es auch nicht im Debugger durchgehen, weil in dieser Zeile kein Ausdruck ausgeführt wird.

Beachten Sie, dass Sie den Code wie folgt umschreiben könnten:

%Vor%

Das wäre praktisch identisch mit dem ursprünglichen Code, den Sie geschrieben haben, was die Ausführung betrifft. Ohne den Optimizer wird es ein bisschen anders sein, da es nichts auf dem Stapel speichert. Mit dem Optimierer wird wahrscheinlich identischer Code wie für Ihren ursprünglichen Code generiert. Der Optimierer ist ziemlich gut darin, lokale Variablen zu eliminieren, wenn sie nicht benötigt werden (was teilweise auch der Grund ist, warum das Debuggen von optimiertem Code so schwierig ist).

Gegeben:

%Vor%

Dies ist die nicht optimierte x86_64-Assembly. Ignoriere das "fixup" Zeug. Schauen Sie sich die callq -Zeilen an; Sie sind die eigentlichen Aufrufe von objc_msgSend (), wie oben beschrieben. Auf x86_64 ist% rdi - ein Register - das Argument 0 für alle Funktionsaufrufe. Daher ist% rdi das Ziel von Methodenaufrufen. % rax ist das für Rückgabewerte verwendete Register.

Also, wenn Sie ein Callq sehen, gefolgt von movq %rax, %rdi , gefolgt von einem anderen Callq, das sagt "nimm den Rückgabewert des ersten callq und übergebe es als erstes Argument an das nächste callq .

Wie bei Ihren Variablen sehen Sie Dinge wie movq %rax, -8(%rbp) nach callq . Dies bedeutet "nimm alles, was vom callq zurückgegeben wurde, schreibe es an die aktuelle Stelle auf dem Stapel und bewege dann den Stapelzeiger um 8 Orte (der Stapel wächst nach unten)". Leider zeigt die Assembly die Variablennamen nicht an.

%Vor%

Sehen Sie sich zum Glück die Assembly an, die mit aktiviertem Optimizer generiert wurde (-Os - am schnellsten, am kleinsten, der Standard für den implementierten Code):

Das erste, was zu beachten ist - und das kommt zurück zu Frage (3) - ist, dass es keine Manipulation von %rbp außerhalb der allerersten und allerletzten Anweisungen gibt. Das ist es wird nichts auf den Stapel geschoben oder von ihm abgezogen; Im wörtlichen Sinne gibt es keinen Beweis dafür, dass obj1 und obj2 jemals deklariert wurden, weil der Compiler sie nicht benötigte, um gleichwertigen Code zu generieren.

Alles geschieht über Register und Sie werden feststellen, dass es zwei move %rax, %rdi gibt. Das erste ist "nimm das Ergebnis von +alloc und verwende es als erstes Argument für den Aufruf von -init " und das zweite ist "nimm das Ergebnis von -init und verwende es als Argument für -release .

Beiseite; %rsi ist der Ort, an dem sich das zweite Argument für Funktionsaufrufe auf x86_64 befindet. Bei Methodenaufrufen - bei Aufrufen der Funktion objc_msgSend() - enthält dieses Argument immer den Namen der Methode (Selektor), die aufgerufen werden soll.

%Vor%

Wenn Sie noch mehr über Methodenversand lernen möchten, schrieb ich eine ein bisschen Leitfaden . Es ist ein paar Versionen von objc_msgSend () veraltet, aber immer noch relevant.

Beachten Sie, dass ARM-Code auf die gleiche Weise philosophisch funktioniert, aber die generierte Assembly wird ein bisschen anders und ein bisschen mehr davon sein.

  

Ich kann immer noch nicht verstehen, warum ich nicht über Linie 3 springen kann ^^

Wenn Sie sich die generierte Assembly ansehen, wird nichts für die Variablendeklarationen generiert. Zumindest nicht direkt. Der nächste wäre movq %rax, -8(%rbp) , der das Ergebnis von init in das Objekt verschiebt, aber das ist nach den beiden Funktionsaufrufen .

Für NSObject *obj2; generiert der Compiler keinen Code. Nicht einmal mit deaktiviertem Optimierer.

Das liegt daran, dass eine Variablendeklaration kein Ausdruck ist; es macht eigentlich nichts anderes, als ein Label für Sie - den Entwickler - bereitzustellen, um Werte festzuhalten. Nur wenn Sie die Variable tatsächlich verwenden, wird Code generiert.

Wenn Sie also den Debugger betreten, überspringt er diese Zeile, weil nichts zu tun ist.

    
bbum 09.04.2013 15:55
quelle
12
  1. Der Zeiger namens obj1 wird auf dem Stapel erstellt. Es ist nicht initialisiert, was bedeutet, dass es alles enthält, was an diesem Speicherort war. Dies ist eine ständige Fehlerquelle, da die Verwendung eines nicht initialisierten Zeigers zu einem unspezifizierten Verhalten führen kann. Sobald das Objekt zugewiesen ist, wird der Zeiger mit seiner Adresse initialisiert.

  2. Die Adresse ändert sich nicht, da der Zeiger nicht aktualisiert wird. Wenn die Nachricht -release an das Objekt gesendet wird, wird der Retain-Zähler normalerweise um eins reduziert. Wenn der Retain-Zähler bereits auf 1 gesetzt ist, wird die Methode -dealloc aufgerufen und der Speicher wird als frei markiert. Nur der Speicher, auf den der Zeiger zeigt, ist als frei markiert, der Zeiger bleibt jedoch gleich. Deshalb bevorzugen manche ihre Zeiger auch auf nil , wenn sie sie nicht mehr brauchen.

  3. Sie erstellen einen nicht initialisierten Zeiger. Da es nicht initialisiert ist, wird es die Daten wiederverwenden, die sich bereits an dem Speicherort befanden, an dem der Zeiger gespeichert ist.

Über die Buchempfehlung. Ich würde Compiler empfehlen: Prinzipien, Techniken und Tools .

    
My Karlsson 09.04.2013 11:08
quelle