VS-Tipps & Tricks: Tracepoints die zweite…

In meinen ersten Artikel zu Tracepoints habe ich ja auch erwähnt, dass man sehr einfach auch Variablen ausgeben kann. Das schöne ist, dass dies sofort auch für Iteratoren und manche andere Klasse funktioniert, ohne dass man spezielle Member angeben muss. Man setzt einfach die Variable in geschweifte Klammern. Und das dürfen auch komplexe Ausdrücke sein, wie im Debug-Watch-Fenster.

So führt die folgende Tracepoint Definition in einem meiner Programme:

$FUNCTION, {iCountry,x} {s_countries[iCountry]}

zu der folgenden Ausgabe:

wmain(int, wchar_t * *, wchar_t * *), 1 {m_szISO3=0x00487e48 „AUT“ m_szISO2=0x00487e44 „AT“ m_szCountryCode=0x00487e40 „43“ }
wmain(int, wchar_t * *, wchar_t * *), 2 {m_szISO3=0x00487e3c „AUS“ m_szISO2=0x00487e38 „AU“ m_szCountryCode=0x00487e34 „61“ }

Ein leider undokumentiertes Feature von Tracepoints ist jedoch, dass innerhalb der geschweiften Klammern auch die bekannten Formatierungen aus dem Debug-Watch-Fenster verwendet werden können. Wie hier zum Beispiel um die Ausgabe des Intergers in hexadezimal zu ändern:

$FUNCTION, {iCountry,x} {s_countries[iCountry]}

wmain(int, wchar_t * *, wchar_t * *), 0x00000001 {m_szISO3=0x00487e48 „AUT“ m_szISO2=0x00487e44 „AT“ m_szCountryCode=0x00487e40 „43“ }
wmain(int, wchar_t * *, wchar_t * *), 0x00000002 {m_szISO3=0x00487e3c „AUS“ m_szISO2=0x00487e38 „AU“ m_szCountryCode=0x00487e34 „61“ }

Besonders ist das interessant, wenn Fensterprozeduren tracen möchte ohne das Trace-Tool oder Spy++ verwenden will und man die Formatspezifkation wm verwendet und alle Fensternachrichten im Klartext lesen kann. Oder man verwendet hr und sieht auch einen HRESULT nicht mehr nur als kryptische Zahl.

VS-Tipps & Tricks: Insert Tracepoint, der nette Helfer beim Debuggen

Breakpoints kennt jeder, aber Tracepoints!?!

Den Menüpunkt Insert Tracepoint findet man im Kontext Menü des Editors.
Wer kennt das nicht? Man ist mitten in einer Debug-Session, aber man müsste jetzt eine Variable beobachten, wie sie sich entwickelt. Manchmal ist es einfach ungünstig einen Breakpoint zu stetzen, weil der Timing, Focus und manches andere ändert. Zudem, Breakpoints sind lahm und die F5-Taste will man auch icht kaputt machen. Ein TRACE Statement wäre super. Aber jetzt den Code ändern? Evtl. klappt das Edit&Continue nicht oder man kann es nicht einrichten, weil man eine Release Version debuggt.

Hier ist ein Tracepoint ideal. Er leistet das, was ein TRACE Statement auch leistet, nur ohne Code Änderung. Einfach Insert Tracepoint auswählen und angeben was man gerne sehen möchte.
z.B.: $FUNCTION bReset={bResetClipRegion} bEmpty={bEmptyClipRect}

Es stehen einige Makros zur Verfügung, die direkt im Dialog erklärt werden. Gigantisch nützlich sind die Variablen $FUNCTION , $CALLER, und $CALLSTACK. Das geht auch mit Variablen, die sich im aktuellen Kontext befinden, wie in meinem Beispiel zu sehen. Einfach die Variablen in geschweifte Klammern setzten und das war es schon.

Die Ausgabe erfolgt umgehend in der Debug-Ausgabe:
RedirectEraseBkgndToParent(CWnd *, CDC *) bReset=true bEmpty=true
RedirectEraseBkgndToParent(CWnd *, CDC *) bReset=true bEmpty=true
RedirectEraseBkgndToParent(CWnd *, CDC *) bReset=true bEmpty=true

