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:
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!
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 Sie können diese Zeile im Debugger durchlaufen, da auf der Zeile ein Ausdruck ausgeführt wird. Wenn der Code wie folgt geschrieben wurde: Dann würden Sie feststellen, dass Sie die Deklaration nicht durchgehen können. Vorher Diese Zeile ruft die 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 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: 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: Dies ist die nicht optimierte x86_64-Assembly. Ignoriere das "fixup" Zeug. Schauen Sie sich die Also, wenn Sie ein Callq sehen, gefolgt von Wie bei Ihren Variablen sehen Sie Dinge wie 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 Alles geschieht über Register und Sie werden feststellen, dass es zwei Beiseite; 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 Für 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. 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 . 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). release
-Methode für die -Instanz von NSObject auf, auf die von obj1
zeigt. sizeof(NSObject*)
anheben, um Speicherplatz auf dem Stack mit dem Namen obj2
zu reservieren. 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. 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
. 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. %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. 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
. %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.
movq %rax, -8(%rbp)
, der das Ergebnis von init
in das Objekt verschiebt, aber das ist nach den beiden Funktionsaufrufen . NSObject *obj2;
generiert der Compiler keinen Code. Nicht einmal mit deaktiviertem Optimierer.
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.
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.
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 .
Tags und Links objective-c compiler-construction runtime