Interner Aufbau von IL2CPP: Debugging-Tipps für generierten Code

JOSH PETERSON / UNITY TECHNOLOGIESSenior Software Engineer
May 20, 2015|8 Min.
Interner Aufbau von IL2CPP: Debugging-Tipps für generierten Code
Diese Website wurde aus praktischen Gründen für Sie maschinell übersetzt. Die Richtigkeit und Zuverlässigkeit des übersetzten Inhalts kann von uns nicht gewährleistet werden. Sollten Sie Zweifel an der Richtigkeit des übersetzten Inhalts haben, schauen Sie sich bitte die offizielle englische Version der Website an.

Dies ist der dritte Blogbeitrag in der Reihe „IL2CPP Internals“ . In diesem Beitrag untersuchen wir einige Tipps, die das Debuggen von von IL2CPP generiertem C++-Code etwas einfacher machen. Wir werden sehen, wie man Haltepunkte setzt, den Inhalt von Zeichenfolgen und benutzerdefinierten Typen anzeigt und bestimmt, wo Ausnahmen auftreten.

Bedenken Sie dabei, dass wir generierten C++-Code debuggen, der aus .NET IL-Code erstellt wurde. Daher wird das Debuggen wahrscheinlich nicht die angenehmste Erfahrung sein. Mit einigen dieser Tipps ist es jedoch möglich, aussagekräftige Einblicke in die Ausführung des Codes für ein Unity-Projekt auf dem tatsächlichen Zielgerät zu erhalten (am Ende des Beitrags sprechen wir ein wenig über das Debuggen von verwaltetem Code).

Seien Sie außerdem darauf vorbereitet, dass der in Ihrem Projekt generierte Code von diesem Code abweicht. Mit jeder neuen Version von Unity suchen wir nach Möglichkeiten, den generierten Code besser, schneller und kleiner zu machen.

Das Setup

Für diesen Beitrag verwende ich Unity 5.0.1p3 unter OSX. Ich verwende dasselbe Beispielprojekt wie im Beitrag zum generierten Code, dieses Mal erstelle ich es jedoch für das iOS-Ziel mithilfe des IL2CPP-Skripting-Backends. Wie im vorherigen Beitrag werde ich mit ausgewählter Option „Development Player“ erstellen, sodass il2cpp.exe C++-Code mit Typ- und Methodennamen basierend auf den Namen im IL-Code generiert.

Nachdem Unity mit der Generierung des Xcode-Projekts fertig ist, kann ich es in Xcode öffnen (ich habe Version 6.3.1, aber jede aktuelle Version sollte funktionieren), mein Zielgerät auswählen (ein iPad Mini 3, aber jedes iOS-Gerät sollte funktionieren) und das Projekt in Xcode erstellen.

Festlegen von Haltepunkten

Bevor ich das Projekt ausführe, setze ich zunächst oben in der Startmethode in der HelloWorld-Klasse einen Haltepunkt. Wie wir im vorherigen Beitrag gesehen haben, lautet der Name dieser Methode im generierten C++-Code HelloWorld_Start_m3. Wir können Cmd+Umschalt+O verwenden und mit der Eingabe des Namens dieser Methode beginnen, um sie in Xcode zu suchen und dann darin einen Haltepunkt zu setzen.

image05

Wir können in XCode auch „Debuggen > Haltepunkte > Symbolischen Haltepunkt erstellen“ wählen und ihn so einstellen, dass er bei dieser Methode unterbrochen wird.

image02

Wenn ich jetzt das Xcode-Projekt ausführe, sehe ich sofort, dass es am Anfang der Methode unterbrochen wird.

Wir können auf diese Weise Haltepunkte für andere Methoden im generierten Code setzen, wenn wir den Namen der Methode kennen. Wir können in Xcode auch Haltepunkte an einer bestimmten Zeile in einer der generierten Codedateien setzen. Tatsächlich sind alle generierten Dateien Teil des Xcode-Projekts. Sie finden sie im Projektnavigator im Verzeichnis Classes/Native.

image03

Zeichenfolgen anzeigen

Es gibt zwei Möglichkeiten, die Darstellung einer IL2CPP-Zeichenfolge in Xcode anzuzeigen. Wir können den Speicher eines Strings direkt anzeigen oder eines der String-Dienstprogramme in libil2cpp aufrufen, um den String in einen std::string zu konvertieren, den Xcode anzeigen kann. Sehen wir uns den Wert der Zeichenfolge mit dem Namen _stringLiteral1 an (Spoiler-Alarm: Der Inhalt lautet „Hallo, IL2CPP!“).

Im generierten Code mit integrierten Ctags (oder mit Cmd+Strg+J in Xcode) können wir zur Definition von _stringLiteral1 springen und sehen, dass sein Typ Il2CppString_14 ist:

Unbekannter Blocktyp „codeBlock“, bitte geben Sie einen Serialisierer dafür in der Eigenschaft „serializers.types“ an

Tatsächlich werden alle Zeichenfolgen in IL2CPP auf diese Weise dargestellt. Die Definition von Il2CppString finden Sie in der Header-Datei object-internals.h. Diese Zeichenfolgen umfassen den Standardheaderteil jedes verwalteten Typs in IL2CPP, Il2CppObject (auf das über den Il2CppDataSegmentString-Typ zugegriffen wird), gefolgt von einer Länge von vier Byte und dann einem Array aus zwei Byte großen Zeichen. Zur Kompilierzeit definierte Zeichenfolgen wie _stringLiteral1 ergeben am Ende ein Zeichenarray mit fester Länge, während zur Laufzeit erstellte Zeichenfolgen ein zugewiesenes Array haben. Die Zeichen in der Zeichenfolge sind als UTF-16 codiert.

Wenn wir _stringLiteral1 zum Überwachungsfenster in Xcode hinzufügen, können wir die Option „Speicher von „_stringLiteral1“ anzeigen“ auswählen, um das Layout der Zeichenfolge im Speicher anzuzeigen.

image06

Dann können wir im Speicherbetrachter Folgendes sehen:

image00

Das Header-Element der Zeichenfolge ist 16 Bytes lang. Wenn wir dies überspringen, können wir sehen, dass die vier Bytes für die Größe einen Wert von 0x000E (14) haben. Das nächste Byte nach der Länge ist das erste Zeichen der Zeichenfolge, 0x0048 ('H'). Da jedes Zeichen zwei Bytes breit ist, in diesem String aber alle Zeichen in nur ein Byte passen, zeigt Xcode sie auf der rechten Seite mit Punkten zwischen den einzelnen Zeichen an. Der Inhalt der Zeichenfolge ist jedoch dennoch deutlich sichtbar. Diese Methode zum Anzeigen von Zeichenfolgen funktioniert, ist bei komplexeren Zeichenfolgen jedoch etwas schwierig.

Wir können den Inhalt einer Zeichenfolge auch von der lldb-Eingabeaufforderung in Xcode aus anzeigen. Der Header utils/StringUtils.h bietet uns die Schnittstelle für einige String-Dienstprogramme in libil2cpp, die wir verwenden können. Lassen Sie uns insbesondere die Methode Utf16ToUtf8 von der lldb-Eingabeaufforderung aus aufrufen. Die Benutzeroberfläche sieht folgendermaßen aus:

Unbekannter Blocktyp „codeBlock“, bitte geben Sie einen Serialisierer dafür in der Eigenschaft „serializers.types“ an

Wir können das Chars-Element der C++-Struktur an diese Methode übergeben und sie gibt einen UTF-8-codierten std::string zurück. Wenn wir dann an der lldb-Eingabeaufforderung den Befehl p verwenden, können wir den Inhalt der Zeichenfolge ausdrucken.

Unbekannter Blocktyp „codeBlock“, bitte geben Sie einen Serialisierer dafür in der Eigenschaft „serializers.types“ an


Benutzerdefinierte Typen anzeigen

Wir können auch den Inhalt eines benutzerdefinierten Typs anzeigen. Im einfachen Skriptcode dieses Projekts haben wir einen C#-Typ namens „Important“ mit einem Feld namens „InstanceIdentifier“ erstellt. Wenn ich einen Haltepunkt setze, direkt nachdem wir die zweite Instanz des wichtigen Typs im Skript erstellt haben, kann ich sehen, dass der generierte Code InstanceIdentifier wie erwartet auf den Wert 1 gesetzt hat.

image09

Das Anzeigen des Inhalts benutzerdefinierter Typen im generierten Code erfolgt daher auf die gleiche Weise, wie Sie es normalerweise in C++-Code in Xcode tun würden.

Unterbrechen bei Ausnahmen im generierten Code

Ich ertappe mich häufig dabei, generierten Code zu debuggen, um die Ursache eines Fehlers herauszufinden. In vielen Fällen manifestieren sich diese Fehler als verwaltete Ausnahmen. Wie wir im letzten Beitrag besprochen haben, verwendet IL2CPP C++-Ausnahmen, um verwaltete Ausnahmen zu implementieren. Daher können wir das Auftreten einer verwalteten Ausnahme in Xcode auf verschiedene Weise unterbrechen.

Die einfachste Möglichkeit, eine Unterbrechung beim Auslösen einer verwalteten Ausnahme zu vermeiden, besteht darin, einen Haltepunkt für die Funktion il2cpp_codegen_raise_exception festzulegen, die von il2cpp.exe überall dort verwendet wird, wo explizit eine verwaltete Ausnahme ausgelöst wird.