Absolut super ist, dass man dazu noch nicht mal einen Breakpoint setzen muss. D.h. man kann während der Laufzeit einfach so einen Tracepoint setzen ohne große Eingriffe in die Software.

Und ganz ohne Probleme lässt sich auch ein Breakpoint in einen Tracepoint umwandeln und umgekehrt. Bei den Eigenschaften eines Breakpoints im Kontextmenü kann man die Eigenschaft When hit… entsprechend bearbeiten. Nimmt man den Haken bei Continue Execution heraus, dann hat man einen normalen Breakpoint. Setzt man den Haken bei Continue Execution, so macht man aus einem Breakpoint einen Tracepoint, man muss nur noch eine entsprechende Nachricht nach Wunsch angeben, die in der Debug Ausgabe angezeigt werden soll.

❗ Beachten muss man jedoch, dass die Performance schlechter ist als bei einem eingebauten TRACE. Denn ein Breakpoint wird ausgelöst und der Debugger übernimmt kurzfristig die Kontrolle.

MFC 8.0 PDB Dateien mit und ohne Source Informationen

Gestern hat es mich überrascht, dass ich beim debuggen auf einmal nicht mehr in eine MFC Funktion mit der F11 Taste steppen konnte. Er sprang immer über diese Zeile. Selbst über die Assembler Ansicht war es nicht möglich die entsprechenden Source-Dateien der MFC im Debugger durch zu steppen.

❓ Eigentümlich. Ein kurzer Blick in die Debug Ausgabe und in die Liste der geladenen Module zeigte, dass die PDB Datei der MFC80UD.DLL aus meinem Symbol-Cache geladen wurden.
In der Modulliste stand als Info: Symbols loaded (source information stripped).
Ich verwende einen zentralen Symbol Cache auf unserem Entwicklungsserver und habe natürlich auch als http://msdl.microsoft.com/download/symbols als Quelle für unbekannte Symbole angegeben.

Scheinbar ist auf irgend einem Weg vom Symbolserver aus dem Netz eine Version in meinen Cache hineingelangt, die nicht alle Debug Informationen enthält. Also gerade die Informationen, die es mir erlauben durch den Sourcecode der MFC zu steppen.

Die entsprechende Version mit den Source Informationen befindet sich durch die Installation von Visual Studio im Verzeichnis C:\Windows\Symbols\dll. Auch in den Optionen für mein Visual Studio war zusätzlich natürlich korrekt auch C:\Windows\Symbols\dll als Pfad angegeben. Die dortigen PDB Dateien wurden jedoch offensichtlich ignoriert. Ein manuelles Nachladen aus diesem Verzeichnis half allerdings sofort.
Bei der nächsten Debug Session wurden jedoch wieder die Informationen aus dem Cache geladen.

Also was machen?

❗ Ich habe einfach die entsprechenden Symbol aus dem C:\Windows\Symbols\dll Verzeichnis in meinen Symbol Cache geladen. Das geht einfach mit dem entsprechenden symstore Befehl:

symstore add /r /f C:\Windows\Symbols\dll\*.* /s <My symbol store>

Und siehe da. Nun werden immer die richtigen PDB-Dateien geladen und Step-Into Befehl F11 verhält sich beim debuggen wieder wie gewohnt.

CreateStreamOnHGlobal und GlobalAlloc

Man könnte diesen Artikel auch den folgenden Titel „Was passieren kann, wenn man die Dokumentation nicht richtig liest!“ geben. 😈

Dieser Code

HGLOBAL hMem = ::GlobalAlloc(GMEM_MOVEABLE,iMaximumSize);
if (!hMem)
    AfxThrowMemoryException();
LPVOID pImage = ::GlobalLock(hMem);
int iSize = FillBufferWithData(pImage);
::GlobalUnlock(hMem);
CComPtr<IStream> spStream;
HRESULT hr = ::CreateStreamOnHGlobal(hMem,FALSE,&spStream);
if (SUCCEEDED(hr))
{
    // Limit the stream to its real size
    ULARGE_INTEGER ulSize;
    ulSize.QuadPart = iSize;
    VERIFY(SUCCEEDED(spStream->SetSize(ulSize)));
    // Do whatever has to be done
   DoSomethingWithStream(spStream);
}
// Release Stream
spStream = NULL;
// Free memory WITH A RANDOM CRASH
::GobalFree(hMem);

