Using MemDumperValidator

[Previous] [Next]

The MemDumperValidator extension makes memory debugging much easier. By default, the DCRT library reports memory leaks and validates that memory blocks haven't experienced overwrite or underwrite corruption. Both those reports can be very useful, but when the memory leak report looks something like the following, it can get difficult to figure out exactly which type of memory leaked:

 Detected memory leaks Dumping objects -> c:\vc\INCLUDE\crtdbg.h(552) : {596} normal block at 0x008CD5B0,     24 bytes long.  Data: < k w k > 90 6B 8C 00 B0 DD 8C 00 00 00 80 77 90 6B 8C 00 Object dump complete. 

As I mentioned earlier, having more than just the default memory validation helps—having deep memory validation can help you catch some wild writes that you might not otherwise catch. The extra information in MemDumperValidator's leak report and the additional validations the extension supplies provide you with more information when you're debugging. And the more information you have when you debug, the faster you can debug.

The MemDumperValidator extension takes advantage of the DCRT library block identifier capabilities so that it can associate a block type with a set of functions that know something about what the block contains. Each block allocated through the DCRT library is assigned an identifier. The different identifiers are listed in Table 15-3. The block types are a parameter to DCRT library functions that allocate memory: _nh_malloc_dbg (new), _malloc_dbg (malloc), _calloc_dbg (calloc), and _realloc_dbg (realloc).

Table 15-3 Memory Block Identifiers

Block IdentifierDescription
_NORMAL_BLOCKA normal call to new, malloc, or calloc creates a Normal block. Defining _CRTDBG_MAP_ALLOC causes all heap allocation to default to Normal blocks and associates the source file and line number that allocated the block with the memory block.
_CRT_BLOCKThe memory blocks allocated internally by many run-time library functions are marked as CRT blocks so that they can be handled separately. As a result, you're able to exclude them from leak detection and other memory validation operations. Your application must never allocate, reallocate, or free any block of type CRT.
_CLIENT_BLOCKIf you want your application to do special tracking on a type of memory, you can call the debug allocation functions and pass a special Client value as a parameter. You can track subtypes of Client blocks by putting a 16-bit value into the upper 16 bits of the Client block value, as shown here:

#define CLIENT_BLOCK_VALUE(x) \       (_CLIENT_BLOCK|(x<<16))    _heap_alloc_dbg ( 10 ,       CLIENT_BLOCK_VALUE(0xA),                     __FILE__ ,                     __LINE__ ) ; 

The application can supply a hook function, through _CrtSetDumpClient, for dumping memory registered as Client blocks. The hook will be called whenever a DCRT library function needs to dump a Client block. The _CrtDoForAllClientObjects function also allows you to enumerate the Client blocks currently allocated.

MFC uses a Client block identifier for all CObject-derived classes. MemDumperValidator also uses the Client hook.

_FREE_BLOCKCalling a memory deallocation routine normally removes the memory from the debug heap lists. If you set the _CRTDBG_DELAY_FREE_MEM_DF flag with _CrtSetDbgFlag, however, the memory isn't freed but left allocated and filled with 0xDD.
_IGNORE_BLOCKIf you temporarily toggle off DCRT library tracking, any allocations made after the tracking is turned off will be marked as Ignore blocks.

After you set up a class or a C data type to use the MemDumperValidator extension, the DCRT library will call MemDumperValidator when it wants to dump a block. The extension will look at the block value, and if it sees a matching dumping function, it will call the function to dump the memory. The validation portion goes through the same process when called by the DCRT library except that it calls the matching validation function on the memory block.

Describing MemDumperValidator is the easy part—getting it to work is a little more difficult. Listing 15-1 shows MEMDUMPERVALIDATOR.H, which takes care of much of the initialization for you. By including BUGSLAYERUTIL.H in your program, you'll automatically include MEMDUMPERVALIDATOR.H.

