HOWTO:Remote Debugging fast and easy

Vor Jahren habe ich dazu bereits mal einen Artikel für VC6 veröffentlicht: Remote Debugging for Dummies! BTW: Es war überhaupt mein erster Artikel!
Also Zeit für ein kleines Upgrade.
Anmerkung: Mir geht es hier nur um natives Debuggen von C++ (also unmanaged Code), wen wird es wundern ? 😉

Für mich das ultimative Mittel der Qualitätssicherung und die beste Waffe für die Jagd auf den gemeinen Software-Bug ist und bleibt Remote Debugging.

Remote Debugging ist seit es Visual Studio 2002 gibt so einfach geworden wie noch nie. Und seit es Visual Studio 2005 ist es noch einen Tick einfacher geworden. Einziger Wermuthstropfen: Remote Debugging ist erst ab der Professional Version verfügbar.
Um so erstaunlicher wie wenig Entwickler dieses Werkzeug einsetzen.

Nehmen wir einfach mal den Fall, wir haben auf einer Maschine einen Crash, oder wir haben einen Zustand, den wir sofort Debuggen wollen. Geht das On-the-fly?

JA ❗

Wenn die Rechner in einer Domäne sind, gibt es meistens gar keine Probleme und man kann einfach wie folgt vorgehen. 

  1. Wir kopieren die Datei MSVSMON.EXE auf den Rechner auf dem wir Debuggen wollen:
    „C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\Remote Debugger\x86\msvsmon.exe“
    Ja! Erstaunlich, mehr ist nicht notwendig.
  2. Beim ersten Start erhalten wir eine Warnung, dass wir die Firewall von XP SP2 oder Vista noch freischalten müssen. Gesagt getan.
  3. Nun wählen wir einfach die schnelle etwas unsichere Methode:
    Menü Tools -> Options
    In dem entsprechenden Dialog, wählen wir nun einfach:
    No Authentication (native only)
    Allow any user to Debug
    Port 4015 bleibt unverändert.
    Am Besten jetzt noch die Maximum idle time (seconds) von 900 auf was Brauchbares hoch setzen, sonst wird die Session einfach beendet nach 15 Minuten.
  4. Ja ❗ Das wars. Nun einfach im Visual Studio Tools -> Attach to Process wählen (Strg+Alt+P)
    Transport: Remote (Native only with no authentication)
    Qualifier: <Name des PCs den es zu debuggen gilt, oder IP-Adresse>
    Refresh
  5. WOW! Und schon sieht man seinen Prozess den man debuggen möchte.
  6. Jetzt nur noch auswählen und Attach

So einfach geht es.
Wenn man die passenden PDB Dateien in Reichweite hat, kann man sofort Breakpoints setzen und am „lebenden Patienten“ operieren, als nur „post-mortem“ mit Crashdumps zu arbeiten.

Wie man gezielt auch die Projekteinstellungen nutzen kann, habe ich mir für einen späteren Artikel vorgemerkt. 😉

Siehe auch:
How to: Set Up Remote Debugging

VS Tipps & Tricks: Heap Bugs finden (Teil 3)

Mancher Bug macht einem nicht den Gefallen und lässt sich in der Debug-Version finden. Ursache ist oft genug eine Variable, die in der Debug-Version initialisiert (0xCC) wird aber in der Release-Version zu einem Crash führt, wenn zufällige Daten auf dem Stack für undefiniertes Verhalten sorgen.

Also macht man sich an das debuggen der Release Version und kann keinen Fehler finden.
Kaum startet man das Programm ohne Debugger dann kracht es wieder. Warum?

Manch einer könnte jetzt denken: Der Debugger verändert das Memory Layout! Das tut er schon, aber entscheidend für ein anderes Verhalten ist der Debug Heap!
Die wenigsten Entwickler wissen überhaupt, dass es ihn gibt. Ich meine hier nicht die Debug Funktionen, die die CRT zur Verfügung stellt, denn mein Thema heute ist ja das Debuggen eines Release-Programms, und die Debug-CRT hat ja bekanntlich in einem Release Programm nichts zu suchen!

Machen wir es praktisch und nehmen wieder mein kleines Crashtest-Programm:

#include <windows.h>
#include <tchar.h>
#include <crtdbg.h>
int _tmain(int argc, _TCHAR* argv[])
{
  char *pCorrupt = new char[100];
  ZeroMemory(pCorrupt,104);
  char *pOther = new char[100];
  ZeroMemory(pOther,100);
  delete [] pOther;
  delete [] pCorrupt;
  return 0;
}