sieht ganz normal aus. Eigentümlicher Weise passiert es manchmal, genauer gesagt recht selten, dass der GlobalFree fehlschlägt. Der Application Verifier meldet einen inkorrekten Heap Block. Aber wie das. Der Block wird mit GlobalAlloc allokiert an CreateStreamOnHGlobal übergeben und freigegeben nachdem der Stream entsorgt wurde.

Was ist die Ursache für das Problem? Nach einigem Prüfen und Tests kam ich dahinter, dass offensichtlich nach einer Reallocation des Speichers mein Heap-Block ungültig wird.
Erstaunlich ist, dass SetSize hier nicht den Speicherbedarf vergrößert sondern nur den Stream auf die korrekte Größe verkleinert! Dennoch… unter bestimmten Umständen wird hier eine Reallocation vorgenommen.
Alles Rumexperimentieren nützt nichts. Der Fehler oder das Problem in diesem Stück Code wird auch nicht vom stundenlangen Ansehen und Debuggen nicht klarer.

Also nochmal richtig RTFM (Read the fine MSDN) und dort finden wir diesen netten Absatz:
hGlobal [in] Memory handle allocated by the GlobalAlloc function. The handle must be allocated as ➡ movable and nondiscardable.

Und nun wird klar wo der Fehler liegt. Der Speicherblock der mit GMEM_FIXED alloziert wurde kann nicht realloziert und vergrößert werden. CreateStreamOnHGlobal reagiert nicht mit einem Fehler, wenn man den Block mit GMEM_FIXED allokiert. Aber das hat später evtl. einen sehr schwierig zu lokalisierenden Fehler zur Folge.

Also flink die Allokation in GMEM_MOVEABLE geändert und siehe da. Das Handle bleibt erhalten, der GlobalFree schlägt nicht mehr fehl.

Die Dokumentation ist an dieser Stelle aber auch wirklich nicht sehr auffällig. 😐
Deshalb habe ich einen Verbesserungsvorschlag dazu gemacht:
https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=260866

Anmerkung: Der Ursprüngliche Code verwendete CHeapPtr<BYTE,CGlobalAllocator>, weil ich es liebe wenn Destruktoren aufräumen. Der CGlobalAllocator verwendet auch immer GMEM_FIXED, was das ganze noch etwas unübersichtlicher gemacht hat, denn wer denkt schon dran wie eine solche Wrapper-Klasse Speicher allokiert.

BTW: Auch hier führte der Application Verifier auf die korrekte Spur.

Der Application Verifier, mein neuer Freund

Durch die Vista Zertifizierung habe ich den Application Verifier von Microsoft als neuen guten Freund kennengelernt. Warum? Schauen wir uns mal den nachfolgenden Code an:

{
  CImageList il;
  il.Create(IDB_IMAGELIST,16,0,RGB(255,0,255));
  m_lcList1.SetImageList(&il,LVSIL_SMALL);
  m_lcList2.SetImageList(&il,LVSIL_SMALL);
  il.Detach();
}

Dieser Code ist natürlich fehlerhaft! Allerdings nicht ersichtlich auf den ersten Blick. Er erzeugt korrekt eine Image List. Setzt diese in ein List View 1 und auch in ein List View 2. Sofern LVS_SHAREIMAGELISTS nicht gesetzt wird die Image List beim Zerstören des List Views 1 freigegeben. Aber eben auch noch mal wenn List View 2 zerstört wird! Nicht gut.

Oder noch ein übles Beispiel:

{
  CImageList il;
  il.Create(IDB_IMAGELIST,16,0,RGB(255,0,255));
  m_lcList.SetImageList(&il,LVSIL_SMALL);
  ImageList_Destroy(il.m_hImageList);
}