Listing 15-1 MEMDUMPERVALIDATOR.H

 /*---------------------------------------------------------------------- "Debugging Applications" (Microsoft Press) Copyright (c) 1997-2000 John Robbins -- All rights reserved. -----------------------------------------------------------------------*/ #ifndef _MEMDUMPERVALIDATOR_H #define _MEMDUMPERVALIDATOR_H // Don't include this file directly; include BUGSLAYER.H // instead. #ifndef _BUGSLAYERUTIL_H #error "Include BUGSLAYERUTIL.H instead of this file directly!" #endif // _BUGSLAYERUTIL_H // Include the header that sets up CRTDBG.H. #include "MSJDBG.h" #ifdef __cplusplus extern "C" { #endif // _ _cplusplus // This library can be used only in _debug builds. #ifdef _DEBUG //////////////////////////////////////////////////////////////////////// // The typedefs for the dumper and validator functions //////////////////////////////////////////////////////////////////////// // The memory dumper function. This function's only parameter is a // pointer to the block of memory. This function can output the memory // data for the block in any way it chooses, but to be consistent, it // should use the same reporting mechanism the rest of the DCRT // library uses. typedef void (*PFNMEMDUMPER)(const void *) ; // The validator function. This function's first parameter is the memory // block to validate, and the second parameter is the context // information passed to the ValidateAllBlocks function. typedef void (*PFNMEMVALIDATOR)(const void * , const void *) ; //////////////////////////////////////////////////////////////////////// // Useful macros //////////////////////////////////////////////////////////////////////// // The macro used to set a Client block subtype value. Using this macro // is the only approved means of setting the value of the dwValue field // in the DVINFO structure below. #define CLIENT_BLOCK_VALUE(x) (_CLIENT_BLOCK|(x<<16)) // A macro to pick out the subtype #define CLIENT_BLOCK_SUBTYPE(x) ((x >> 16) & 0xFFFF) //////////////////////////////////////////////////////////////////////// // The header used to initialize the dumper and validator for a specific // Client block subtype //////////////////////////////////////////////////////////////////////// typedef struct tag_DVINFO { // The subtype value for the Client blocks. This value must be set // with the CLIENT_BLOCK_VALUE macro above. See the AddClientDV // function to find out how to have the extension assign this // number. unsigned long dwValue ; // The pointer to the dumper function PFNMEMDUMPER pfnDump ; // The pointer to the dumper function PFNMEMVALIDATOR pfnValidate ; } DVINFO , * LPDVINFO ; /*---------------------------------------------------------------------- FUNCTION : AddClientDV DISCUSSION : Adds a Client block dumper and validator to the list. If the dwValue field in the DVINFO structure is 0, the next value in the list is assigned. The value returned must always be passed to _malloc_dbg as the subtype value of the Client block. If the subtype value is set with CLIENT_BLOCK_VALUE, a macro can be used for the value passed to _malloc_dbg. Notice that there's no corresponding remove function. Why run the risk of introducing bugs in debugging code? Performance is a nonissue when it comes to finding errors. PARAMETERS : lpDVInfo - The pointer to the DVINFO structure RETURNS : 1 - The client block dumper and validator were properly added. 0 - The client block dumper and validator couldn't be added. ----------------------------------------------------------------------*/ int BUGSUTIL_DLLINTERFACE __stdcall AddClientDV (LPDVINFO lpDVInfo); /*---------------------------------------------------------------------- FUNCTION : ValidateAllBlocks DISCUSSION : Checks all the memory allocated out of the local heap. Also goes through all Client blocks and calls the special validator function for the different subtypes. It's probably best to call this function with the VALIDATEALLBLOCKS macro below. PARAMETERS : pContext - The context information that will be passed to each validator function RETURNS : None ----------------------------------------------------------------------*/ void BUGSUTIL_DLLINTERFACE __stdcall ValidateAllBlocks ( void * pContext ) ; #ifdef __cplusplus //////////////////////////////////////////////////////////////////////// // Helper C++ class macros //////////////////////////////////////////////////////////////////////// // Declare this macro in your class just as you would an MFC macro. #define DECLARE_MEMDEBUG(classname) \ public : \ static DVINFO m_stDVInfo ; \ static void ClassDumper ( const void * pData ) ; \ static void ClassValidator ( const void * pData , \ const void * pContext ) ;\ static void * operator new ( size_t nSize ) \ { \ if ( 0 == m_stDVInfo.dwValue ) \ { \ m_stDVInfo.pfnDump = classname::ClassDumper ; \ m_stDVInfo.pfnValidate = classname::ClassValidator ; \ AddClientDV ( &m_stDVInfo ) ; \ } \ return ( _malloc_dbg ( nSize , \ (int)m_stDVInfo.dwValue , \ __FILE__ , \ __LINE__ ) ) ; \ } \ static void * operator new ( size_t nSize , \ char * lpszFileName , \ int nLine ) \ { \ if ( 0 == m_stDVInfo.dwValue ) \ { \ m_stDVInfo.pfnDump = classname::ClassDumper ; \ m_stDVInfo.pfnValidate = classname::ClassValidator ; \ AddClientDV ( &m_stDVInfo ) ; \ } \ return ( _malloc_dbg ( nSize , \ (int)m_stDVInfo.dwValue , \ lpszFileName , \ nLine ) ) ; \ } \ static void operator delete ( void * pData ) \ { \ _free_dbg ( pData , (int)m_stDVInfo.dwValue ) ; \ } // Declare this macro at the top of your CPP file. #define IMPLEMENT_MEMDEBUG(classname) \ DVINFO classname::m_stDVInfo = { 0 , 0 , 0 } // The macro for memory debugging allocations. If DEBUG_NEW is defined, // it can be used. #ifdef DEBUG_NEW #define MEMDEBUG_NEW DEBUG_NEW #else #define MEMDEBUG_NEW new ( __FILE__ , __LINE__ ) #endif #endif // __cplusplus defined //////////////////////////////////////////////////////////////////////// // Helper C macros //////////////////////////////////////////////////////////////////////// // Use this macro for C-style allocations. The only problem // with C is that you need to drag around a DVINFO structure. #define INITIALIZE_MEMDEBUG(lpDVInfo , pfnD , pfnV ) \ { \ ASSERT ( FALSE == IsBadWritePtr ( lpDVInfo , \ sizeof ( DVINFO ) ) ) ; \ ((LPDVINFO)lpDVInfo)->dwValue = 0 ; \ ((LPDVINFO)lpDVInfo)->pfnDump = pfnD ; \ ((LPDVINFO)lpDVInfo)->pfnValidate = pfnV ; \ AddClientDV ( lpDVInfo ) ; \ } // The macros that map the C-style allocations. It might be easier if // you use macros to wrap these so that you don't have to remember which // DVINFO block value to drag around with each memory usage function. #define MEMDEBUG_MALLOC(lpDVInfo , nSize) \ _malloc_dbg ( nSize , \ ((LPDVINFO)lpDVInfo)->dwValue , \ __FILE__ , \ __LINE__ ) #define MEMDEBUG_REALLOC(lpDVInfo , pBlock , nSize) \ _realloc_dbg( pBlock , \ nSize , \ ((LPDVINFO)lpDVInfo)->dwValue , \ __FILE__ , \ __LINE__ ) #define MEMDEBUG_EXPAND(lpDVInfo , pBlock , nSize ) \ _expand_dbg( pBlock , \ nSize , \ ((LPDVINFO)lpDVInfo)->dwValue , \ __FILE__ , \ __LINE__ ) #define MEMDEBUG_FREE(lpDVInfo , pBlock) \ _free_dbg ( pBlock , \ ((LPDVINFO)lpDVInfo)->dwValue ) #define MEMDEBUG_MSIZE(lpDVInfo , pBlock) \ _msize_dbg ( pBlock , ((LPDVINFO)lpDVInfo)->dwValue ) // Macro to call ValidateAllBlocks #define VALIDATEALLBLOCKS(x) ValidateAllBlocks ( x ) #else // _DEBUG is not defined. #ifdef __cplusplus #define DECLARE_MEMDEBUG(classname) #define IMPLEMENT_MEMDEBUG(classname) #define MEMDEBUG_NEW new #endif // __cplusplus #define INITIALIZE_MEMDEBUG(lpDVInfo , pfnD , pfnV ) #define MEMDEBUG_MALLOC(lpDVInfo , nSize) \ malloc ( nSize ) #define MEMDEBUG_REALLOC(lpDVInfo , pBlock , nSize) \ realloc ( pBlock , nSize ) #define MEMDEBUG_EXPAND(lpDVInfo , pBlock , nSize) \ _expand ( pBlock , nSize ) #define MEMDEBUG_FREE(lpDVInfo , pBlock) \ free ( pBlock ) #define MEMDEBUG_MSIZE(lpDVInfo , pBlock) \ _msize ( pBlock ) #define VALIDATEALLBLOCKS(x) #endif // _DEBUG #ifdef __cplusplus } #endif // __cplusplus #endif // _MEMDUMPERVALIDATOR_H 

