In meinem Beitrag zur MSDN Blogparade zum Thema Entwicklungstools zählte ich WinDbg als einen meiner Favoriten auf. In einem Nebensatz erwähnte ich, dass ich bei Interesse gerne ein kleines Tutorial zu diesem Werkzeug schreiben könnte. Die Anzahl der Rückmeldungen auf diesen Beitrag führten zu zwei Schlussfolgerungen:

  1. Mein Blog lesen mehr Leute als ich dachte, und nicht wie vorher vermutet nur meine Frau und meine Mutter.
  2. Das Interesse an einem Tutorial ist definitiv vorhanden. Also werde ich mein Versprechen einhalten und ein kleines Tutorial schreiben.

Sicherlich stellt sich nun die Frage, warum überhaupt so lange gedauert hat, das Tutorial zu verfassen!

Nun, das liegt zum einen daran, dass ich zur Zeit nicht in Köln, sondern in Bonn arbeite. Somit verbringe ich viel weniger Zeit im Zug und habe somit auch viel weniger Zeit zum bloggen. Zum anderen liegt es daran, dass ich die knappe Zeit im Zug nicht zum schreiben, sondern zum Ansehen von Rob Conneries StoreFront Webcasts genutzt habe. An dieser Stelle möchte ich mich als absoluter Fan der Serie bekennen. Wer sich die Webcasts noch nicht angesehen hat, sollte dies unbedingt nachholen! Dann kam auch noch der Urlaub hinzu, so dass dieser Eintrag einfach ein wenig warten musste.

Jetzt aber zurück zum eigentlichen Thema!

WinDbg - Was ist das überhaupt?

Wie in meinem vorherigen Blog Post geschrieben, ist WinDbg ein unmanaged (native) Debugger mit grafischer Benutzeroberfläche. Dank der SOS Erweiterung kann man ihn jedoch wunderbar dazu verwenden, auch managed Code zu debuggen. WinDbg benötigt eigentlich keinerlei Installation auf dem zu debuggenden System und kann somit wunderbar als Geheimwaffe auf einem Schweizer Taschenmesser USB Stick mitgeführt werden.

Wo bekommt man ihn her und was muss bei der Installation beachtet werden?

Herunterladen kann man WinDbg in Form eines MSI Paketes auf der Download Seite der Microsoft Debugging Tools for Windows. Die Installation an sich verläuft recht geradlinig (Next -> Next -> I Agree -> Next -> Finish ;-)). Sind die Debugging Tools installiert, kann der komplette Inhalt des Verzeichnisses wie bereits erwähnt auf einen Stick kopiert werden und wäre auch von dort aus lauffähig.

Bevor WinDbg nun wirklich genutzt werden kann, ist jedoch noch eine kleine Vorarbeit - nämlich die Definition des Symbol-Pfads - notwendig. Andernfalls meldet WinDbg während der Debugging Aktivitäten stets, dass er keine Symbole (PDB-Dateien) findet, was die Übersichtlichkeit ein wenig leiden lässt.

Zur Angabe des Symbol Pfads legt man nun zunächst ein Verzeichnis auf seiner Festplatte/Stick an, zum Beispiel c:\symbols. Anschließend startet man WinDbg und wählt im Menü File den Eintrag Symbol File Path ... aus. Im sich anschließend öffnenden Dialog gibt man über folgenden Befehl an, dass man die Symbole gerne von Microsoft herunter laden und auf der Festplatte im Verzeichnis c:\symbols  speichern würde:

SRV*c:\symbols*http://msdl.microsoft.com/download/symbols

 

Genug der Vorarbeit. Los gehts!

Jetzt, nachdem WinDbg korrekt installiert und konfiguriert ist, möchte ich anhand eines kleinen Beispiels die Funktionsweise zeigen. Source Code sowie die kompilierte Version gibt es übrigens bald auf meiner Hompage zum Download.

Die Applikation um die es sich handelt ist eine kleine Windows Anwendung, die nur aus einem Login Dialog besteht. Gibt der Anwender die korrekten Zugangsdaten ein, erhält er Bestätigungsmeldung:

windbg1  

Sind die Benutzerdaten falsch, kommt eine Fehlermeldung:

windbg2

