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:
- Mein Blog lesen mehr Leute als ich dachte, und nicht wie vorher vermutet nur meine Frau und meine Mutter. 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:
Sind die Benutzerdaten falsch, kommt eine Fehlermeldung:
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:
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 nccs=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 loadedOS Thread Id: 0x1380 (0)ESP/REG Object Name0012ed5c
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.SqlCeException0012ee7c
014c04d8 System.Data.SqlServerCe.SqlCeConnection0012ef00
014bf388 System.Windows.Forms.MouseEventArgs0012ef04
014c0430 Codemurai.Tutorial.WinDbg.ExceptionHunting.UserService0012ef08
014c04d8 System.Data.SqlServerCe.SqlCeConnection0012ef14
014c04d8 System.Data.SqlServerCe.SqlCeConnection0012ef18
014c04d8 System.Data.SqlServerCe.SqlCeConnection0012ef2c
014b075c System.Object[] (System.Object[])0012ef30
014bf388 System.Windows.Forms.MouseEventArgs0012ef34
014c0430 Codemurai.Tutorial.WinDbg.ExceptionHunting.UserService0012ef50
014c048c System.Text.StringBuilder0012ef5c 014c04a0 System.String
test0012ef60 014a6e80 System.String Data Source=CodemuraiDb2.sdf0012ef64
014c04d8 System.Data.SqlServerCe.SqlCeConnection0012ef68
014c04d8 System.Data.SqlServerCe.SqlCeConnection0012ef6c
014a6b78 System.Configuration.ConnectionStringSettings0012ef70
014a5f8c System.Configuration.ConnectionStringSettingsCollection0012ef80
014c04d8 System.Data.SqlServerCe.SqlCeConnection0012ef84
014a6b78 System.Configuration.ConnectionStringSettings0012ef88
014a5f8c System.Configuration.ConnectionStringSettingsCollection0012ef8c
014c045c System.String wilhelm0012ef98
014c0430 Codemurai.Tutorial.WinDbg.ExceptionHunting.UserService0012efb0
014bf388 System.Windows.Forms.MouseEventArgs0012efb4
013c8b28 System.EventHandler0012efb8
013c6a04 System.Windows.Forms.Button0012efc4
014c04a0 System.String test0012efc8
014c04a0 System.String test0012efcc 014c045c System.String wilhelm0012efd0
014c0430 Codemurai.Tutorial.WinDbg.ExceptionHunting.UserService0012efd4
014c0430 Codemurai.Tutorial.WinDbg.ExceptionHunting.UserService0012efd8
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.SqlCeExceptionMethodTable: 07d8255c
EEClass: 07d04f14Size: 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.StringMethodTable: 79330a00EEClass: 790ed64cSize: 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.
!pe
Exception object:
014c8974
Exception type: System.Data.SqlServerCe.SqlCeExceptionMessage: 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.
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 ;-)
Es gibt 11 Kommentare
bin als UNIXer heute ueber deinen Blog gestolpert, da ich App- und Systemcrashs unter Windows 2008 Server debuggen muss, da sind diese Tips leider nicht sehr hilfreich, wenn auch genial fuer Livedumps. Ich schliesse mich Andi an: du solltest auch Zeit darin investieren "post-mortem" Dumps analysieren zu koennen, z.B. wie man ein Crash Dump Verzeichnis fuer Applikationen erstellt, Symbols einliest, etc .... alles boehmische Doerfer fuermich, viel zu lesen bei Microplatsch, unter UNIX ist alles viel einfacher :-) Waer echt toll, wenn du deinen Blog dahingehend erweiterst und so komprimiert Infos zusammenfasst, dass man nicht 10.000 Webseiten lesen muss.
Gruss,
dj
bitte weitere Besipiele.
Eine Idee wäre Windows Troubleshooting, einen memory Dump zu analysieren.... Symbol-Dateien einbinden etc.
Merci