Wie funktioniert ein Debugger?

170

Ich frage mich immer wieder, wie ein Debugger funktioniert. Insbesondere derjenige, der an eine bereits ausgeführte ausführbare Datei angehängt werden kann. Ich verstehe, dass der Compiler Code in die Maschinensprache übersetzt, aber woher weiß der Debugger dann, woran er angehängt wird?

ya23
quelle
5
Elis
Oktalist
@Oktalist Dieser Artikel ist interessant, befasst sich jedoch nur mit der Abstraktion auf API-Ebene für das Debuggen unter Linux. Ich denke, OP möchte mehr über unter der Haube wissen.
smwikipedia

Antworten:

96

Die Details zur Funktionsweise eines Debuggers hängen davon ab, was Sie debuggen und welches Betriebssystem Sie verwenden. Für das native Debuggen unter Windows finden Sie einige Details zu MSDN: Win32 Debugging API .

Der Benutzer teilt dem Debugger anhand seines Namens oder seiner Prozess-ID mit, an welchen Prozess eine Verbindung hergestellt werden soll. Wenn es sich um einen Namen handelt, sucht der Debugger die Prozess-ID und initiiert die Debug-Sitzung über einen Systemaufruf. Unter Windows wäre dies DebugActiveProcess .

Nach dem Anhängen tritt der Debugger in eine Ereignisschleife ein, ähnlich wie bei jeder Benutzeroberfläche. Anstelle von Ereignissen, die vom Fenstersystem stammen, generiert das Betriebssystem Ereignisse basierend auf den Vorgängen im zu debuggenden Prozess - beispielsweise einer auftretenden Ausnahme. Siehe WaitForDebugEvent .

Der Debugger kann den virtuellen Speicher des Zielprozesses lesen und schreiben und sogar seine Registerwerte über vom Betriebssystem bereitgestellte APIs anpassen. Siehe die Liste der Debugging-Funktionen für Windows.

Der Debugger kann Informationen aus Symboldateien verwenden, um Adressen in Variablennamen und Speicherorte im Quellcode zu übersetzen. Die Informationen zur Symboldatei sind separate APIs und kein zentraler Bestandteil des Betriebssystems. Unter Windows erfolgt dies über das Debug Interface Access SDK .

Wenn Sie eine verwaltete Umgebung (.NET, Java usw.) debuggen, sieht der Prozess normalerweise ähnlich aus, die Details sind jedoch unterschiedlich, da die Umgebung der virtuellen Maschine die Debug-API anstelle des zugrunde liegenden Betriebssystems bereitstellt.

Rob Walker
quelle
5
Diese Frage mag dumm klingen, aber wie verfolgt das Betriebssystem, wenn eine bestimmte Adresse im Programm erreicht wird? Beispielsweise wird an der Adresse 0x7710cafe ein Haltepunkt gesetzt. Wenn sich der Befehlszeiger ändert, muss das Betriebssystem (oder möglicherweise die CPU) den Befehlszeiger mit allen Haltepunktadressen vergleichen, oder irre ich mich? Wie funktioniert das ..?
Anzeigename
3
@StefanFalk Ich habe eine Antwort geschrieben , die einige Details der unteren Ebene (auf x86) behandelt.
Jonathon Reinhart
Können Sie erklären, wie genau die Variablennamen Adressen zugeordnet sind? Verwendet die Anwendung bei jeder Ausführung dieselben Speicheradressen für Variablen? Ich war immer davon ausgegangen, dass es nur aus dem verfügbaren Speicher zugeordnet wurde, hatte aber nie wirklich darüber nachgedacht, ob die Bytes direkt derselben Stelle im Speicher der App zugeordnet werden würden. Es scheint, dass dies ein großes Sicherheitsproblem wäre.
James Joshua Street
@JamesJoshuaStreet Ich würde mir vorstellen, dass dies ein Detail ist, das für den Debugger spezifisch ist.
Moonman239
Diese Antwort enthüllt etwas. Aber ich denke, op interessiert sich mehr für einige Details auf niedriger Ebene als für einige API-Abstraktionen.
smwikipedia
63

So wie ich es verstehe:

Bei Software-Haltepunkten auf x86 ersetzt der Debugger das erste Byte der Anweisung durch CC( int3). Dies geschieht unter WriteProcessMemoryWindows. Wenn die CPU zu dieser Anweisung gelangt und die ausführt int3, generiert die CPU eine Debug-Ausnahme. Das Betriebssystem empfängt diesen Interrupt, erkennt, dass der Prozess debuggt wird, und benachrichtigt den Debugger-Prozess, dass der Haltepunkt erreicht wurde.

Nachdem der Haltepunkt erreicht und der Prozess gestoppt wurde, sucht der Debugger in seiner Liste der Haltepunkte und ersetzt den CCdurch das ursprünglich vorhandene Byte. Die Debugger - Sets TF, der Trap - Flag in EFLAGS(durch das Modifizieren CONTEXT) und setzt den Prozess fort. Das Trap-Flag bewirkt, dass die CPU INT 1beim nächsten Befehl automatisch eine Einzelschritt-Ausnahme ( ) generiert .

Wenn der zu debuggende Prozess das nächste Mal stoppt, ersetzt der Debugger erneut das erste Byte der Haltepunktanweisung durch CCund der Prozess wird fortgesetzt.

Ich bin nicht sicher, ob dies genau so von allen Debuggern implementiert wird, aber ich habe ein Win32-Programm geschrieben, das es schafft, sich mithilfe dieses Mechanismus selbst zu debuggen. Völlig nutzlos, aber lehrreich.