Er erzeugt auch korrekt eine Image List. Setzt diese in ein List View und zerstört dann einmal die Image List über das Handle und anschließend noch einmal durch den Destruktor von CImageList. Und ein drittes Mal wird die Image List beim zerstören des List Views freigegeben. Übel übel!

Was passiert wenn dieser Code ausgeführt wird? Nichts…
Natürlich würde keiner so etwas programmieren 😉 . Aber man kann sich vorstellen, dass so etwas ablauftechnisch und programmiertechnisch schon mal passieren kann.
Alles ist scheinbar in Ordnung, obwohl hier eine Zeitbombe tickt.

Wie kommt man einem solchen Bug auf die Spur?
Der Titel dieses Beitrages gibt die Antwort: Der Application Verifier.

Man installiert den Verifier und fügt die Anwendung zu den Verifier Einstellungen hinzu. Man belässt es bei den Default Einstellungen und ergänzt am Besten noch unter Miscellaneous die Checkboxen für DangerousAPIs und DirtyStacks.

Sobald man nun den obigen Code im Kontext eines Debuggers ausführt bekommt man eine Exception! Wow… und wenn man einen entsprechenden Symbolserver hat und den Stacktrace betrachtet sieht man

comctl32.dll!CImageListBase::IsValid()+0x2a bytes
comctl32.dll!_HIMAGELIST_QueryInterface@12()+0x29 bytes
comctl32.dll!_ImageList_Destroy@4()+0x19 bytes

Das Ausgabefenster des Debuggers zeigt zusätzlich:

03D8F964 : Invalid address causing the exception.
75C273A8 : Code address executing the invalid access.
0012F08C : Exception record.
0012F0A8 : Context record.

Aus den Informationen kann man sich leicht denken was hier faul ist, oder sein könnte. Es empfiehlz sich die Anwendung auch im Release Mode mit Debug-Infos zu erzeugen. Das sollte sowieso Standard sein!

Es lohnt sich seine Applikation mal mit dem Application Verifier auszuführen, wenn man mal gerade nichts zu tun hat und man wundert sich dann, an welchen Stellen einem sein – so gut programmierter oder gut gemeinter – Code um die Ohren fliegt :mrgreen:

Für wen Qulitätssicherung kein Fremdwort ist, der kommt an diesem sehr nützlichen Tool nicht vorbei. Der Application Verifier und weitere brauchbare Infos findet sich hier auf der entsprechenden Produktseite von Microsoft:
http://www.microsoft.com/technet/prodtechnol/windows/appcompatibility/appverifier.mspx

Visual Studio 2005 zeigt keine Symbole für die MFC80U/MFC80UD DLLs

Beim Testen auf meinem Laptop wundere ich mich, dass ich nicht mehr in die MFC80UD.DLL tracen kann. Die Routinen der MFC werden einfach übersprungen, auch, wenn ich mit der F11 einen Step Into machen möchte.
Was ist das? 😕

Ich habe auf meinem Rechner einen Symbolserver eingerichtet. Dazu habe ich eine Environment Variable definiert.
_NT_SYMBOL_PATH=symsrv*symsrv.dll*c:\Temp\localsymbols*http://msdl.microsoft.com/download/symbols

Soweit gut. Unter den Einstellungen im Visual Studio unter Tools -> Options -> Debugging -> Symbols, ist nichts eingetragen.
Das führt nun dazu, dass die Symbole aus dem Internet geladen werden. Dort sind jedoch für die MFC-DLLs keine Symbolinformationen vorhanden. Durch die Installation von Visual Studio befinden sich die Symboldateien unter C:\Windows\Symbols\DLL! Dieses Verzeichnis wird aber nicht mehr durchsucht.

Also einfach das Verzeichnis C:\Windows\Symbols\DLL in die Liste der Symbol-Verzeichnisse als erstes eingetragen und siehe da, alles funktioniert wieder wie gewünscht.

Es geht übrigens in VS 2005 noch eleganter als mit der _NT_SYMBOL_PATH Environment-Variable. Einfach in die Liste der Pfade http://msdl.microsoft.com/download/symbols, des schon erwähnten Dialoges, eintragen.
Wichtig ❗ Natürlich hinter dem Pfad C:\Windows\Symbols\DLL.
Auch das Verzeichnis für den lokalen Cache wird hier unter „Cache symbols from symbol server to this directory“ eingetragen.