Using MemDumperValidator with C++

Fortunately, setting up a C++ class so that MemDumperValidator can handle it is a relatively simple operation. In the declaration of the C++ class, just specify the DECLARE_MEMDEBUG macro with the class name as the parameter. This macro is a little like some of the "magic" MFC macros in that it expands into a couple of data and method declarations. If you're following along in Listing 15-1, you'll notice three inline functions: new, delete, and new with source file and line number information. If any of these three operators are defined in your class, you'll need to extract the code from the extension operators and place it in your class's operators.

In the implementation file for your C++ class, you need to use the IMPLEMENT_MEMDEBUG macro, again with your class name as the parameter. This macro sets up a static variable for your class. The DECLARE_MEMDEBUG and IMPLEMENT_MEMDEBUG macros expand only in debug builds, so they don't need to have conditional compilation used around them.

After you've specified both macros in the correct place, you'll need to implement only the two methods that will do the actual dumping and validating for your class. The prototypes for those methods are shown on the next page. Obviously, you'll want to use conditional compilation around them so that they aren't compiled into release builds.

static void ClassDumper ( const void * pData ) ; static void ClassValidator ( const void * pData,  const void * pContext ) ; 

For both methods, the pData parameter is the memory block that points to an instance of the class. All you need to do to get to a usable pointer is to cast the value in pData to the class type. Whatever you do when you're dumping or validating, treat the value in pData as super read-only or you could easily introduce as many bugs into your code as you meant to eliminate. For the ClassValidator method, the second parameter, pContext, is the context parameter you passed to the original call to the ValidateAllBlocks function. I'll talk more about the ValidateAllBlocks function in the "Deep Validations" section later in this chapter.