Jonathon Reinhart
quelle
25

Unter Linux beginnt das Debuggen eines Prozesses mit dem Systemaufruf ptrace (2) . Dieser Artikel enthält ein großartiges Tutorial ptracezur Implementierung einiger einfacher Debugging-Konstrukte.

Adam Rosenfield
quelle
1
Sagt (2)uns das etwas mehr (oder weniger) als "ptrace ist ein Systemaufruf"?
Lazer
5
@eSKay, nein nicht wirklich. Dies (2)ist die manuelle Abschnittsnummer. Eine Beschreibung der manuellen Abschnitte finden Sie unter en.wikipedia.org/wiki/Man_page#Manual_sections .
Adam Rosenfield
2
@AdamRosenfield Mit Ausnahme der Tatsache, dass Abschnitt 2 speziell "Systemaufrufe" ist. Indirekt sagt es uns also, dass ptracees sich um einen Systemaufruf handelt.
Jonathon Reinhart
1
In der Praxis (2)sagt uns das, dass wir man 2 ptracedie richtige Manpage eingeben und erhalten können - hier nicht wichtig, da es keine andere gibt ptrace, die eindeutig ist, als zum Vergleich man printfmit man 3 printfLinux.
schlank
9

Wenn Sie ein Windows-Betriebssystem verwenden, ist "Debuggen von Anwendungen für Microsoft .NET und Microsoft Windows" von John Robbins eine hervorragende Ressource dafür:

(oder sogar die ältere Ausgabe: "Debugging Applications" )

Das Buch enthält ein Kapitel über die Funktionsweise eines Debuggers, das Code für einige einfache (aber funktionierende) Debugger enthält.

Da ich mit den Details des Unix / Linux-Debuggens nicht vertraut bin, gilt dieses Zeug möglicherweise überhaupt nicht für andere Betriebssysteme. Aber ich würde vermuten, dass als Einführung in ein sehr komplexes Thema die Konzepte - wenn nicht die Details und APIs - auf die meisten Betriebssysteme "portiert" werden sollten.

Michael Burr
quelle
3

Eine weitere wertvolle Quelle zum Verständnis des Debuggens ist das Intel CPU-Handbuch (Intel® 64 und IA-32 Architectures Software Developer's Manual). In Band 3A, Kapitel 16, wurde die Hardwareunterstützung des Debuggens eingeführt, z. B. spezielle Ausnahmen und Hardware-Debugging-Register. Folgendes stammt aus diesem Kapitel:

T (Trap) -Flag, TSS - Erzeugt eine Debug-Ausnahme (#DB), wenn versucht wird, zu einer Aufgabe zu wechseln, bei der das T-Flag in seinem TSS gesetzt ist.

Ich bin nicht sicher, ob Windows oder Linux dieses Flag verwenden oder nicht, aber es ist sehr interessant, dieses Kapitel zu lesen.

Hoffe das hilft jemandem.

Jiang
quelle
2

Ich denke, hier sind zwei Hauptfragen zu beantworten:

1. Woher weiß der Debugger, dass eine Ausnahme aufgetreten ist?

Wenn eine Ausnahme in einem Prozess auftritt, der debuggt wird, wird der Debugger vom Betriebssystem benachrichtigt, bevor Benutzerausnahmehandler, die im Zielprozess definiert sind, die Möglichkeit erhalten, auf die Ausnahme zu reagieren. Wenn der Debugger diese Ausnahmebenachrichtigung (erste Chance) nicht behandelt, wird die Ausnahmeverteilungssequenz weiter fortgesetzt, und der Zielthread erhält dann die Möglichkeit, die Ausnahme zu behandeln, wenn er dies möchte. Wenn die SEH-Ausnahme nicht vom Zielprozess behandelt wird, wird dem Debugger ein weiteres Debug-Ereignis gesendet, das als Benachrichtigung der zweiten Chance bezeichnet wird, um ihn darüber zu informieren, dass im Zielprozess eine nicht behandelte Ausnahme aufgetreten ist. Quelle

Geben Sie hier die Bildbeschreibung ein


2. Woher weiß der Debugger, wie er an einem Haltepunkt anhalten kann?

Die vereinfachte Antwort lautet: Wenn Sie einen Haltepunkt in das Programm einfügen, ersetzt der Debugger Ihren Code an diesem Punkt durch einen int3-Befehl, der ein Software-Interrupt ist . Infolgedessen wird das Programm angehalten und der Debugger aufgerufen.

InTheNameOfScience
quelle
1

Ich verstehe, dass beim Kompilieren einer Anwendung oder DLL-Datei alles, was kompiliert wird, Symbole enthält, die die Funktionen und Variablen darstellen.

Wenn Sie einen Debug-Build haben, sind diese Symbole weitaus detaillierter als bei einem Release-Build, sodass der Debugger Ihnen mehr Informationen geben kann. Wenn Sie den Debugger an einen Prozess anhängen, wird überprüft, auf welche Funktionen gerade zugegriffen wird, und alle verfügbaren Debugging-Symbole werden von hier aus aufgelöst (da er weiß, wie die Interna der kompilierten Datei aussehen, kann er feststellen, was sich möglicherweise im Speicher befindet , mit Inhalten von Ints, Floats, Strings usw.). Wie das erste Poster sagte, hängen diese Informationen und die Funktionsweise dieser Symbole stark von der Umgebung und der Sprache ab.

DavidG
quelle
2
Hier geht es nur um Symbole. Debugging ist weit mehr als nur Symbole.
Jonathon Reinhart