image08

Wenn ich das Projekt dann laufen lasse, bricht Xcode ab, wenn der Code in Start eine Ausnahme vom Typ InvalidOperationException auslöst. Dies ist ein Ort, an dem das Anzeigen von String-Inhalten sehr nützlich sein kann. Wenn ich mir die Mitglieder des Ex-Arguments ansehe, sehe ich, dass es ein ___message_2-Mitglied hat, bei dem es sich um eine Zeichenfolge handelt, die die Meldung der Ausnahme darstellt.

image07

Mit ein wenig Herumprobieren können wir den Wert dieses Strings ausdrucken und sehen, wo das Problem liegt:

Unbekannter Blocktyp „codeBlock“, bitte geben Sie einen Serialisierer dafür in der Eigenschaft „serializers.types“ an


Beachten Sie, dass die Zeichenfolge hier dasselbe Layout wie oben hat, die Namen der generierten Felder jedoch geringfügig abweichen. Das Zeichenfeld heißt ___start_char_1 und sein Typ ist uint16_t, nicht uint16_t[]. Da es sich jedoch immer noch um das erste Zeichen eines Arrays handelt, können wir dessen Adresse an die Konvertierungsfunktion übergeben und finden, dass die Meldung in dieser Ausnahme ziemlich beruhigend ist.

Aber nicht alle verwalteten Ausnahmen werden explizit vom generierten Code ausgelöst. Der Laufzeitcode von libil2cpp löst in einigen Fällen verwaltete Ausnahmen aus und ruft hierzu nicht il2cpp_codegen_raise_exception auf. Wie können wir diese Ausnahmen abfangen?

Wenn wir in Xcode „Debuggen > Haltepunkte > Ausnahme-Haltepunkt erstellen“ verwenden und dann den Haltepunkt bearbeiten, können wir C++-Ausnahmen auswählen und anhalten, wenn eine Ausnahme vom Typ Il2CppExceptionWrapper ausgelöst wird. Da dieser C++-Typ zum Umschließen aller verwalteten Ausnahmen verwendet wird, können wir alle verwalteten Ausnahmen abfangen.

image10

Lassen Sie uns beweisen, dass dies funktioniert, indem wir die folgenden beiden Codezeilen oben in der Start-Methode unseres Skripts hinzufügen:

Unbekannter Blocktyp „codeBlock“, bitte geben Sie einen Serialisierer dafür in der Eigenschaft „serializers.types“ an

Die zweite Zeile hier führt dazu, dass eine NullReferenceException ausgelöst wird. Wenn wir diesen Code in Xcode mit festgelegtem Ausnahme-Haltepunkt ausführen, sehen wir, dass Xcode tatsächlich unterbrochen wird, wenn die Ausnahme ausgelöst wird. Der Haltepunkt befindet sich jedoch im Code in libil2cpp, sodass wir nur Assemblercode sehen. Wenn wir einen Blick auf den Aufrufstapel werfen, können wir erkennen, dass wir einige Frames nach oben zur NullCheck-Methode gehen müssen, die von il2cpp.exe in den generierten Code eingefügt wird.

image01

Von dort aus können wir einen weiteren Frame nach oben gehen und sehen, dass unsere Instanz des Typs „Wichtig“ tatsächlich den Wert NULL hat.

image04

Fazit

Nachdem wir nun einige Tipps zum Debuggen des generierten Codes besprochen haben, hoffe ich, dass Sie nun besser verstehen, wie Sie mithilfe des von IL2CPP generierten C++-Codes mögliche Probleme aufspüren können. Ich empfehle Ihnen, das Layout anderer von IL2CPP verwendeter Typen zu untersuchen, um mehr darüber zu erfahren, wie der generierte Code debuggt wird.

Wo ist jedoch der verwaltete Code-Debugger IL2CPP? Sollten wir nicht in der Lage sein, verwalteten Code zu debuggen, der über das IL2CPP-Skripting-Backend auf einem Gerät ausgeführt wird? Tatsächlich ist dies möglich. Wir haben jetzt einen internen, verwalteten Code-Debugger in Alpha-Qualität für IL2CPP. Es ist noch nicht zur Veröffentlichung bereit, steht aber auf unserer Roadmap, also bleiben Sie dran.

Der nächste Beitrag dieser Reihe untersucht die unterschiedlichen Möglichkeiten, mit denen das IL2CPP-Skripting-Backend verschiedene Arten von Methodenaufrufen implementiert, die in verwaltetem Code vorhanden sind. Wir werden uns die Laufzeitkosten der einzelnen Arten von Methodenaufrufen ansehen.