Tipp: Sollte evtl. im Cache schon eine Version der MFC80 geladen worden sein, dann muss man diese Version evtl. manuell aus dem Cache entfernen, damit die Version aus C:\Windows\Symbols\DLL verwendet wird.

Voraussetzungen für Remote debugging mit MSVCMON (unmanaged)

Ich bin ein Fan von Remote Debugging!
Aber was benötigt man minimal für Remote Debugging auf dem Target auf dem man debuggen möchte (unmanaged Code)?
Das gesamte Remote Debugging Paket zu installieren ist aufwendig und verändert das Zielsystem. Wenn man auch noch bei einem Kunden vor Ort ist auch natürlich ein Eingriff, den man einem Admin erst mal erklären muss.
Geht es also auch mit weniger? JA…

Für VS.NET 2003 braucht man nicht viel, genau genommen 4 Dateien mit einem Datenvolumen von nicht mal 600kb (passt auf jede Diskette, sofern die noch einer benutzt ;-)) :

  • msvcmon.exe
  • msvcr71.dll
  • NatDbgDM.dll
  • NatDbgTLNet.dll

Wenn man nun auf Firewall Probleme verzichtet und Named Pipes als Transportschicht verwendet dann war es das schon. Dann muss man nur noch MSVCMON auf dem Target starten und es kann losgehen. Seit VS.NET 2003 startet MSVCMON ohne Angabe von Parametern mit named Pipes als Transportschicht. Unter Vista muss man nach meinen Erfahrungen immer den -u Parameter mit angeben wenn man Named Pipes verwendet (siehe hier).

Das einfachste ist es nun sich mit VS.NET 2003 remote auf einen laufenden Prozess zu attachen, den man debuggen möchte. Man öffnet dazu aus dem Menü Debug den Punkt Processes, wählt als Transportschicht Named Pipes und den Target PC. Die Liste der Prozesse füllt sich automatisch.
Nun nur noch den Prozess auswählen und auf Attach klicken. Sofern die PDB-Dateien übereinstimmen kann man sofort seine Breakpoints setzen und loslegen.
Das geht teilweise sogar noch, wenn eine UAE Meldung auf dem Monitor sichtbar ist.

Wer unbedingt TCP/IP als Transportschicht wählen will, der muss seit XP-SP2 einiges an der Firewall einstellen. Die zwei nachfolgenden Links geben entsprechende Hinweise:
http://support.microsoft.com/kb/833977/de
http://support.microsoft.com/kb/841177/en-us

Remote debugging mit MSVCMON im Pipe-Mode auf Vista

Man sollte meinen, dass MSVCMON (aus VS.NET 2003) im Pipe Modus unter dem selben User Account sofort funktionieren sollte. Aber dem ist nicht so.

Normalerweise melde ich mich an der Entwicklungsmaschine und dem Remote-PC auf dem gedebuggt werden soll mit dem selben Benutzernamen an. Wird MSVCMON nun ohne Parameter auf dem Remote Computer gestartet kann man normalerweise sofort eine Verbindung herstellen.
Nicht so unter Vista. Dort bekommt man beim Versuch eine Verbindung herzustellen sofort die Meldung: „Unable to connect to ‚DEV-VISTA‘. Zugriff verweigert“, oder auf einem englischen OS „access denied“.

Nach vielem hin und herspielen und dem Versuch MSVCMON im Administrator-Modus zu starten bin ich auf die Lösung gekommen.

Gibt man direkt noch einmal mit dem -u Parameter den gewünschten Usernamen an, dann erlaubt Vista auch den entsprechenden Zugriff.
Also so gestartet MSVCMON -u MeineDomain\MeinUserName hat man keine Probleme und man kann in gewohnter Weise eine Remote Debug-Session starten und auch unter Vista elementar einfach die Programme debuggen.

PS: Es geht in diesem Artikel natürlich um das Debuggen von nativen Programme, sprich unmanaged Code.