André Krämers Blog

Lösungen für Ihre Probleme

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

BefehlBedeutung
sxe clrBei jeder CLR Exception anhalten
.loadby sos mscorwksSOS Extension passend zur .NET Framework Version laden
gAusführung fortführen
!DumpStackObjects / !dsoAuflistung aller Objekte, die auf dem Stack verwiesen werden
!DumpObj /!do 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 ;-)

Es gibt 11 Kommentare

Comment by klaus_b
Von | 29.10.2011 21:11
Hallo Andrè,das Thema hat richtig Potential. Ich enpfinde diesen ersten Teil als sehr gelungen. Vor allem da du mit der Installation und den Debugsymbolen beginnst :)Ich von meiner Seite bin an einer Vortführung deiner Serie sehr interessiert.Servus,Klaus
Comment by Marcell Spies
Von Marcell Spies | 29.10.2011 21:11
Hi,einsame Spitze! Vielen Dank für diesen Artikel!Habe momentan leider keine Zeit das zu Testen, aber es ist gut sowas im Hinterkopf zu haben, falls mal wieder ein Problem auftritt.Ich würde mich auf jeden Fall sehr freuen, wenn du weitere Artikel zu diesem Thema veröffentlichst!Viele GrüßeMarcell
Comment by Marcell Spies
Von Marcell Spies | 29.10.2011 21:11
Hi,einsame Spitze! Vielen Dank für diesen Artikel!Habe momentan leider keine Zeit das zu Testen, aber es ist gut sowas im Hinterkopf zu haben, falls mal wieder ein Problem auftritt.Ich würde mich auf jeden Fall sehr freuen, wenn du weitere Artikel zu diesem Thema veröffentlichst!Viele GrüßeMarcell
Comment by André Krämer
Von | 29.10.2011 21:11
Vielen Dank für das Lob!@Marcell: Mehrfachkommentare zählen nicht ;-)
Comment by Steffen
Von Steffen | 29.10.2011 21:11
Gut gemacht. Auf jeden Fall bitte eine Serie daraus machen! :-)
Comment by Michael Eichhorn
Von Michael Eichhorn | 29.10.2011 21:11
Danke für den tollen Artikel. Auch ich wäre an einer Fortsetzung sehr interessiert.
Comment by unbekannt
Von | 29.10.2011 21:11
Bitte, bitte Forstsetzung !!!
Comment by unbekannt
Von | 29.10.2011 21:11
Bitte bitte Fortsetzung !!
Comment by dj
Von dj | 21.12.2014 00:06
Hi Andre,
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
Comment by Tim
Von Tim | 22.01.2014 10:20
Großes Lob für die Qualität und die investierte Zeit, die in dem Artikel steckt. Gerne würde auch ich mehr KnowHow dieser Form genießen. Viele Grüße
Comment by Andi
Von Andi | 17.06.2014 06:35
Hallo Andre,

bitte weitere Besipiele.
Eine Idee wäre Windows Troubleshooting, einen memory Dump zu analysieren.... Symbol-Dateien einbinden etc.
Merci