Das ganze läuft seit einer ganzen Weile recht gut beim Kunden im produktiven Einsatz. Seit kurzem ist jedoch kein Login mehr möglich. Der Kunde meldet, dass trotz 100%ig richtiger Zugangsdaten stets die Meldung "Ungültige Benutzername / Passwort Kombination" Meldung kommt. Eine Log Datei wird leider nicht erstellt, so dass die Ursache des Fehlers derzeit vollkommen offen ist. Ein Blick auf den Quellocde zeigt folgende Zeilen innerhalb des Login-Formulars

private void LoginButton_Click(object sender, EventArgs e)
{
    UserService service = new UserService();
    try
    {
        service.ValidateUser(UserNameTextBox.Text, PasswordTextBox.Text);
        MessageBox.Show("Login erfolgreich");
    }
    catch
    {
        MessageBox.Show("Ungültige Benutzername / Passwort Kombination");
    }
}

Wie man sieht, wird hier Logik über Exceptions gesteuert. Nicht schön, aber vorerst leider nicht zu ändern. Da der Code im Falle irgendeiner Exception die Meldung "Ungültige Benutzername / Passwort Kombination" bringt, liegt die Vermutung nahe, dass irgendein Fehler innerhalb der Methode ValidateUser auftritt, der zu einer Exception führt. Die Frage ist nur: Welche Exception und warum tritt diese überhaupt auf?

Die Ursache des Problems wäre mit einer kleinen Quellcodeänderung schnell gefunden. Eine schnelle Lösung ist zwar genau das, was unser Kunde braucht, jedoch möchte er uns weder zum Debuggen in sein Netzwerk, noch auf Gut Glück neue Versionsstände mit erweiterten Log Nachrichten einspielen lassen. Allerdings willigt er ein, dass wir mit einem USB Stick bewaffnet an einen der betroffenen PCs dürfen. Voraussetzung jedoch ist, dass wir keine Software installieren.

Showtime

Die beschriebene Situation ist ein typisches Einsatzszenario für WinDbg.

Wir starten also WinDbg von unserem USB Stick und wählen das Menü File->Attach to a process. Anschließend wählen wir unsere fehlerhafte Applikation aus der Prozessliste aus und drücken OK. WinDbg sollte nun ungefähr so aussehen:

windbg3

Als nächstes geben wir folgende Kommandos in die Eingabezeile ein:

sxe clr

.loadby sos mscorwks

Falls nun keine Fehlermeldung kommt, haben wir alles richtig gemacht ;-)

sxe clr sagt dem Debugger, dass er bei jeder CLR Exception anhalten soll. Der Befehl ".loadby sos mscorwks" dient dazu, die SOS Extension zu laden. Diese DLL ermöglicht die Untersuchung von Managed Code innerhalb von WinDbg, der ja eigentlich ein Debugger für unmanaged Code ist. Für jede Version der CLR gibt es eine eigene SOS.DLL. Um nun die zum Framework der fehlerhaften Anwendung passende SOS.DLL zu laden, kann man entweder den vollständigen Pfad angeben, oder man lädt die Extension einfach aus dem Pfad, aus dem auch die mscorwks geladen wurden. Die Datei mscorwks gehört zum .NET Framework.

Derzeit befindet sich das Programm immer noch im Haltemodus. Über die Eingabe von g (für Go) bzw. drücken von F5 können wir die Ausführung fortführen.

Als nächstes Klicken wir in unserer fehlerhaften Applikation erneut auf den Button Login, um den Fehler zu provozieren. Ein Wechsel zu WinDbg zeigt, dass die Ausführung aufgrund der Exception angehalten wurde. Außerdem werden folgende Zeilen ausgegeben:

