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.

Die Unsitte GetCursorPos statt GetMessagePos zu verwenden

Im Endeffekt ist es der gleiche Grund warum man GetKeyState und nicht GetAsynchKeyState verwenden sollte (siehe Die Unsitte GetAsyncKeyState statt GeyKeyState zu verwenden…).

Auch hier liefert GetCursorPos die aktuelle Mauszweiger Position während GetMessagePos die Position liefert in dem Moment in dem die aktuelle Windows Nachricht eingeliefert wurde.
Oft genug wird hier kein Unterschied sein, aber durch aufwendigere Berechnungen oder gar durch eine verzögerte Abarbeitung von Windows Nachrichten (lange Zeit wurde evtl. keine Nachrichtenschleife mehr abgearbeitet), kann es sehr wohl passieren, dass ein Mausklick für eine Position eingeliefert wird, der (Millisekunden) her ist, während die aktuelle Mausposition schon Millimeter weiter ist.

Ist ein User dann richtig schnell und hektisch, kann die folgende Kombination fatale Folgen bei Verwendung von GetCursorPos haben.

  • System ist etwas beschäftigt und die Nachrichtenschleife wird nicht zeitnah abgearbeitet
  • User klickt mit Maus auf Item1
  • Die Maus wird etwas weiter bewegt und steht nun auf Item2
  • Verzögertes Abarbeiten beginnt jetzt und das Programm ermittelt mit GetCursorPos Item2 und selektiert es.
  • User drückt Entf-Taste und wundert sich 😮 dass Item2 weg ist!

Wäre GetMessagePos hier verwendet worden, wäre das korrekte Item1 gelöscht worden.

Das Verwechseln dieser beiden Funktionen hatte mir in einem Stück Software sogar einen richtigen Crash beschert. Wir hatten in einem bestimmten Fenster eigentümliche Abstürze. Er trat immer auf wenn bestimmte Leute (Kategorie Poweruser oder Übernervös) mit der Maus bestimmte Teile in der Auswertung markierten bzw. selektierten.

Ein Programmierer nutze GetMessagePos, der andere GetCursorPos für einige Kalkulationen. Meistens ist kein großer Unterschied zwischen den beiden Werten aber manchmal kam es schon vor. Effekt war aber hier, dass die eine Routine mit einem Objekt eine Funktion einleitete und an anderer Stelle ein anderes Objekt ermittelt wurde. Und gerade einige hektische Leute, die Klicken während Sie noch nicht mal genau gezielt haben, brachten es zustande, dass es einen Unterschied gab zwischen der ursprünglichen Position (bei der ersten auslösenden Nachricht) und der aktuellen Mausposition (wenn die Nachricht dann abgearbeitet wurde).

Kleiner Workarround für MFCNext in Verbindung mit CScrollView

Wenn man die BCG-Library oder MFCNext aus der VC++ 9.0 SP1 nutzt erhält man einen ASSERT wenn man ein CScrollView verwendet und wenn das Programm maximiert gestartet wird.

—————————
Microsoft Visual C++ Debug Library
—————————
Debug Assertion Failed!

Program: …\Debug\TestSDIScrollView.exe
File: f:\dd\vctools\vc7libs\ship\atlmfc\src\mfc\viewscrl.cpp
Line: 385

For information on how your program can cause an assertion
failure, see the Visual C++ documentation on asserts.

(Press Retry to debug the application)
—————————
Abbrechen   Wiederholen   Ignorieren  
—————————

Der Grund liegt darin, dass in der MFCNext Implementierung schon sehr früh ein RedrawWindow ausgeführt wird wenn das Main Window maximiert wird. In diesem Fall wird OnDraw/OnPaint bereits ausgeführt wenn SetScrollSizes noch nicht aufgerufen wurde. Das geschieht ja normalerweise meistens erst in OnInitialUpdate.
Dieser ASSERT soll dem Programmierer darauf hinweisen, dass SetScrollSizes unabdingbar für die korrekte Funktion des CScrollView notwendig ist.
Leider ist in diesem alten Code ein Seiteneffekt nicht berücksichtigt worden, der durch MFCNext in Spiel kam.

Das Ganze lässt sich jedoch einfach umschiffen indem man im Konstruktor seines Views vorab SetScrollSizes mit Dummywerten aufruft. Die eigentliche Initialisierung mag dann später wie gewohnt in OnInitialUpdate erfolgen.

CScriptEditorView::CScriptEditorView()
{
  // If the program is launched maximized, a RedrawWindow occurs in a very
  // early stage and OnDraw would be called without an initialized mapping mode
  // So we just do a dummy init here.
  SetScrollSizes(MM_TEXT,CSize(0,0));
}

Der Handler CWnd::OnGetDlgCode hat eine falsche Signatur

Ich habe mich in der letzten Zeit viel mit den Standard-Fensterklassen beschäftigen müssen. Insbesondere war hier auch das Zusammenspiel der Control in Dialogen in meinem Fokus.
In meinem Artikel Die Return-Taste in Dialogen, eine unendliche Geschichte habe ich ja bereits einiges dazu geschrieben.