I have only two recommendations for implementing the ClassDumper method. First, stick to the _RPTn and _RPTFn macros from the DCRT library so that your formatted output will be dumped in the same place as the rest of the DCRT library output. Second, end your output with a carriage return/line feed combination because the DCRT library macros don't do any formatting for you.

Setting up a dumper and a validator for a C++ class seems almost trivial. But what about those C data structures that you'd like to dump cleanly at long last? Unfortunately, handling them takes a little more work.

Using MemDumperValidator with C

You might be wondering why I even bothered to support C. The answer is simple: there's still quite a bit of C code out there in many products you and I are using. And believe it or not, some of these applications and modules use memory too.

The first step you need to take to use MemDumperValidator in a C application is to declare a DVINFO structure for each different type of memory you want to dump and validate. The C++ macros automatically declare dumper and validator methods for you, but in C, you have to do some work. Keep in mind that all the macros I talk about here require a pointer to the specific DVINFO structures.

The prototypes for the C dumper and validator functions are the same as the C++ methods except that the static keyword isn't used. As with declaring the unique memory block DVINFO structures, for convenience, you might want to consider placing all the C memory dumping and validation function implementations in a combined file.

Before you can start allocating, dumping, or validating memory in a C application, you must tell the MemDumperValidator extension about the Client block subtype and the dumper and validator functions for it. You pass all this information to the MemDumperValidator extension by using the INITIALIZE_MEMDEBUG macro, which takes the assigned DVINFO structure, the dump function, and the validation function as parameters. You'll need to execute the macro before you allocate any memory blocks of this type.

Finally—and this is the area in which C++ memory handling is far better than C memory handling—to allocate, free, reallocate, expand, or get the size of a block, you must use an entire set of macros that pass the block value through to the underlying memory function. For example, if your DVINFO structure is stdvBlockInfo, you need to allocate your C blocks with the following code:

MEMDEBUG_MALLOC ( &stdvBlockInfo , sizeof ( x ) ) ; 

At the bottom of Listing 15-1, you'll see all the different macros for the C memory functions. Remembering the DVINFO structure for every type of allocation isn't impossible but it isn't practical either, so you might want to set up wrapper macros to handle the different DVINFO structures for you; all you'll need to pass to your wrapper macros are the normal parameters to the memory functions.

Deep Validations

Although the dumping portion of the MemDumperValidator extension is unquestionably useful, you might be wondering why you need the validation method, even if it does allow you to do deep validation of the memory block. In many cases, the validation function might even be an empty function if all the class holds is a couple string variables. Even so, the validation function can be invaluable because it gives you some excellent debugging capabilities. One of the reasons I started using deep validation was to provide a second level of data validation on a set of base classes that I had developed. Although a validation function shouldn't replace good old-fashioned parameter and input checking, it can give you another layer of assurance that your data is correct. Deep validations can also be a second line of defense against wild writes.

The neatest use of the validation function is for double-checking complex data structures after operations have been performed on them. For example, one time I had a relatively complex situation in which two separate self-referential data structures both used the same allocated objects because of space considerations. After filling in the data structures with a large set of data, I used the validation function to look at the individual blocks from the heap and check that they were referentially correct. I could've written a bunch of code to walk each data structure, but I knew that any code I wrote would be an open target for bugs. By using the validation function, I could bounce through the allocated blocks using code that I'd already tested, and I could check the data structures from different positions because the memory was in the order of allocation, not in sorted order.

Although setting up allocations is more difficult in C than in C++, using the memory validation function is the same in both languages. All you need to do is call the VALIDATEALLBLOCKS macro. This macro expands in debug builds to a call to the ValidateAllBlocks routine. The parameter required is any value that you want to pass on through to the validation functions that you registered with the library. I've used this parameter in the past to determine the depth of the validation that the function will perform. Keep in mind that ValidateAllBlocks passes this value to every registered validation routine, so you might need to coordinate the values across your team.

