Debugger.godMode() - JVM-Anwendungen mit dem Debugger hacken

Andere Sprachen: English Español Français 日本語 한국어 Português 中文

Früher waren Computerspiele anders. Nicht nur haben sich ihre Grafik und Mechaniken weiterentwickelt, sondern sie hatten auch ein Merkmal, das heute nicht mehr so verbreitet zu sein scheint: Cheat-Codes.

Cheat-Codes waren Tastenfolgen, die dir etwas Außergewöhnliches gaben, wie unendliche Munition oder die Fähigkeit, durch Wände zu gehen. Der häufigste und mächtigste von ihnen war der “God Mode”, der dich unverwundbar machte.

Screenshot des Marines aus Doom mit aktiviertem 'God Mode'

So würde dein Charakter in Doom aussehen, wenn du IDDQD eingegeben hättest

Während der God Mode in Spielen nicht mehr so verbreitet ist wie früher und die Ära des IDDQD-Memes zu verblassen scheint, könnte man sich fragen, ob es ein zeitgenössisches Äquivalent gibt. Persönlich habe ich meine eigene moderne Interpretation von IDDQD - den Debugger. Auch wenn er nicht unbedingt mit Spielen zusammenhängt, ruft er dennoch dasselbe Gefühl von Superkräften hervor.

Space Invaders

Hier ist ein unterhaltsames kleines Szenario, um meinen Punkt zu veranschaulichen. Selbst wenn du nicht mit Doom vertraut bist, hast du wahrscheinlich dieses noch ältere Spiel namens Space Invaders gesehen. Wie Doom dreht sich die Handlung um das Thema des Kampfes gegen Invasoren im Weltraum.

Mein Freund und Kollege Eugene Nizienko hat ein IntelliJ IDEA Plugin geschrieben, mit dem du dieses Spiel direkt im Editor spielen kannst

Space Invaders im Editor von IntelliJ IDEA Space Invaders im Editor von IntelliJ IDEA

Es gibt keinen God Mode in diesem Spiel, aber wenn wir sehr entschlossen sind, können wir ihn selbst hinzufügen? Lassen wir die klassische Tradition des Hackens von Programmen mit einem Debugger wiederaufleben, um es herauszufinden!

Info icon

Sei verantwortungsvoll! Ich habe Eugenes Zustimmung eingeholt, bevor ich an seinem Programm herumgebastelt habe. Wenn du den Debugger bei Code verwendest, der nicht dir gehört, stelle sicher, dass du es ethisch tust. Andernfalls lass es einfach sein.

Die Tools vorbereiten

Bereite dich auf eine Meta-Erfahrung vor - wir werden IntelliJ IDEA mit seinem eigenen Debugger debuggen.

Aber es gibt ein kleines Problem: Beim Debuggen von IntelliJ IDEA müssen wir es anhalten, was die IDE unresponsive macht. Daher brauchen wir eine zusätzliche IDE-Instanz, die funktionsfähig bleibt und als unser Debugging-Tool dient.

Um mehrere IDE-Instanzen zu verwalten, werde ich JetBrains Toolbox App verwenden. Das ist eine App, die deine JetBrains IDEs organisiert. Damit kannst du mehrere Versionen derselben IDE installieren oder Shortcuts erstellen, um sie mit verschiedenen Sätzen von VM-Optionen auszuführen.

Installieren wir zwei Instanzen von IntelliJ IDEA:

JetBrains Toolbox zeigt mehrere JetBrains IDEs, darunter zwei Instanzen von IntelliJ IDEA, die Space Invaders und Debug genannt werden. JetBrains Toolbox zeigt mehrere JetBrains IDEs, darunter zwei Instanzen von IntelliJ IDEA, die Space Invaders und Debug genannt werden.

Wenn du dieselbe IDE-Version fuer beide Instanzen verwendest, stelle sicher, dass du verschiedene System-, Config- und Logs-Verzeichnisse in Tool actions | Settings | Configuration angibst. Auf dieser Seite kannst du auch Namen für die IDE-Instanzen zur Bequemlichkeit vergeben. Ich habe sie ‘Space Invaders’ und ‘Debug’ genannt.

Um die Space Invaders-Instanz debuggen zu können, klicke auf Tool actions daneben, dann gehe zu Settings | Edit JVM options. Füge in der Datei, die sich öffnet, folgende Zeile ein:

-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
Die Datei mit den VM-Optionen, die an die IDE-Instanz übergeben werden. Die Datei mit den VM-Optionen, die an die IDE-Instanz übergeben werden.

Dies lässt die Ziel-JVM mit dem Debug-Agent laufen und auf eingehende Debugger-Verbindungen auf Port 5005 lauschen.

Das Spiel starten