Wenn wir dieses Programm als Release Version kompilieren und ausführen, dann erhalten wir keine Fehlermeldung ❗ Interessant. Der Heap ist nicht soweit zerstört, dass es zu einer Zugriffsverletzung kommt. Starten wir aber unser Programm mit dem Debugger, dann wird der so genannte Debug Heap des Systems verwendet, der wie die Debug-CRT Guardbytes setzt und kontrolliert.

Ein weiteres Problem entsteht dadurch, dass der Debug Heap den allokierten Speicher auf feste Werte initialisiert genau wie die Debugversion der CRT. Wenn also nicht initialisierter Speicher genutzt wird, dann ist das Verhalten mit dem Debug-Heap deterministisch, ohne Debug Heap eher zufällig.

Das im Debugger alles etwas anders sein kann ist sogar dokumentiert 😉

Processes that the debugger creates (also known as spawned processes) behave slightly differently than processes that the debugger does not create.
Instead of using the standard heap API, processes that the debugger creates use a special debug heap. On Microsoft Windows XP and later versions of Windows, you can force a spawned process to use the standard heap instead of the debug heap by using the _NO_DEBUG_HEAP environment variable or the -hd command-line option.

In diesem Text steht auch, wie man den Debug-Heap ausschalten kann, mit:

SETX _NO_DEBUG_HEAP 1

Diese Environment-Variable sorgt dafür, dass sich auch bei geladenem Debugger, das Programm so verhält wie ohne Debugger (hoffentlich). Führt man mein Testprogramm nun im Debugger aus, wenn die Environment-Variable _NO_DEBUG_HEAP auf 1 gesetzt ist, erhält man keinen Debug-Break mehr. Denn in diesem Fall gibt es keine Guardbytes, die geprüft werden.
Löscht man den Eintrag _NO_DEBUG_HEAP wieder, dann erhält man im Debugger wieder wie erwartet einen Break.

Will man also wirklich realitätsnah eine Release-Version debuggen, dann kommt man um das Ausschalten des Debug-Heaps nicht herum.

PS: Man kann es auch etwas einfacher haben, wenn man sich nachträglich an den Prozess mit dem Debugger attached (wenn das geht). Ideal ist dieses Verfahren auch beim Remote-Debugging (dazu demnächst mehr).

VS Tipps & Tricks: Heap Bugs finden (Teil 2)

Einige Hilfsmittel um einen Heap-Fehler zu finden habe ich in meinem letzten Beitrag ja beschrieben.

Eigentlich wünscht sich der Entwickler nichts mehr, als dass ein falscher Zugriff auf den Heap, sofort einen Break im Debugger auslöst. Die Methoden, die ich bisher gezeigt habe (AfxCheckMemory, _CrtCheckMemory, _CrtSetDbgFlag) können das nicht direkt , aber zumindest helfen sie den Fehler einzukreisen.

Ein unverzichtbarer Helfer, der sofort solch einen Break auslösen kann, ist der Application Verifier, den ich bereits in einem älteren Artikel als Freund und Helfer vorgestellt habe.

Seit Visual Studio 2005 kann man direkt Parameter für den Application Verifier im Projekt einstellen und auch direkt den Debug-Prozess mit dem Application Verifier starten (Umschalt+Alt+F5).
An den Standardeinstellungen im Projekt braucht man hier gar nichts zu ändern:
Conserve Memory – No
Protection Location – Je nach Testfall (man sollte mit beiden Einstellungen mal debuggen)
Alle anderen Einstellungen Verification Layers Settings – auf Enable

Mit dem Application Verifier lässt sich der so genannte Paged Heap nutzen, der Guard Pages anlegt hinter oder vor den allokierten Speicherbereichen (siehe auch GFLAGS.EXE). Der Vorteil: Man erhält sofort eine Access Violation, wenn man den Speicherbereich überschreitet.

Mein kleines Demoproramm

#include <windows.h>
#include <tchar.h>
#include <crtdbg.h>
int _tmain(int argc, _TCHAR* argv[])
{
  char *pCorrupt = new char[100];
  ZeroMemory(pCorrupt,106); // -- This will corrupt the heap
  char *pOther = new char[100];
  ZeroMemory(pOther,100);
  delete [] pOther;
  delete [] pCorrupt;
  return 0;
}