Wer Aufmerksam nun die Windows API zu WM_GETDLGCODE liest und diese mit CWnd::OnGetDlgCode vergleicht, dem wird sofort auffallen, dass die Signatur nicht mit der Windows API Beschreibung übereinstimmt.

Ich habe aufgrund dieser Signatur in der MFC immer gedacht, dass alle Controls immer eine Konstante zurückgeben. Das die Controls individuell für jede Taste entscheiden können ob sie die haben wollen oder nicht ist mir dabei glatt entgangen. Will man es also richtig machen, dann muss man auf AfxGetCurrentMessage zurückgreifen um die entsprechenden wParam und lParam Werte zu erhalten.

UINT CMyWnd::OnGetDlgCode()
{
    MSG* pMsg = AfxGetCurrentMessage();
    // virtual keycode
    UINT uiVirtKey = pMsg->wParam;
    // get keyboard message causing WM_GETDLGCODE
    MSG *pKeyboardMsg = reinterpret_cast<MSG*>(pMsg->lParam);
...
    return uiNewDlgCode;
}

Jubiläum: Mein 10. Award als deutscher MVP für Visual C++

Am 1. Oktober habe ich nun zum zehnten Mal den MVP-Award für Visual C++ erhalten.

Nach dem nun zehnten Award erlaube ich mir mal einen kleinen Rückblick, denn in all den Jahren hat sich doch einiges geändert.

  • In den ersten Jahren war ich hauptsächlich in den stark genutzten NNTP Foren tätig. Leider sind die im Allgemeinen auf den absteigenden Ast. Von weit über 2000 Postings im Monat in 2000 sind wir auf um die 500 abgesunken. Früher langte es auch als MVP einfach ein Teil der Gruppe von Postern zu sein, die qualitativ und auch quantitativ Fragen beantworten. Die meisten MVPs damals waren mehr oder weniger fast alles Regulars. Ich vermute jedoch, dass dies heute nicht mehr langt um MVP zu werden.
  • Vor ein paar Jahren hat mich die deutsche Szene dann doch ziemlich gelangweilt und ich war dann ziemlich aktiv in den neuen MSDN HTTP Foren, die allerdings nur in englisch wirklich Sinn machen. Die deutschen Foren benutzt kein Mensch.
    In den englischen Foren war es in den Anfangszeiten spannend und interessant. Ich habe es da sogar bis zum Moderator gebracht.
    Allerdings tummeln sich da nun auch so viele US-MVPs, dass ich mehr und mehr wieder den Spaß an diesen Foren verloren habe.
  • Und seit April 2006 bin ich dann im Deutschen C++ Forum eingestiegen.
    Seit Oktober diesen Jahres bin ich nun auch dort Moderator für das MFC und WinAPI Forum geworden.
  • Um 2006 fing ich auch mit meinem Blog an und war doch sehr erstaunt wie viele Leser ich erreiche.
    Und ich merke, dass auch das von den heutigen MVPs erwartet wird: Öffentlichkeit zu erreichen!
    Nicht wenige der aktuellen MVPs zeichnen sich mehr durch WEB-Seiten und Veröffentlichungen (Bücher) aus, als durch große Präsenz in Foren.   

Wenn ich auf all die Jahre so zurückblicke, dann sehe ich doch etwas wehleidig auf die Hochzeiten von nntp://microsoft.public.de.vc zurück, wo zeitweilig bis zu 5 VC++ MVPs aktiv waren und vermisse einige von ihnen.
Meines Wissens gibt es nur noch 2 „echte“ VC++ MVPs: Jochen Kalmbach und mich.
Hierzu jedoch noch zwei Anmerkungen:

  1. Muss ich hinzufügen: Soweit ich es weiß! Der Titel wird zwar verliehen, aber es steht einem ernannten MVP frei den Titel anzunehmen aber nicht öffentlich zu machen. So ist es möglich, dass es weitere MVPs gibt, die wir anderen MVPs gar nicht kennen. Oft kommen diese MVPs auch nicht zu treffen oder Konferenzen und somit bleibt die echte Anzahl von VC++ MVPs in Deutschland ein „kleines“ Geheimnis.
  2. Den Zusatz „Echte“ MVPs habe ich gewählt, weil die Windows SDK und TAPI MVPs mit uns VC++ MVPs zusammengelegt worden sind. Die Produktgruppe Windows SDK hat nun keine eigenen „speziellen“ MVPs mehr. Auch ein komisches und für mich nicht nachvollziehbares Vorgehen von Microsoft.
  3. Ich selbst kann nur Vermutungen über den Nominierungsprozess anstellen und so sind auch meine Äußerungen hier bitte zu werten. Es gibt sicherlich Empfehlungen für neue MVPs. Aber was genau notwendig ist um MVP zu werden weiß kein MVP (zumindest nicht ich). Genaues wissen da nur unsere MVP-Leads.