Starte die ‘Space Invaders’-Instanz, installiere das Spiel, und starte es, indem du die Space Invaders-Aktion ausführst. Um die Aktion zu finden, drücke zweimal Shift und beginne Space Invaders zu tippen:

Führen Sie die Space Invaders-Aktion über den Dialog aus, der sich beim Drücken der doppelten Shift-Taste öffnet. Führen Sie die Space Invaders-Aktion über den Dialog aus, der sich beim Drücken der doppelten Shift-Taste öffnet.

Lass uns eine Weile spielen und das Verhalten beobachten, das wir beheben wollen: Wenn feindliche Raketen das Raumschiff treffen, verlieren wir Gesundheit von der Gesundheitsleiste in der oberen linken Ecke.

Anhängen und anhalten

Unsere Debugging-Reise beginnt mit dem Öffnen der ‘Debug’-IDE-Instanz und dem Einrichten eines neuen Kotlin-Projekts. Wir brauchen dieses Projekt hauptsächlich, weil es nicht möglich wäre, den Debugger ohne eines zu starten.

Zusätzlich enthält IntelliJ IDEA die Java/Kotlin-Standardbibliothek im neuen Projekttemplate, die wir später möglicherweise verwenden werden. Ich erkläre die Verwendung der Standardbibliothek in den folgenden Kapiteln.

Nach dem Erstellen des Projekts gehe zum Hauptmenü und wähle Run | Attach to Process. Dies zeigt die Liste der lokalen JVMs, die auf Debugger-Attach-Anfragen lauschen. Wähle die andere laufende IDE aus der Liste.

Ein Popup mit der Liste der lokal laufenden JVMs Ein Popup mit der Liste der lokal laufenden JVMs

Wir sollten folgende Nachricht in der Konsole sehen, die bestätigt, dass der Debugger sich erfolgreich an die Ziel-VM angehängt hat.

Connected to the target VM, address: 'localhost:5005', transport: 'socket'

Jetzt kommen wir zum interessanten Teil: Wie halten wir die Anwendung an?

Normalerweise würden wir einen Breakpoint im Anwendungscode setzen, aber in diesem Fall fehlen uns die Quellen sowohl für IntelliJ IDEA als auch für das Space Invaders Plugin. Das verhindert nicht nur, dass wir einen Breakpoint setzen, sondern erschwert auch unser Verständnis davon, wie das Programm funktioniert. Auf den ersten Blick scheint es nichts zu inspizieren oder durchzusteppen.

Glücklicherweise hat IntelliJ IDEA eine Funktion namens Pause Program. Sie ermöglicht es dir, das Programm zu einem beliebigen Zeitpunkt anzuhalten, ohne die entsprechende Codezeile angeben zu müssen. Du findest sie in der Debugger-Symbolleiste oder im Hauptmenue: Run | Debugging Actions | Pause Program.

Das Debug-Werkzeugfenster für die angehaltene Space Invaders-Instanz Das Debug-Werkzeugfenster für die angehaltene Space Invaders-Instanz

Die Anwendung wird angehalten und gibt uns einen Ausgangspunkt fuer das Debugging.

Tip icon

Pause Program ist eine sehr mächtige Technik, die besonders in mehreren fortgeschrittenen Szenarien hilfreich ist. Um mehr zu erfahren, schau dir die verwandten Artikel an:

Die relevanten Objekte finden

Wenn wir unser Ziel in Programmierbegriffen betrachten, läuft es darauf hinaus, zu verhindern, dass die Gesundheit des Raumschiffs sinkt. Finden wir das Objekt, das den entsprechenden Zustand hält.

Da wir nichts über den Plugin-Code wissen, können wir den Heap direkt mit der Memory-Ansicht des IntelliJ IDEA Debuggers inspizieren:

Ein Menü erscheint, wenn Sie auf "Layout-Einstellungen" in der oberen rechten Ecke des Debug-Toolfensters klicken. Ein Menü erscheint, wenn Sie auf "Layout-Einstellungen" in der oberen rechten Ecke des Debug-Toolfensters klicken.

Diese Funktion gibt dir Informationen über alle Objekte, die derzeit aktiv sind. Tippen wir invaders und schauen, ob wir etwas finden können:

Wenn Sie 'invaders' im Suchfeld der Memory-Ansicht eingeben, werden Objekte von Klassen angezeigt, die zum 'spaceinvaders'-Paket gehören. Wenn Sie 'invaders' im Suchfeld der Memory-Ansicht eingeben, werden Objekte von Klassen angezeigt, die zum 'spaceinvaders'-Paket gehören.

Offensichtlich befinden sich die Plugin-Klassen unter dem Paket com.github.nizienko.spaceinvaders . Innerhalb dieses Pakets gibt es eine Klasse namens GameState , die mehrere aktive Instanzen hat. Auf den ersten Blick sieht das nach dem aus, was wir brauchen.