crashed mit der Nutzung des Application Verifiers sofort und man kann im Call Stack die Zeile 7 ausmachen.
Genial ist besonders, dass der Application Verifier auch mit der Release Version sofort die Zeile 7 als Ursache identifiziert. Gerade wenn man also nicht auf die Debug-CRT zurückgreifen kann, ist der Application Verifier ein super Hilfsmittel.

Der Nachteil: Die Guard Pages liegen nicht exakt und direkt hinter dem allokierten Bereich, sondern auf der nächsten Page Boundary. Deshalb crashed mein Sample auch nicht wenn man den Speicher um nur 1 Byte überschreitet.

Aber der Application Verifier ist zum Testen ein absolutes Muss, weil auch falsche Handles erkannt werden und auch der Lock Verfification Layer für die Qualitätssicherung einfach nützlich zum entwanzen sind. (siehe auch Application Verifier Einstellungen in der MSDN).

Hinweis ❗

Auf Windows XP und Windows Server 2003 erhält man ohne administrative Rechte die folgende Fehlermeldung:

Access denied. You need administrative credentials to use Application Verifier on image <App_Name.exe> on machine <Machine_Name>. Contact your system administrator for assistance

Unter Windows Vista oder Windows Server 2008 erhält man die flogende Fehlermeldung wenn der Application Verifier nicht elevated gestartet wird:

Access denied. You need administrative credentials to use Application Verifier on image <App_Name.exe> on machine <Machine_Name> or per user verifier settings should be enabled by the administrator. Please refer to documentation for more information.

Durch einen simplen Eintrag in der Registry lässt sich aber auch als normaler Benutzer, ohne administrative Rechte, der Application Verifier nutzen, man erzeugt einen DWORD Eintrag in der Registry mit dem Wert 1
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manger\ImageExecutionOptions
Nach einem Reboot kann man nun einfach den Application Verifier auch non-elevated, als normaler Benutzer nutzen.

VS Tipps & Tricks: Heap Bugs finden (Teil 1)

Probleme finden, die mit dem Heap zusammenhängen ist oft genug eine Sache für sich und für Anfänger nicht selten ein Buch mit sieben Siegeln. Die CRT und der Debugger stellen aber einige Werkzeuge zur Verfügung, die es einem doch mit etwas Geschick und Wissen erlauben auch komplexere versteckte Fehler zu finden, die Heapfehler auslösen.

Jeder C/C++ Entwickler hat schon Meldungen dieser Art beim Testen seiner Programme gesehen:

HEAP[CrashTest.exe]: Heap block at 006D7920 modified at 006D79B0 past requested size of 88
Windows has triggered a breakpoint in CrashTest.exe.
This may be due to a corruption of the heap, which indicates a bug in CrashTest.exe or any of the DLLs it has loaded. 

oder etwas in dieser Art:

