WinDbg – Zeitleisten

WinDbg-Logo mit einer Lupe, die Bits inspiziert.

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.

Zeitleiste im Debugger, die Ausnahmen, Speicherzugriffe, Haltepunkte und Funktionsaufrufe anzeigt.

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.

Zeitleiste im Debugger, die Ausnahmen mit Informationen zu einem bestimmten Ausnahmecode anzeigt.

Über das optionale Feld für den Ausnahmecode können Sie weiter nach einem bestimmten Ausnahmecode filtern.

Dialogfeld der Ausnahme für den Zeitleisten-Debugger mit einem auf Ausnahme festgelegten Zeitleistentyp und einem auf 0xC0000004 festgelegten Ausnahmecode.

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.

Die Zeitleiste im Debugger zeigt etwa 30 Haltepunktindikatoren an.

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*.

Hinzufügen einer Zeitleiste im Debugger mit eingegebenem Funktionsaufrufnamen.

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.

Zeitleiste im Debugger, die Funktionsaufrufe und das Registerfenster anzeigt.

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.

Hinzufügen eines Dialogs für den Speicherzugriff auf die Zeitleiste mit ausgewählter Schaltfläche „Schreiben“.

Wenn Sie den Mauszeiger über ein Speicherzugriffselement bewegen, werden der Wert und der Anweisungszeiger angezeigt.

Zeitleiste im Debugger zur Anzeige von Speicherzugriffsereignissen.

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.

Die Zeitleiste im Debugger zeigt den oberen Bereich, der zur Auswahl des aktiven Ansichtsfensters verwendet wird.

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.

Zeitleiste im Debugger mit Anzeige der Demolab-Ausnahme und des Registerfensters.

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.

Debugger mit Anzeige von Registern, Stack und Speicherfenstern.

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.

Hinzufügen eines Dialogfelds für den Speicherzugriff in der Zeitleiste mit ausgewählter Schaltfläche „Schreiben“ und einem Startwert von 003cf7c8.

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 den Speicherzugriff und Fenster für lokale Werte mit unterschiedlichen Quell- und Zielwerten.

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.

Zeitleiste im Debugger mit Anzeige der Speicherzugriffszeitleiste und der Stapelfenster.

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.

Fügen Sie ein neues Dialogfeld „Timeline“ hinzu, in dem die Zeitleiste für Funktionsaufrufe mit einer Zeichenfolge zur Funktionssuche von DisplayGreeting!GetCppConGreeting angezeigt wird.

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.

Zeitleiste im Debugger mit Speicherzugriffszeitleiste und lokalem Fenster mit Meldung und Puffer, die verschiedene Zeichenfolgewerte enthalten.

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?“.

Zeitleiste im Debugger mit Anzeige der Speicherzugriffszeitleiste und lokaler Fenster.

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.

Neuer Lesezeichendialog mit Beispielnamen für den ersten API-Aufruf in der App Display Greeting.

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.

Die Zeitleiste zeigt drei Lesezeichen an, wobei der Mauszeiger über ein Lesezeichen bewegt wird und den Namen des Lesezeichens anzeigt.

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.

Ein Popup-Menü für Lesezeichen mit der rechten Maustaste, in dem die Optionen zum Positionieren, Bearbeiten und Entfernen angezeigt werden.

Hinweis

In Version 1.2402.24001.0 des Debuggers ist die Lesezeichenfunktion nicht verfügbar.

Weitere Informationen

WinDbg-Features

Zeitreise-Debugging-Walkthrough