WinDbg – Zeitleisten
Time Travel Debugging (TTD) ermöglicht die Aufzeichnung von Ablaufverfolgungen, d. h. von Aufzeichnungen der Ausführung eines Programms. Zeitleisten sind eine visuelle Darstellung der Ereignisse, die während der Ausführung stattfinden. Bei diesen Ereignissen kann es sich um Haltepunkte, Lese- und Schreibvorgänge im Speicher, Funktionsaufrufe und -rückgaben sowie Ausnahmen handeln.
Verwenden Sie das Zeitleisten-Fenster, um wichtige Ereignisse schnell zu sehen, ihre relative Position zu verstehen und einfach zu ihrer Position in Ihrer TTD-Ablaufverfolgungsdatei zu springen. Verwenden Sie mehrere Zeitleisten, um Ereignisse in der Zeitreisen-Ablaufverfolgung visuell zu erkunden und die Korrelation von Ereignissen zu entdecken.
Das Zeitleisten-Fenster wird beim Öffnen einer TTD-Ablaufverfolgungsdatei angezeigt und zeigt wichtige Ereignisse an, ohne dass Sie manuell Datenmodellabfragen erstellen müssen. Gleichzeitig sind alle Zeitreiseobjekte verfügbar, um komplexere Datenabfragen zu ermöglichen.
Weitere Informationen zum Erstellen und Arbeiten mit Zeitreise-Ablaufverfolgungsdateien finden Sie unter Zeitreise-Debugging – Überblick.
Arten von Zeitleisten
Im Zeitleisten-Fenster können die folgenden Ereignisse angezeigt werden:
- Ausnahmen (Sie können weiter nach einem bestimmten Ausnahmecode filtern)
- Breakpoints
- Funktionsaufrufe (Suche in Form von module!function)
- Speicherzugriffe (Lesen/Schreiben/Ausführen zwischen zwei Speicheradressen)
Bewegen Sie den Mauszeiger über die einzelnen Ereignisse, um weitere Informationen über Tooltipps zu erhalten. Wenn Sie auf ein Ereignis klicken, wird die Abfrage für dieses Ereignis ausgeführt und es werden weitere Informationen angezeigt. Ein Doppelklick auf ein Ereignis führt zu dieser Stelle in der TTD-Ablaufverfolgungsdatei.
Ausnahmen
Wenn Sie eine Ablaufverfolgungsdatei laden und die Zeitleiste aktiv ist, werden alle Ausnahmen in der Aufzeichnung automatisch angezeigt.
Wenn Sie den Mauszeiger über einen Haltepunkt bewegen, werden Informationen wie der Ausnahmetyp und der Ausnahmecode angezeigt.
Über das optionale Feld für den Ausnahmecode können Sie weiter nach einem bestimmten Ausnahmecode filtern.
Sie können auch eine neue Zeitleiste für eine bestimmte Ausnahmeart hinzufügen.
Breakpoints
Nachdem Sie einen Haltepunkt hinzugefügt haben, können Sie die Positionen, an denen dieser Haltepunkt erreicht wird, auf einer Zeitleiste anzeigen. Dies kann zum Beispiel mit dem Befehl bp Set Breakpoint durchgeführt werden. Wenn Sie den Mauszeiger über einen Haltepunkt bewegen, werden die Adresse und der Anweisungszeiger angezeigt, die mit dem Haltepunkt verbunden sind.
Wenn der Haltepunkt gelöscht wird, wird die zugehörige Haltepunkt-Zeitleiste automatisch entfernt.
Funktionsaufrufe
Sie können die Positionen der Funktionsaufrufe in der Zeitleiste anzeigen. Geben Sie dazu die Suche in Form von module!function
an, zum Beispiel TimelineTestCode!multiplyTwo
. Sie können auch Platzhalter angeben, zum Beispiel TimelineTestCode!m*
.
Wenn Sie den Mauszeiger über einen Funktionsaufruf bewegen, werden der Funktionsname, die Eingabeparameter, ihre Werte und der Rückgabewert angezeigt. Dieses Beispiel zeigt Puffer und Größe, da dies die Parameter für DisplayGreeting!GetCppConGreeting sind.
Speicherzugriff
Verwenden Sie die Zeitleiste für den Speicherzugriff, um anzuzeigen, wann ein bestimmter Bereich des Speichers gelesen oder beschrieben wurde oder wo die Codeausführung stattgefunden hat. Eine Start- und Stoppadresse wird verwendet, um einen Bereich zwischen zwei Speicheradressen zu definieren.
Wenn Sie den Mauszeiger über ein Speicherzugriffselement bewegen, werden der Wert und der Anweisungszeiger angezeigt.
Arbeiten mit Zeitleisten
Eine vertikale graue Linie folgt dem Cursor, wenn er über der Zeitleiste bewegt wird. Die vertikale blaue Linie zeigt die aktuelle Position in der Ablaufverfolgung an.
Klicken Sie auf die Lupensymbole, um die Zeitleiste zu vergrößern oder zu verkleinern.
Verwenden Sie das Rechteck im oberen Bereich der Zeitleiste, um die Ansicht der Zeitleiste zu verschieben. Ziehen Sie die äußeren Begrenzungslinien des Rechtecks, um die Größe der aktuellen Zeitleistenansicht zu ändern.
Maus-Bewegungen
Vergrößern und verkleinern Sie die Ansicht mit Strg + Scrollrad.
Schwenken Sie mit Umschalttaste + Scrollrad von Seite zu Seite.
Techniken zur Fehlersuche in der Zeitleiste
Zur Veranschaulichung der Debugging-Techniken in der Zeitleiste wird der Zeitreise-Debugging-Walkthrough hier wiederverwendet. In dieser Demonstration wird davon ausgegangen, dass Sie die ersten beiden Schritte zur Erstellung des Beispielcodes abgeschlossen und die TTD-Aufzeichnung mithilfe der ersten beiden dort beschriebenen Schritte erstellt haben.
Abschnitt 1: Erstellen Sie den Beispielcode
Abschnitt 2: Zeichnen Sie eine Ablaufverfolgung des Beispiels „DisplayGreeting“ auf
In diesem Szenario besteht der erste Schritt darin, die Ausnahme in der Zeitreise-Ablaufverfolgung zu finden. Dies kann durch einen Doppelklick auf die einzige Ausnahme in der Zeitleiste geschehen.
Im Befehlsfenster sehen wir, dass der folgende Befehl ausgegeben wurde, als wir auf die Ausnahme geklickt haben.
(2dcc.6600): Break instruction exception - code 80000003 (first/second chance not available)
Time Travel Position: CC:0
@$curprocess.TTD.Events.Where(t => t.Type == "Exception")[0x0].Position.SeekTo()
Wählen Sie View>>Registers aus, um die Register an diesem Punkt in der Zeitleiste anzuzeigen und unsere Untersuchung zu beginnen.
Beachten Sie in der Befehlsausgabe, dass der Stack (esp) und der Basiszeiger (ebp) auf zwei sehr unterschiedliche Adressen zeigen. Dies könnte darauf hinweisen, dass der Stack beschädigt ist – möglicherweise wurde eine Funktion zurückgegeben und hat dann den Stack beschädigt. Um dies zu überprüfen, müssen wir zurückgehen, bevor der CPU-Status beschädigt wurde, und prüfen, ob wir feststellen können, wann die Stack-Beschädigung aufgetreten ist.
Dabei werden wir die Werte der lokalen Variablen und des Stacks untersuchen.
Wählen Sie Ansicht>>Locals aus, um die lokalen Werte anzuzeigen.
Wählen Sie View>>Stack aus, um den Codeausführungsstapel anzuzeigen.
An der Stelle, an der in der Ablaufverfolgung ein Fehler auftritt, kommt es häufig vor, dass der Fehler einige Schritte hinter der wahren Ursache im Fehlerbehandlungscode liegt. Mit Zeitreisen können wir eine Anweisung nach der anderen zurückgehen, um die wahre Ursache zu finden.
Verwenden Sie im Menüband Home den Befehl Step Into Back, um drei Anweisungen zurückzugehen. Untersuchen Sie dabei weiterhin die Fenster Stack, Locals und Register.
Im Befehlsfenster werden die Zeitreiseposition und die Register angezeigt, wenn Sie drei Anweisungen zurückgehen.
0:000> t-
Time Travel Position: CB:41
eax=00000000 ebx=00564000 ecx=c0d21d62 edx=7a1e4a6c esi=00061299 edi=00061299
eip=00540020 esp=003cf7d0 ebp=00520055 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
00540020 ?? ???
0:000> t-
Time Travel Position: CB:40
eax=00000000 ebx=00564000 ecx=c0d21d62 edx=7a1e4a6c esi=00061299 edi=00061299
eip=00061767 esp=003cf7cc ebp=00520055 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
DisplayGreeting!main+0x57:
00061767 c3 ret
0:000> t-
Time Travel Position: CB:3A
eax=0000004c ebx=00564000 ecx=c0d21d62 edx=7a1e4a6c esi=00061299 edi=00061299
eip=0006175f esp=003cf718 ebp=003cf7c8 iopl=0 nv up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206
DisplayGreeting!main+0x4f:
0006175f 33c0 xor eax,eax
An diesem Punkt in der Ablaufverfolgung haben unser Stack und unser Basiszeiger Werte, die mehr Sinn ergeben, so dass es scheint, dass wir uns dem Punkt im Code nähern, an dem die Beschädigung aufgetreten ist.
esp=003cf718 ebp=003cf7c8
Interessant ist auch, dass das Locals-Fenster Werte aus unserer Zielanwendung enthält und das Quellcode-Fenster die Codezeile hervorhebt, die in unserem Quellcode an diesem Punkt der Aufzeichnung zur Ausführung bereit ist.
Zur weiteren Untersuchung können wir ein Speicherfenster öffnen, um den Inhalt in der Nähe der Speicheradresse des Stapelzeigers (esp) zu sehen. In diesem Beispiel hat es einen Wert von 003cf7c8. Wählen Sie Memory>>Text>>ASCII aus, um den an dieser Adresse gespeicherten ASCII-Text anzuzeigen.
Zeitleiste für den Speicherzugriff
Nachdem ein interessanter Speicherplatz identifiziert wurde, fügen Sie eine Speicherzugriffszeitleiste mit diesem Wert hinzu. Klicken Sie auf + Add Timeline (Zeitleiste hinzufügen), und geben Sie die Startadresse ein. Wir betrachten 4 Bytes, und wenn wir das zur Startadresse 003cf7c8 hinzufügen, erhalten wir 003cf7cb. Standardmäßig werden alle Schreibvorgänge im Speicher untersucht, aber Sie können auch nur die Schreibvorgänge oder die Codeausführung an dieser Adresse untersuchen.
Wir können nun die Zeitleiste in umgekehrter Richtung durchlaufen, um zu untersuchen, zu welchem Zeitpunkt in dieser Zeitreise-Ablaufverfolgung dieser Speicherplatz geschrieben wurde, um zu sehen, was wir finden können. Wenn wir auf diese Position in der Zeitleiste klicken, sehen wir, dass sich die locals-Werte von den Werten für die zu kopierende Zeichenfolge unterscheiden. Der Zielwert scheint unvollständig zu sein, als ob die Länge unserer Zeichenfolge nicht korrekt wäre.
Zeitleiste für Haltepunkte
Die Verwendung von Haltepunkten ist eine gängige Methode, um die Codeausführung bei einem bestimmten Ereignis von Interesse anzuhalten. Mit TTD können Sie einen Haltepunkt setzen und in der Zeit zurückreisen, bis dieser Haltepunkt nach der Aufzeichnung der Ablaufverfolgung erreicht wird. Die Möglichkeit, den Prozessstatus zu untersuchen, nachdem ein Problem aufgetreten ist, um die beste Stelle für einen Haltepunkt zu bestimmen, ermöglicht zusätzliche Debugging-Workflows, die es nur bei TTD gibt.
Um eine alternative Technik zum Debuggen der Zeitleiste zu erproben, klicken Sie auf die Ausnahme in der Zeitleiste und gehen Sie noch einmal drei Schritte zurück, indem Sie den Befehl Step Into Back auf der Multifunktionsleiste Home verwenden.
In diesem sehr kleinen Beispiel wäre es ziemlich leicht, einfach in den Code zu schauen, aber wenn es Hunderte von Codezeilen und Dutzende von Unterprogrammen gibt, können die hier beschriebenen Techniken verwendet werden, um die Zeit zu verkürzen, die notwendig ist, um das Problem zu finden.
Wie bereits erwähnt, verweist der Basiszeiger (esp) nicht auf eine Anweisung, sondern auf unseren Nachrichtentext.
Verwenden Sie den Befehl ba, um einen Haltepunkt beim Speicherzugriff zu setzen. Wir setzen einen w – write Haltepunkt, um zu sehen, wann in diesen Speicherbereich geschrieben wird.
0:000> ba w4 003cf7c8
Obwohl wir einen einfachen Speicherzugriffs-Haltepunkt verwenden, können Haltepunkte auch als komplexere bedingte Anweisungen konstruiert werden. Weitere Informationen finden Sie unter bp, bu, bm (Set Breakpoint).
Wählen Sie im Menü Home die Option Go Back aus, um in der Zeit zurückzureisen, bis der Haltepunkt erreicht ist.
An diesem Punkt können wir den Programmstapel untersuchen, um zu sehen, welcher Code aktiv ist.
Da es sehr unwahrscheinlich ist, dass die von Microsoft bereitgestellte Funktion wscpy_s() einen solchen Code-Fehler hat, schauen wir weiter im Stack nach. Der Stack zeigt, dass Greeting!main Greeting!GetCppConGreeting aufruft. In unserem sehr kleinen Codebeispiel könnten wir den Code an dieser Stelle einfach öffnen und den Fehler wahrscheinlich ziemlich leicht finden. Zur Veranschaulichung der Techniken, die bei größeren, komplexeren Programmen verwendet werden können, fügen wir eine Zeitleiste für Funktionsaufrufe hinzu.
Zeitleiste für Funktionsaufrufe
Klicken Sie auf + Add Timeline und geben Sie DisplayGreeting!GetCppConGreeting
als Suchbegriff für die Funktion ein.
Die Kontrollkästchen Start- und Endposition zeigen an, dass der Beginn und das Ende eines Funktionsaufrufs in der Ablaufverfolgung angezeigt werden.
Mit dem Befehl dx können wir das Objekt des Funktionsaufrufs anzeigen, um die zugehörigen Felder TimeStart und TimeEnd zu sehen, die dem Startort und dem Endort des Funktionsaufrufs entsprechen.
dx @$cursession.TTD.Calls("DisplayGreeting!GetCppConGreeting")[0x0]
EventType : 0x0
ThreadId : 0x6600
UniqueThreadId : 0x2
TimeStart : 6D:BD [Time Travel]
SystemTimeStart : Thursday, October 31, 2019 23:36:05
TimeEnd : 6D:742 [Time Travel]
SystemTimeEnd : Thursday, October 31, 2019 23:36:05
Function : DisplayGreeting!GetCppConGreeting
FunctionAddress : 0x615a0
ReturnAddress : 0x61746
Parameters
Entweder das Feld Start oder Ende oder beide Felder müssen markiert sein.
Da unser Code weder rekursiv noch eintrittsinvariant ist, lässt sich der Zeitpunkt des Aufrufs der Methode GetCppConGreeting recht einfach in der Zeitleiste lokalisieren. Der Aufruf von GetCppConGreeting erfolgt zur gleichen Zeit wie unser Haltepunkt und das von uns definierte Speicherzugriffsereignis. Es sieht also so aus, als hätten wir einen Bereich des Codes gefunden, den wir uns genau ansehen sollten, um die Ursache für den Absturz unserer Anwendung zu finden.
Untersuchung der Codeausführung durch Anzeige mehrerer Zeitleisten
Obwohl unser Codebeispiel klein ist, ermöglicht die Technik der Verwendung mehrerer Zeitleisten die visuelle Erkundung einer Zeitreise-Ablaufverfolgung. Sie können die Ablaufverfolgungsdatei durchsuchen, um Fragen zu stellen, z. B. „Wann wird auf einen Speicherbereich zugegriffen, bevor ein Haltepunkt erreicht wird?“.
Die Möglichkeit, zusätzliche Korrelationen zu sehen und Dinge zu finden, die Sie vielleicht nicht erwartet haben, unterscheidet das Zeitleisten-Tool von der Interaktion mit der Zeitreise-Ablaufverfolgung über Kommandozeilenbefehle.
Zeitleisten-Lesezeichen
Setzen Sie ein Lesezeichen für wichtige Zeitreisepositionen in WinDbg, anstatt die Position manuell in den Notizblock zu kopieren. Lesezeichen erleichtern es, auf einen Blick verschiedene Positionen in der Spur im Verhältnis zu anderen Ereignissen zu sehen und sie mit Anmerkungen zu versehen.
Sie können einen beschreibenden Namen für Lesezeichen vergeben.
Greifen Sie auf Lesezeichen über das Fenster Zeitleiste zu, das unter View > Timeline verfügbar ist. Wenn Sie den Mauszeiger über ein Lesezeichen bewegen, wird der Name des Lesezeichens angezeigt.
Sie können mit der rechten Maustaste auf das Lesezeichen klicken, um zu dieser Position zu gelangen und so das Lesezeichen umzubenennen oder zu löschen.
Hinweis
In Version 1.2402.24001.0 des Debuggers ist die Lesezeichenfunktion nicht verfügbar.