To see the MemDumperValidator functions in action, check out the Dump program shown in Listing 15-2. Dump is a stripped-down program that shows what you need in order to use the extension. Although I didn't provide a code example, the MemDumperValidator extension works well with MFC because MFC will call any previously registered client dump hook functions. With MemDumperValidator, you get the best of both worlds!

Listing 15-2 DUMP.CPP

 /*---------------------------------------------------------------------- "Debugging Applications" (Microsoft Press) Copyright (c) 1997-2000 John Robbins -- All rights reserved. ----------------------------------------------------------------------*/ #include <stdio.h> #include <stdlib.h> #include <memory.h> #include <string.h> #include <iostream.h> #include "BugslayerUtil.h" class TestClass { public: TestClass ( void ) { strcpy ( m_szData , "TestClass constructor data!" ) ; } ~TestClass ( void ) { m_szData[ 0 ] = '\0' ; } // The declaration of the memory debugging stuff for C++ classes DECLARE_MEMDEBUG ( TestClass ) ; private : char m_szData[ 100 ] ; } ; // This macro sets up the static DVINFO structure. IMPLEMENT_MEMDEBUG ( TestClass ) ; Listing 15-2DUMP.CPP // The methods you must implement to dump and // validate #ifdef _DEBUG void TestClass::ClassDumper ( const void * pData ) { TestClass * pClass = (TestClass*)pData ; _RPT1 ( _CRT_WARN , " TestClass::ClassDumper : %s\n" , pClass->m_szData ) ; } void TestClass::ClassValidator ( const void * pData , const void * ) { // Validate the data here. TestClass * pClass = (TestClass*)pData ; _RPT1 ( _CRT_WARN , " TestClass::ClassValidator : %s\n" , pClass->m_szData ) ; } #endif typedef struct tag_SimpleStruct { char szName[ 256 ] ; char szRank[ 256 ] ; } SimpleStruct ; // The dumper and validator for simple string data memory void DumperOne ( const void * pData ) { _RPT1 ( _CRT_WARN , " Data is : %s\n" , pData ) ; } void ValidatorOne ( const void * pData , const void * pContext ) { // Validate the string data here. _RPT2 ( _CRT_WARN , " Validator called with : %s : 0x%08X\n" , pData , pContext ) ; } // The dumper and validator for the structure allocations void DumperTwo ( const void * pData ) { _RPT2 ( _CRT_WARN , " Data is Name : %s\n" " Rank : %s\n" , ((SimpleStruct*)pData)->szName , ((SimpleStruct*)pData)->szRank ) ; } void ValidatorTwo ( const void * pData , const void * pContext ) { // Validate any structures here. _RPT2 ( _CRT_WARN , " Validator called with :\n" " Data is Name : %s\n" " Rank : %s\n" , ((SimpleStruct*)pData)->szName , ((SimpleStruct*)pData)->szRank ) ; } // Unfortunately, the C functions need to drag around their own // DVINFO structures. In the real world, you'd define these structures // as extern references and wrap the MEMDEBUG macros with your own // macros. static DVINFO g_dvOne ; static DVINFO g_dvTwo ; void main ( void ) { cout << "At start of main\n" ; // The memory debugging initialization for type one. INITIALIZE_MEMDEBUG ( &g_dvOne , DumperOne , ValidatorOne ) ; // The memory debugging initialization for type two. INITIALIZE_MEMDEBUG ( &g_dvTwo , DumperTwo , ValidatorTwo ) ; // Allocate the class with the MEMDEBUG new. TestClass * pstClass ; //pstClass = MEMDEBUG_NEW TestClass ; pstClass = new TestClass ; // Allocate the two C types. char * p = (char*)MEMDEBUG_MALLOC ( &g_dvOne , 10 ) ; strcpy ( p , "VC VC" ) ; SimpleStruct * pSt = (SimpleStruct*)MEMDEBUG_MALLOC ( &g_dvTwo , sizeof ( SimpleStruct ) ) ; strcpy ( pSt->szName , "Pam" ) ; strcpy ( pSt->szRank , "CINC" ) ; // Validate all the blocks in the list. VALIDATEALLBLOCKS ( NULL ) ; cout << "At end of main\n" ; // Every block will get dumped as part of the memory leak checking. } 



Debugging Applications
Debugging Applications for MicrosoftВ® .NET and Microsoft WindowsВ® (Pro-Developer)
ISBN: 0735615365
EAN: 2147483647
Year: 2000
Pages: 122
Authors: John Robbins

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net