Ein Doppelklick auf GameState zeigt alle Instanzen dieser Klasse:

Ein Dialog öffnet sich, der aktive GameState-Instanzen anzeigt. Ein Dialog öffnet sich, der aktive GameState-Instanzen anzeigt.

Wie sich herausstellt, ist es ein Enum - was nicht genau das ist, wonach wir gesucht haben. Setzen wir unsere Suche fort und stoßen auf eine einzelne Instanz von Game .

Das Erweitern des Knotens lässt uns die Felder der Instanz inspizieren:

Speicheransicht mit einem erweiterten Objektknoten, der die Felder des Objekts anzeigt. Speicheransicht mit einem erweiterten Objektknoten, der die Felder des Objekts anzeigt.

Die health -Eigenschaft scheint die interessante zu sein. Unter ihren Feldern finden wir eines namens _value . In meinem Fall ist der Wert 100 , was damit korreliert, dass die Gesundheitsleiste voll war, als ich das Spiel angehalten habe. Es ist also wahrscheinlich das richtige Feld, und sein Wert scheint von 0 bis 100 zu reichen.

Testen wir diese Hypothese. Rechtsklicke auf _value , dann wähle Set Value. Wähle einen Wert, der sich von deinem aktuellen unterscheidet. Zum Beispiel wähle ich 50 .

Speicheransicht mit einem Textfeld gegen das Feld 'Gesundheit', das den vom Benutzer eingegebenen Wert von 50 enthält. Speicheransicht mit einem Textfeld gegen das Feld 'Gesundheit', das den vom Benutzer eingegebenen Wert von 50 enthält.

Bei diesem Schritt stoßen wir auf einen Fehler, der Cannot evaluate methods after Pause action lautet:

Fehlermeldung "Methoden können nach der Pause-Aktion nicht ausgewertet werden" Fehlermeldung "Methoden können nach der Pause-Aktion nicht ausgewertet werden"

Das Problem entsteht, weil wir Pause Program anstelle von Breakpoints verwendet haben, und diese Funktion kommt mit einigen Einschränkungen. Wir können jedoch einen kleinen Trick verwenden, um dies zu umgehen.

Ich habe ihn in einem der vorherigen Beiträge beschrieben, der die Pause Program-Aktion behandelt. Falls du es dort verpasst hast, hier ist, was getan werden muss: Sobald die Anwendung angehalten ist, führe eine Stepping-Aktion aus, wie Step Into oder Step Over. Das ermöglicht die Verwendung erweiterter Funktionen wie Set Value und Evaluate Expression.

Jetzt sollten wir in der Lage sein, den Wert für health zu setzen. Versuche den Wert zu ändern, dann setze die Anwendung fort, um zu sehen, ob die Gesundheitsleiste Änderungen anzeigt.

Wir haben also das Objekt gefunden, das den relevanten Zustand haelt. Zumindest koennen wir die Gesundheitsleiste von Zeit zu Zeit manuell auffuellen. Wir sind vielleicht noch nicht ganz am Ziel, aber wir kommen naher.

Labels und Ausdrücke

Nachdem wir das Objekt identifiziert haben, auf das wir uns konzentrieren wollen, wäre es praktisch, es zu markieren. Falls du mit Debug-Labels nicht vertraut bist, so sieht ein markiertes Objekt aus:

Die Variablen-Registerkarte zeigt ein Array von Benutzerobjekten, von denen eines mit einem Debug-Label markiert ist, das "User_Charlie" sagt. Die Variablen-Registerkarte zeigt ein Array von Benutzerobjekten, von denen eines mit einem Debug-Label markiert ist, das "User_Charlie" sagt.

Labels können auf viele Arten nützlich sein. Im Kontext dieses Artikels stellt das Markieren des relevanten Objekts sicher, dass wir es direkt in Funktionen wie Evaluate Expression verwenden können, ohne vom aktuellen Ausführungskontext abhängig zu sein.

Leider ist es nicht möglich, _value direkt zu markieren, aber wir können das umschließende Objekt markieren. Um dies zu tun, rechtsklicke auf health , wähle Mark Object, und gib ihm einen Namen.

Dialogfeld zur Auswahl des Objektbezeichners, das den Benutzer auffordert, einen Namen für das Objekt einzugeben. Dialogfeld zur Auswahl des Objektbezeichners, das den Benutzer auffordert, einen Namen für das Objekt einzugeben.

Wir können jetzt testen, wie das Label anderswo funktioniert. Öffne den Evaluate Expression-Dialog und gib health_object_DebugLabel als Ausdruck ein. Wie du sehen kannst, ist das Objekt von überall im Programm durch den Evaluate-Dialog zugänglich:

Bewertung des Dialogs mit dem Diagnoseetikett, das als Ausdruck eingegeben wurde. Bewertung des Dialogs mit dem Diagnoseetikett, das als Ausdruck eingegeben wurde.