(1144.1380): CLR exception - code e0434f4d (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=0012ecf0 ebx=e0434f4d ecx=00000000 edx=00000028 esi=0012ed7c edi=0015b718
eip=7c812a6b esp=0012ecec ebp=0012ed40 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for C:\WINDOWS\system32\KERNEL32.dll -
KERNEL32!RaiseException+0x52:
7c812a6b 5e              pop     esi

Wir sehen also, dass eine CLR Exception aufgetreten ist. Leider sagt die aktuelle Ausgabe noch relativ wenig über die Ursache aus. Wie kommen wir also an die Details?

Dazu gibt es prinzipiell zwei Möglichkeiten.

Variante 1 ist, über den Befehl

!DumpStackObjects (oder kurz !dso) eine Liste aller Objekte, die aktuell auf dem Stack verwiesen werden, abzurufen.

Das Ergebnis sieht in meinem Beispiel wie folgt aus:

0:000> !dso
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for c:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\mscorwks.dll -
PDB symbol for mscorwks.dll not loaded
OS Thread Id: 0x1380 (0)
ESP/REG  Object   Name
0012ed5c 014c8974 System.Data.SqlServerCe.SqlCeException
0012eda8 014c8974 System.Data.SqlServerCe.SqlCeException
0012edec 014c8974 System.Data.SqlServerCe.SqlCeException
0012edf8 014c8974 System.Data.SqlServerCe.SqlCeException
0012ee20 014c04d8 System.Data.SqlServerCe.SqlCeConnection
0012ee24 014c04d8 System.Data.SqlServerCe.SqlCeConnection
0012ee50 014c8974 System.Data.SqlServerCe.SqlCeException
0012ee7c 014c04d8 System.Data.SqlServerCe.SqlCeConnection
0012ef00 014bf388 System.Windows.Forms.MouseEventArgs
0012ef04 014c0430 Codemurai.Tutorial.WinDbg.ExceptionHunting.UserService
0012ef08 014c04d8 System.Data.SqlServerCe.SqlCeConnection
0012ef14 014c04d8 System.Data.SqlServerCe.SqlCeConnection
0012ef18 014c04d8 System.Data.SqlServerCe.SqlCeConnection
0012ef2c 014b075c System.Object[]    (System.Object[])
0012ef30 014bf388 System.Windows.Forms.MouseEventArgs
0012ef34 014c0430 Codemurai.Tutorial.WinDbg.ExceptionHunting.UserService
0012ef50 014c048c System.Text.StringBuilder
0012ef5c 014c04a0 System.String    test
0012ef60 014a6e80 System.String    Data Source=CodemuraiDb2.sdf
0012ef64 014c04d8 System.Data.SqlServerCe.SqlCeConnection
0012ef68 014c04d8 System.Data.SqlServerCe.SqlCeConnection
0012ef6c 014a6b78 System.Configuration.ConnectionStringSettings
0012ef70 014a5f8c System.Configuration.ConnectionStringSettingsCollection
0012ef80 014c04d8 System.Data.SqlServerCe.SqlCeConnection
0012ef84 014a6b78 System.Configuration.ConnectionStringSettings
0012ef88 014a5f8c System.Configuration.ConnectionStringSettingsCollection
0012ef8c 014c045c System.String    wilhelm
0012ef98 014c0430 Codemurai.Tutorial.WinDbg.ExceptionHunting.UserService
0012efb0 014bf388 System.Windows.Forms.MouseEventArgs
0012efb4 013c8b28 System.EventHandler
0012efb8 013c6a04 System.Windows.Forms.Button
0012efc4 014c04a0 System.String    test
0012efc8 014c04a0 System.String    test
0012efcc 014c045c System.String    wilhelm
0012efd0 014c0430 Codemurai.Tutorial.WinDbg.ExceptionHunting.UserService
0012efd4 014c0430 Codemurai.Tutorial.WinDbg.ExceptionHunting.UserService
0012efd8 014c0430 Codemurai.Tutorial.WinDbg.ExceptionHunting.UserService

...

Wir sehen, dass ganz oben auf dem Stack eine SqlCeException liegt. Diese ist unter der Adresse 014c8974 auf dem Heap abgelegt. Details eines Objekts kann man sich über !DumpObj bzw. !do ansehen.

0:000> !do 014c8974
Name: System.Data.SqlServerCe.SqlCeException
MethodTable: 07d8255c
EEClass: 07d04f14
Size: 76(0x4c) bytes
(C:\WINDOWS\assembly\GAC_MSIL\System.Data.SqlServerCe\3.5.1.0__89845dcd8080cc91\System.Data.SqlServerCe.dll)
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
79330a00  40000b5        4        System.String  0 instance 00000000 _className
7932fe74  40000b6        8 ...ection.MethodBase  0 instance 00000000 _exceptionMethod
79330a00  40000b7        c        System.String  0 instance 00000000 _exceptionMethodString
79330a00  40000b8       10        System.String  0 instance 014c8e0c _message
7932a35c  40000b9       14 ...tions.IDictionary  0 instance 00000000 _data
79330b94  40000ba       18     System.Exception  0 instance 00000000 _innerException
79330a00  40000bb       1c        System.String  0 instance 00000000 _helpURL
7933061c  40000bc       20        System.Object  0 instance 00000000 _stackTrace
79330a00  40000bd       24        System.String  0 instance 00000000 _stackTraceString
79330a00  40000be       28        System.String  0 instance 00000000 _remoteStackTraceString
79332c4c  40000bf       34         System.Int32  1 instance        0 _remoteStackIndex
7933061c  40000c0       2c        System.Object  0 instance 00000000 _dynamicMethods
79332c4c  40000c1       38         System.Int32  1 instance -2146233087 _HResult
79330a00  40000c2       30        System.String  0 instance 00000000 _source
793332c8  40000c3       3c        System.IntPtr  1 instance        0 _xptrs
79332c4c  40000c4       40         System.Int32  1 instance -532459699 _xcode
07d82660  400032a       44 ...CeErrorCollection  0 instance 014c8784 errors

In den Informationen über unser Exception Objekt sehen wir nun, dass es ein Feld _message gibt, dessen Inhalt sich an der Adresse 014c8e0c befindet. An die Details des Felds _message kommen wir wieder über den Befehl !do.

0:000> !do 014c8e0c
Name: System.String
MethodTable: 79330a00
EEClass: 790ed64c
Size: 282(0x11a) bytes
(C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
String: The database file cannot be found. Check the path to the database. [ Data Source = CodemuraiDb2.sdf ]
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
79332c4c  4000096        4         System.Int32  1 instance      133 m_arrayLength
79332c4c  4000097        8         System.Int32  1 instance      101 m_stringLength
793316e0  4000098        c          System.Char  1 instance       54 m_firstChar
79330a00  4000099       10        System.String  0   shared   static Empty
    >> Domain:Value  00163700:013a1198 <<
79331630  400009a       14        System.Char[]  0   shared   static WhitespaceChars
    >> Domain:Value  00163700:013a18ec <<

Prima, diese Aussage hat doch gleich eine ganz andere Qualität. Benutzername/Passwort waren wirklich nicht falsch. Einzig die geschluckte Exception sorgte für den Eindruck. Statt dessen konnte die DB nicht gefunden werden. Ein kurzer Blick die App.Config zeigt folgenden Eintrag:

<connectionStrings>
    <add name="CodemuraiDb" connectionString="Data Source=CodemuraiDb2.sdf"
        providerName="Microsoft.SqlServerCe.Client.3.5" />

 

Die DB selbst heißt im Dateisystem jedoch: CodemuraiDb.sdf.

Einen kurzen Eintrag in der Datei Codemurai.Tutorial.WinDbg.ExceptionHunting.exe.config später läuft das Programm wieder wie gewünscht.

Geht das auch schneller?

Selbstverständlich. Sobald unser Code wegen einer Exception steht hätten wir statt !dso und mindestens Zwei mal !do auch einfach !PrintException, oder kurz !pe eingeben können.

0:000> !pe
Exception object: 014c8974
Exception type: System.Data.SqlServerCe.SqlCeException
Message: The database file cannot be found. Check the path to the database. [ Data Source = CodemuraiDb2.sdf ]
InnerException: <none>
StackTrace (generated):
<none>
StackTraceString: <none>
HResult: 80131501

Aber das kann ja jeder ;-) Außerdem haben wir über den anderen Weg direkt noch ein paar Debugging Tipps gelernt.

Zusammenfassung

In diesem Eintrag wurden folgende Befehle besprochen.

Befehl Bedeutung
sxe clr Bei jeder CLR Exception anhalten
.loadby sos mscorwks SOS Extension passend zur .NET Framework Version laden
g Ausführung fortführen
!DumpStackObjects / !dso Auflistung aller Objekte, die auf dem Stack verwiesen werden
!DumpObj /!do <Adresse>  Details zu einem Objekt ansehen
!PrintException / !pe  Details zur aktuellen Exception ansehen

Wie gehts weiter?

Ziel dieses kleinen Beispiels war es, den Einstieg in WinDbg zu erleichtern. Natürlich gibt es noch weitaus mehr, was mit WinDbg angestellt werden kann. So ist der Debugger sehr hilfreich, um Speicherlecks, oder (vermeintliche) Deadlocks zu finden. Auch die Option, einen zuvor durch den Kunden generierten MemoryDump zu analysieren ist sehr interessant.

Sollte also Interesse an einer Fortsetzung bestehen, reicht es einen kurzen Kommentar zu diesem Beitrag zu hinterlassen. Kommen genug Kommentare zusammen, schreibe ich gerne weitere Teile - dieses Mal auch mit weniger Wartezeit ;-)


Kick it on dotnet-kicks.de