VS Tipps & Tricks: WinMerge als DIFF-Tool in Verbindung mit TFS

Ich bin ein wirklicher Fan von WinMerge und deshalb nutze ich es auch als Ersatz für das Standard TFS Tool DiffMerge.exe. Wer noch nicht wusste, dass man hier ein eigenes Tool im TFS für Compare und Merge wählen kann, der findet die entsprechenden Einstellungen unter: Tools -> Options -> Source Control -> Visual Studio Team Foundation Server -> Configure User Tools

Um WinMerge für den Compare einzusetzen verwende ich die folgenden Parameter:

Program: C:\Programme\WinMerge\WinMergeU.exe
Arguments: /x /e /wl /ub /dl %6 /dr %7 %1 %2

Und folgende Parameter habe ich für den Merge eingestellt:

Program: C:\Programme\WinMerge\WinMergeU.exe
Arguments: /x /e /wl /ub /dl %6 /dr %7 %1 %2 %4

Hier eine Erklärung aus der Onlinehilfe von WinMerge für Befehlszeilenschalter, die ich aktuell verwende:

  • /x closes WinMerge if opened files are identical (after information dialog is shown). This parameter is useful when WinMerge is used as an external compare application. It helps to faster process and/or ignore files which don’t have any differences. Note that this option does not apply when files become identical when merging/editing them.
  • /e allows WinMerge to be closed with a single Esc keypress. This is useful when using WinMerge as an external compare application. WinMerge can act like an dialog which is easy and fast to close.
  • /wl initially opens left side as read-only. Use this when you don’t want to change left-side items in compare.
  • /ub tells WinMerge to not add both paths to MRU. External applications should not add paths to Open-dialog’s MRU lists.
  • /dl adds a description for left side shown instead of folder / filename. This allows showing version number or label for compared items. Like „Version 1.0“ or „Work Copy“.
  • /dr adds a description for right side shown instead of folder / filename. This allows showing version number or label for compared items. Like „Version 1.0“ or „Work Copy“.

Die entsprechenden % Platzhalter sind so durch Visual Studio vorbelegt:

  • %1 = Original file
  • %2 = Modified file
  • %3 = Base file
  • %4 = Merged file
  • %5 = Diff options
  • %6 = Original file label (label for %1 file)
  • %7 = Modified file label (label for %2 file)
  • %8 = Base file label (label for %3 file)
  • %9 = Merged file label (label for %4 file)

Leider muss man für jeden Datentyp sowohl Compare als auch Merge einzeln einstellen. Eine Default-Einstellung habe ich nicht gefunden.

Es gibt einen schönen Blogeintrag von James Manning, der auch noch andere Diff-Tools behandelt:
http://blogs.msdn.com/jmanning/articles/535573.aspx
Wie man sieht habe ich die WinMerge Parameter jedoch angepasst.

Wie finde ich alte Artikel im Netz?

Microsoft ist dazu übergangen Artikel, die älter als 8 Jahre sind aus dem MSDN heraus zu nehmen. Leider! Denn viele wertvolle Windows API Artikel gehen damit verloren oder sind nicht mehr verfügbar, so zum Beispiel ist auch die OLE Bibel von Kraig Brockschmidt.

Besonders peinlich finde ich es, wenn Microsoft Artikel entfernt auf die in der KB noch Bezug genommen wird, wie zum Beispiel in diesem KB Artikel INFO: Syntax of the Res: Protocol and Some Known Related Issues.
Mir ging es um den Artikel http://www.microsoft.com/mind/0199/cutting/cutting0199.asp, dessen Link leider ins Leere verläuft.

Bleibt die Frage wie man solche alte Artikel wiederfindet, die noch von anderer Seite her referenziert werden ❓

Sofern man (wie hier) die komplette alte URL noch kennt gibt es zum Glück http://web.archive.org/   ❗

Einfach im Advanced Search die URL angegeben http://www.microsoft.com/mind/0199/cutting/cutting0199.asp

Und siehe da man erhält:
http://web.archive.org/web/*/http://www.microsoft.com/mind/0199/cutting/cutting0199.asp
Wie man sieht ist der Artikel Ende 2007 zuletzt auf den MS Seiten zu sehen gewesen.

Bleibt nur noch die Frage, wie man an die alten Links kommt ❓

Es gibt einen schnellen zweiten Weg, wenn der Artikel vor 2001 bereits veröffentlicht war. Google war so nett und hat uns Google 2001 bereitgestellt ❗

Die Suche ergab hier schnell das folgende Ergebnis:
http://www.google.com/search2001/search?hl=en&q=%22Pluggable+Protocols%22+res

Und siehe da, wir erhalten zusätzlich wieder einen Link auf das bereits erwähnte http://web.archive.org :
http://web.archive.org/web/2001/http://msdn.microsoft.com/library/periodic/period99/cutting0199.htm

Google 2001 kann hier also auch ein richtig gutes Helferlein sein um verschollene Informationen und Artikel wieder aufzufinden.