Was ist mit dem Ändern der Gesundheit des Raumschiffs aus Evaluate? health_object_DebugLabel._value = 100 funktioniert nicht.

Gleichzeitig scheint _value ein Backing Field einer Kotlin-Property zu sein. Wenn das stimmt, muss Kotlin einen entsprechenden Getter generiert haben:

health_object_DebugLabel.getValue()

Der Evaluate-Dialog denkt, das sei kein gültiger Code, aber wir probieren es trotzdem:

Ein Property über ein Debug-Label im Evaluieren-Dialog referenzieren. Ein Property über ein Debug-Label im Evaluieren-Dialog referenzieren.

Der Ausdruck gibt die aktuelle Gesundheit des Raumschiffs zurück, also funktioniert dieser Ansatz! Wie zu erwarten, funktioniert auch der Setter:

health_object_DebugLabel.setValue(100)

Nach dem Auswerten des Setters setzen wir die Anwendung fort und überprüfen, ob die Änderungen wirksam wurden. Jep - ich sehe eine volle Gesundheitsleiste!

Den Ausdruck einhängen

Der einzige verbleibende Schritt zum Erreichen unseres Ziels ist die Automatisierung der Zustandsänderung, sodass das Auffüllen der Gesundheit im Hintergrund geschieht und wir das Gameplay ohne Unterbrechungen genießen können.

Dies kann mit nicht-anhaltenden Breakpoints gemacht werden. Diese Art von Breakpoint wird üblicherweise für Logging verwendet; jedoch muss der Logging-Ausdruck nicht unbedingt rein sein. Daher können wir den gewünschten Nebeneffekt innerhalb des Logging-Ausdrucks einführen. Aber ohne den Quellcode scheint es so, als hätten wir keinen Ort, um diesen Breakpoint zu setzen.

Erinnerst du dich, wie ich sagte, dass wir möglicherweise die Quellen der Java/Kotlin-Standardbibliothek verwenden könnten? Hier ist die Idee: IntelliJ IDEA und seine Plugins sind in Java/Kotlin geschrieben und verwenden Swing als UI-Framework. Folglich ruft Space Invaders sicherlich Code aus diesen Abhängigkeiten auf. Das bedeutet, dass wir ihre Quellen zum Setzen von Breakpoints verwenden können.

Info icon

Der Einfachheit halber haben wir keine JDK-Version angegeben. Stattdessen haben wir das Projekt mit der von IntelliJ IDEA vorgeschlagenen Version initialisiert. Für beste Ergebnisse empfehlen wir jedoch, Quellen zu verwenden, die der Version entsprechen, die zum Ausführen deines Programms verwendet wird.

Es gibt zahlreiche Stellen, die zum Setzen eines Breakpoints geeignet sind. Ich habe mich entschieden, einen Methoden-Breakpoint in java.awt.event.KeyListener::keyPressed zu setzen. Dies löst den Nebeneffekt jedes Mal aus, wenn wir eine Taste drücken:

Das Breakpoints-Dialogfeld zeigt einen Logging-Haltepunk für java.awt.event.KeyListener::keyPressed an. Das Breakpoints-Dialogfeld zeigt einen Logging-Haltepunk für java.awt.event.KeyListener::keyPressed an.
Info icon

Das Setzen eines Breakpoints mit einem Ausdruck in heißem Code kann die Zielanwendung erheblich verlangsamen.

Kehren wir zu Space Invaders zurück und schauen, ob unser selbstgemachtes IDDQD funktioniert. Es funktioniert!

Space Invaders in IntelliJ IDEA spielen - jedes Mal, wenn das Raumschiff getroffen wird, füllt sich seine Gesundheitsleiste automatisch wieder auf

Fazit

In diesem Artikel haben wir den Debugger verwendet, um herauszufinden, wie eine Anwendung unter der Haube funktioniert. Dann konnten wir durch ihren Speicher navigieren und ihre Funktionalität ändern, alles ohne die Quellen der Anwendung anzufassen! Ich hoffe, mein Vergleich des Debuggers mit IDDQD kam nicht zu kühn rüber, und dass du einige Techniken gelernt hast, die dir einen Vorteil bei deinen Debugging-Herausforderungen geben werden.

Ich möchte meine Anerkennung an Eugene Nizienko für das Erstellen des Space Invaders Plugins aussprechen und an Egor Ushakov dafür, dass er eine ständige Quelle der Inspiration beim Debugging und Programmieren ist. Computer machen doppelt so viel Spaß mit Menschen wie ihnen.

Wenn du Debugging-Herausforderungen im Sinn hast, die ich in kommenden Beiträgen angehen soll, lass es mich wissen!

Viel Erfolg beim Hacken!

all posts ->