Debug Error!
Program: …nts\Visual Studio 2008\Projects\CrashTest\Debug\CrashTest.exe
HEAP CORRUPTION DETECTED: after Normal block (#110) at 0x000D7948.
CRT detected that the application wrote to memory after end of heap buffer.

Ursache ist ein Fehler wie in diesem kleinen Beispielprogramm:

#include <windows.h>
#include <tchar.h>
#include <crtdbg.h>
int _tmain(int argc, _TCHAR* argv[]) 
{ 
  char *pCorrupt = new char[100];
  ZeroMemory(pCorrupt,106); // -- This will corrupt the heap 
  char *pOther = new char[100]; 
  ZeroMemory(pOther,100); 
  delete [] pOther; 
  delete [] pCorrupt; 
  return 0; 
}

Wenn die Debug-CRT benutzt wird erhält man automatisch einen Break im Debugger wenn der Speicherblock pCorrupt freigeben wird (Zeile 11). Man braucht also nur den Call-Stack oder Stacktrace anzusehen und kann zumindest feststellen, welcher Block defekt ist.

Schwieriger wird es dann schon die Stelle zu finden, an der der Block überschrieben wird. In meinem Beispiel also die Zeile 7. Besonders dann wird es komplex, wenn das Programm größer ist, und der Speicherblock an evtl. sehr vielen Stellen genutzt wird.

Um die Position einzugrenzen und nicht evtl. bis zum Programmende warten zu müssen, wenn man (hoffentlich) alle Objekte freigibt kann man die CRT veranlassen den Heap zu prüfen. Dies geschieht mit _CrtCheckMemory oder AfxCheckMemory.
Streut man also in seinem Code an strategisch guten Stellen das folgende Statement in seinen Code

ASSERT(AfxCheckMemory()); // oder _CrtCheckMemory

kann man relativ gut die Stelle einkreisen die den Fehler verursacht, und das ohne große Performanceverluste. Man erhält sofort einen ASSERT, ab dem Moment ab dem die Integrität des Heaps zerstört wurde und der Check durchgeführt wird.

Noch etwas einfacher ist es, die CRT dazu zu bringen sich sofort zu melden wenn der Heap zerstört wird. Dies kann man erreichen indem man das Debug-Flag _CRTDBG_CHECK_ALWAYS_DF setzt.

Platziert man bei Programmstart die folgende Codezeile in seinem Programm

_CrtSetDbgFlag(_CrtSetDbgFlag(0)|_CRTDBG_CHECK_ALWAYS_DF);

dann unterbricht die CRT das Programm sofort bei der nächsten Allokation eines Speicherbocks, nachdem der Heap zerstört wurde. In meinem Beispiel also direkt bei der nächsten Allokation in Zeile 8!
Nachteil ist, dass bei jeder Allokation der Heap geprüft wird und damit die Performance schon in den Keller gehen kann, wenn das Programm groß ist und der Fehler evtl. selten auftritt.

Man kann also mit den einfachen Bordmitteln der CRT einen Fehler schon relativ leicht eingrenzen.

Soweit für heute. Was man noch alles machen kann um effektiv Heap-Fehler zu finden werde ich demnächst noch in weiteren Artikeln zu diesem Thema beschreiben.

Dr. Watson starb unter Vista…

… und es war nicht etwa wieder Prof. Dr. Moriarty :mrgreen:

Wenn man unter Vista einen Crashdump benötigt dann sucht man DRWTSN32.EXE unter Vista vergeblich. Für die Qualitätssicherung sind mir Minidumps immer extrem wichtig.

Es stellt sich die nette Frage: Wie kommt man von einem Kunden dann an einen informativen Minidump, wenn man keinen WER-Account hat, oder das Programm nicht signiert war, oder gar der WER Server diese Dumps nicht anfordert?

Unter Vista ist alles noch einfacher und schwieriger geworden. Vista speichert leider nicht grundsätzlich Minidumps, aber es legt für Crashs unter Problemberichte und Lösungen einen eigenen Eintrag an. Zu Crashs von WER registrierten Programme werden hier evtl. direkt Lösungen oder Updates angeboten. Und auch Crashdumps werden hier mit abgelegt, wenn sie erzeugt werden..

So ist es unter Vista aus:

  1. Das WER System in Vista erzeugt normalerweise nur Minidumps für signierte Programme bzw. wenn der WER Server einen Crash abruft.
  2. Um immer einen Minidump zu erhalten muss in der Registry der folgende Wert gesetzt werden:
    HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\ (DWORD)  mit Namen ForceQueue  erzeugen und auf 1 setzen.
  3. Dumps werden im Benutzerverzeichnis unter C:\Users\TheUserName\AppData\Local\Temp und hier C:\ProgramData\Microsoft\Windows\WER\ReportQueue abgelegt und sind meistens gepackt. Sie erhalten die Endung *.mdmp.
  4. Der Zugriff kann ganz einfach erfolgen mit Systemsteuerung -> System und Wartung -> Problemberichte und Lösungen -> Probleme anzeigen und überprüfen
  5. Dort findet man das entsprechende Programm und die dazugehörigen entsprechenden Crashs.
  6. Weitere Infos einfach über Details anzeigen lassen.
  7. Wenn es einen Dump gibt, dann findet man hier wieder einen Eintrag Temporäre Kopie dieser Dateien anzeigen
  8. Damit lässt sich der Explorer öffnen und man erhält Zugriff auf die gespeicherten Daten.
  9. Diese kann man sich nun als Entwickler zusenden lassen.

Siehe auch:

Was tun gegen Fehler in statischen Tabellen bzw. Variablen Deklarationen

Ich habe es immer wieder mit langen Listen von statischen Datenbeschreibungen zu tun. Das können Parserlisten, oder Sprachbeschreibungen, die baumartig vorliegen oder einfach nur simple Arrays sein.

In den meisten Fällen versuche ich solche Strukturen als konstante PODs (Plain Old Data) aufzubauen. D.h. also in einer Form:

  1. sodass kein weiterer Code notwendig ist um diese zu initialisieren (z.B. vermeinden von Konstruktoren/Destruktoren)
  2. dass die Daten sogar möglichst als const deklariert werden und so in einem Code Segment in Modulen geshared werden können.
  3. dass sie sich leicht aus einer „Art Datenbank“ in C/C++ Code erzeugen lassen.

Oft haben diese Datenstrukturen eine komplexe Struktur und auch einen Aufbau nach bestimmten Regeln, wie z.B. dass alle Daten eines Arrays sortiert sein müssen. Dadurch kann der Algorithmus, der diese Daten nutzt natürlich effektiver geschrieben werden, als wenn er auf einem unsortierten Array aufsetzt.

Eine Erfahrung, die ich heute weitergeben will, lautet nun:

Vertraue niemals darauf, dass die Daten in statischen konstanten Strukturen so vorliegen wie Du es glaubst… zumindest nicht in der Debug Version! 😉

Hintergrund für diese Erfahrung ist ein Bug, der sich genau aus einer Annahme einschlich, das statische Daten in einer bestimmten Regel vorliegen. In dem aktuellen Fall war es die bereits erwähnte Regel: „Der Array ist sortiert, nach einer ID“. Das Ganze war sogar am Anfang der besagten Struktur und im Code korrekt und ausführlich dokumentiert. Allerdings ist der Sourcecode dieser Tabelle mehrere 1000 Zeilen lang und ein Entwickler fügte nun einfach ein paar neue Einträge an das Ende der Liste an.
<ironiemode>Außerdem, wer liest schon interne Programmdokumentationen, wo wir alle (insb. der Autor und die geschätzten Leser dieses Artikels) doch fähig sind, den Sinn und Zweck von Code mit einem Blick zu erfassen?</ironiemode>

Die Folge war: „Nun ist der Array nicht mehr sortiert“ und die weitere Folge war, dass ein Algorithmus, der auf einer binären Suche basierte, nicht fand, was er finden sollte.

Dabei hätte Stück simpler Validierungscode, vermeiden können, dass dieser Code in Produktion ging:

class CSomeMightyDataHandler
{
public:
    CSomeMightyDataHandler();

    ...

private:
    // complex static data
    struct S_COMPLEXDATA
    {
        ...
    };
    static const S_COMPLEXDATA *m_pComplexData;

    // Validator for Debug mode
#ifdef _DEBUG
    static const bool m_bValidated;
    static bool ValidateData();
#endif
...
};

#ifdef _DEBUG
// Validation for debug code only!
const bool CSomeMightyDataHandler::m_bValidated = CSomeMightyDataHandler::ValidateData();

bool CSomeMightyDataHandler::ValidateData()
{
    // Do some validation and reset bResult on any failure
    bool bResult = true;

    ...

    ASSERT(bResult);
    return bResult;
}
#endif

Mit solch einem Code wäre sofort ein ASSERT nach der ersten inkorrekten Code Ergänzung geflogen.

PS: Folgende Annahmen stimmen:

  1. Der Programmierer, der den Code falsch hinzugefügt war ich selbst.
  2. Der Programmierer, der beim Design der Klasse keinen Validierungscode geschrieben hat war auch ich selbst.
  3. Ich hatte wirklich vergessen, wie dieser Algorithmus ursprünglich mal von mir geplant war (sprich, dass eine Sortierung beachtet werden muss).
  4. Ich kann mir in den Hintern beißen für soviel selbst gemachte Dummheit.

Tipps & Tricks: Testcode sollte immer in #ifdef _DEBUG #endif Blöcke integriert sein!

Dieser Tipp hört sich trivial an, aber wenn man sich nicht dran hält erlebt man übelste Überraschungen.
Nur zu oft muss man während der Entwicklung oder bei der Fehlersuche Code einbauen, der Test, Ausgaben, Verzögerungen oder sonstige Operationen ausführt, die mir als Entwickler helfen ein Problem zu finden, oder einer Lösung für eine verzwickte Frage zu lösen. Um so größer das Problem wird um so mehr Stellen werden oft verändert.
Es bleibt die Frage ob sich jeder Entwickler noch erinnert wo er überall etwas für Testzwecke eingebaut hat.

Das üble Ergebnis ist, dass man manchmal toten nutzlosen, Performance fressenden Code ausliefert. Oder gar Code ausliefert, der evtl. zu neuen Fehlern führt. Da muss nur ein einfacher DebugBreak im Code zurückbleiben und schon crashed die Anwendung sauber beim Kunden…

Testcode sollte grundsätzlich in einem #ifdef Block eingebaut werden. Und Code der wirklich nur für Tests vorhanden ist und er sogar später in der Debug Version des Programmes nichts zu suchen hat sollte mit einem #else #error versehen werden. Ein ASSERT kann einen viel abnehmen um so etwas zu vermeiden, aber sogar mancher ASSERT  ist später überflüssig und behindert auch Tests in der Debug Version.

So habe ich in unserer Software einen Sleep(100); 😮 gefunden, der von einem Entwickler eingebaut wurde, um einen Crash in einem komplexen Kommunikationsproblem zwischen mehreren Threads zu finden.Er hat den Fehler gefunden, den Sleep aber nie wieder entfernt.
Hätte mein Kollege sich an meine Spezifikationen gehalten, hätten wir nicht nachträglich auf die mühsame Suche gehen müssen wo unsere Performanceverluste bei 5% Prozessorlast herkommen. So wäre das Ganze schon beim Release-Build aufgefallen:

#ifdef _DEBUG
// Test Sleep to find cross thread problems for bug#234
Sleep(100);
#else
#error Remove test condition here. Just used to find bug#234 
#endif

BTW: Ich verwende deshalb immer ein Code-Snippet über mein VisualAssist X, der mir einen entsprechenden Codeblock einsetzt.

Debugging und ASSERTs in Services

Ich habe in der letzten Zeit einige COM-PlugIns  und Service Komponenten entwickelt. Alles Teile von anderen Diensten und TSPs (Tapi Service Provider). D.h. alles ohne UI. Die ganze Maschinerie, die ich hierzu verwendete befand sich auf einem Windows 2003 R2 Server. Aufgrund bestimmter Hardware war ein virtueller Server zum Testen nicht drin.
Macht ja nix. Man kann ja auch mit Remote Desktop auf dem Server vom eigenen Platz aus arbeiten, ohne deshalb im klimatisierten und immer zu kaltem und außerdem viel zu lautem Serverraum zu arbeiten…

Ziemlich schnell nervte mich gleich ein bestimmtes Problem. Ein Service mit einer meiner Komponenten stand auf einmal. Ich habe mich mit dem Debugger remote attached und merkte mehr oder weniger schnell, dass ein bestimmter Thread (von 67) auf einen ASSERT gelaufen war. Dämlicher Weise hatte der nun kein DebugBreak ausgelöst. Genaugenommen stand der Thread in einer MessageBox mit dem ASSERT Fenster, dass jeder kennt.
Da ich aber per Remote Session mit dem Server verbunden war sah ich diese nicht. Wäre ich am primären Monitor angemeldet gewesen, hätte mich die MessageBox erreicht, dafür trifft die CRT Vorsorge.
Dämlich! Mir wäre sogar ein Crash (mit Minidump natürlich) lieber gewesen. So stand der Service blockierte noch drei andere Sachen und es dauerte doch einige Zeit bis ich diesen stehenden Service als Ursache ausmachen konnte. Wäre der Service gecrasht hätte ich es in Sekunden mitbekommen.

OK! Wie gestalte ich das System nun um, dass ein ASSERT immer einen DebugBreak auslöst und keine MessageBox, die sowieso keiner zu sehen bekommt?
Das würde einen Minidump schreiben und wenn ich mit dem Debugger verbunden wäre, würde es sofort das System an der entsprechenden Stelle stoppen. Die MessageBox mit dem ASSERT brauche ich nicht.

Ein wenig Lesen in der CRT Doku schadet nicht. Also hier die Lösung:

Schritt 1: Wir verhindern, dass die entsprechende MessageBox erscheint und stellen entsprechend ein, dass der ASSERT in der Debug Ausgabe mit protokolliert wird. Und wenn man es hat auch noch will, zusätzlich in einer Protokolldatei.

_CrtSetReportMode(_CRT_ASSERT,_CRTDBG_MODE_DEBUG/*|_CRTDBG_MODE_FILE*/);

Schritt 2: Nun brauchen wir noch einen DebugBreak, der immer ausgelöst wird. Auch das ist kein Problem. Wir benutzen den Debug Report Hook:

_CrtSetReportHook2(_CRT_RPTHOOK_INSTALL, MyDebugHook);

MyDebugHook ist nun nichts weiter als eine kleine Funktion die nur eins enthält: den Aufruf der Funktion DebugBreak();.

So ausgestattet lassen sich Services im Debugmode weitaus besser entwickeln. Jetzt sorgen Sie wenigstens für einen anständigen Crash (natürlich mit Dump), wenn es ASSERTet… :mrgreen:

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.