Using CrashFinder


As you just saw, reading a MAP or P2M file isn't too terribly difficult. It is rather tedious, however, and certainly not a scalable solution to others on your team, such as quality engineers, technical support staff, and even pointy haired managers. To address the issue of scalability in CrashFinder, I decided to make CrashFinder usable for all members of the development team, from individual developers, through test engineers, and on to the support engineers so that all crash reports include as much information as possible about the crash. If you follow the steps outlined in Chapter 2 for creating the appropriate debug symbols, everyone on your team will be able to use CrashFinder without a problem.

When using CrashFinder in a team setting, you need to be especially vigilant about keeping the binary images and their associated PDB files accessible because CrashFinder doesn't store any information about your application other than the paths to the binary images. CrashFinder stores only the paths to your binary files, so you can use the same CrashFinder project throughout the production cycle. If CrashFinder stored more detailed information about your application, such as symbol tables, you'd probably need to produce a CrashFinder project for each build. If you take this advice and allow easy access to your binaries and PDB files, when your application crashes, all your test or support engineers will have to do is fire up CrashFinder and add a vital piece of information to the bug report. As we all know, the more information an engineer has about the particular problem, the easier correcting the problem will be.

Depending on your application, you might want to have multiple CrashFinder projects for it. For example, you could have one project that points to the daily build location as well as different projects for each milestone release. If you opt to include system DLLs as part of your CrashFinder project, you'll need to create separate CrashFinder projects for each operating system you support. You'll also need to have a CrashFinder project for each version of your application that you send to testers outside your immediate development team, so you'll have to store separate binary images and PDBs for each version you send out.

CrashFinder has been quite a bit of fun to develop. I've been extremely honored by all the people who have told me that they've found it invaluable on their projects and that it's helped them do their jobs better. What's been even cooler is how many smart developers have jumped in with UI and capabilities improvements. The CrashFinder version with this edition had lots of great work added by Scott Bloom, Ching Ming Kwok, Jeff Shanholtz, Rich Peters, Pablo Presedo, Julian Onions, and Ken Gladstone. I want to thank them for the code and for making CrashFinder even better.

Figure 12-4 shows the CrashFinder user interface with one of my personal projects loaded as a project. The top portion of the child window is a tree control that shows the executable and its associated DLLs. The green check marks indicate that the symbols for each of the binary images have been loaded properly. If CrashFinder couldn't load the symbols, a red X would indicate a problem. Additionally, the tree for the problem item would be expanded to show you exactly why CrashFinder could not properly load the binary.

click to expand
Figure 12-4: The CrashFinder user interface

There are three reasons CrashFinder shows the X. The first is that it can't find the PDB file associated with the binary. Your best bet is to always keep the binary and its PDB file together; if you do, you shouldn't have problems. The second reason is because CrashFinder opens a saved project and can no longer find the binary. The last reason is that CrashFinder sees address load conflicts with any of the DLLs in the project. Since the operating system won't allow you to have any conflicting DLLs, CrashFinder won't either. If you do have a load conflict, you can change the conflicting DLL's address just for the current instance of the CrashFinder project. As I've pointed out several times in this book, having your DLLs rebased is vital to your bug hunting success.

The bottom part of the child window is where the magic of turning a mystical address into a source, function, and line takes place. Before I describe that, I need to tell you how to get your binaries into a CrashFinder project. When you click the New button on the toolbar, you are prompted to add a binary image with a common file dialog box. You should add your EXE first, if you have one. You can select multiple binaries with the Add Binary Image dialog box, so you can add your entire project at once.

After selecting Open, lots of action takes place. One of the great enhancements to the new version of CrashFinder is that it automatically hunts down all implicitly loaded DLLs and adds them to the project. If you have explicitly loaded DLLs such as COM objects, click Add Image on the Edit menu to add each of them. CrashFinder will also bring in any additional implicitly linked modules.

When you're adding binary images, keep in mind that CrashFinder will accept only a single EXE for the project. For your applications comprising multiple EXEs, create a separate CrashFinder project for each EXE. Because CrashFinder is a multiple-document interface (MDI) application, you can easily open all the projects for each of your EXEs to locate the crash location. When you add DLLs, CrashFinder checks that there are no load address conflicts with any other DLLs already in the project. If CrashFinder detects a conflict, it will allow you to change the load address for the conflicting DLL just for the current instance of the CrashFinder project. This option is handy when you have a CrashFinder project for a debug build and you accidentally forget to rebase your DLLs.

As your application changes over time, you can remove binary images by selecting the Remove Image command from the Edit menu. At any time, you can also change a binary image's load address through the Image Properties command on the Edit menu. Since CrashFinder will automatically add the system DLLs used by your binaries, as I mentioned in Chapter 2, your symbol server helps you immensely when debugging. Now you have an even better reason for installing a symbol server—CrashFinder can use them, so you can look up crashes even in system modules. If you look at the information displayed for VERSION.DLL, you'll see it's loading the symbols out of my symbol server.

CrashFinder's raison d' tre is to turn a crash address into a source file, function name, and line number. The Hexadecimal Address(es) box in the bottom half of the child window is where you enter the address you want to look up. When you click the Find button, the source file, function name, and line number are displayed in the edit box at the bottom of the window. If you like, you can enter multiple addresses at once, separated by spaces or commas. For example, you can drop in the complete call stack addresses from a Dr. Watson log and find all the sources at once.

By default, CrashFinder doesn't show the function or source displacements. If you would like to see them, you can indicate this in the Options dialog box. The function displacement shows how many code bytes from the start of the function the address is. The source displacement tells you how many code bytes from the start of the closest source line the address is. Remember that many assembly-language instructions can make up a single source line, especially if you use function calls as part of the parameter list. When using CrashFinder, keep in mind that you can't look up an address that isn't a valid instruction address. If you blow out the this pointer, you can cause a crash in an address such as 0x00000001. Fortunately, those types of crashes aren't as prevalent as the usual memory access violation crashes, which you can easily find with CrashFinder.

Implementation Highlights

CrashFinder itself is a straightforward Microsoft Foundation Class (MFC) library application, so most of it should be familiar. I want to point out three key areas and explain their implementation highlights so that you can extend CrashFinder more easily with some of the suggestions I offer in the section "What's Next for CrashFinder?" later in the chapter. The first area is the symbol engine, the second is where the work gets done in CrashFinder, and the last is the data architecture.

CrashFinder uses the DBGHELP.DLL symbol engine introduced in Chapter 4. The only detail of interest is that I need to force the symbol engine to load all source file and line number information by passing the SYMOPT_LOAD_LINES flag to SymSetOptions. The DBGHELP.DLL symbol engine doesn't load source file and line number information by default, so you must explicitly tell the symbol engine to load it.

The second point about CrashFinder's implementation is that all the work is essentially done in the document class, CCrashFinderDoc. It holds the CSymbolEngine class, does all the symbol lookup, and controls the view. The key function, CCrashFinderDoc::LoadAndShowImage, is shown in Listing 12-3. This function is where the binary image is validated and checked against the existing items in the project for load address conflicts, the symbols are loaded, and the image is inserted at the end of the tree. This function is called both when a binary image is added to the project and when the project is opened. By letting CCrashFinderDoc::LoadAndShowImage handle all these chores, I ensure that the core logic for CrashFinder is always in one place and that the project needs to store only the binary image names instead of copies of the symbol table.

Listing 12-3: The CCrashFinderDoc::LoadAndShowImage function

start example
 BOOL CCrashFinderDoc :: LoadAndShowImage ( CBinaryImage * pImage        ,                                            BOOL           bModifiesDoc  ,                                            BOOL           bIgnoreDups   ) {     // Check the assumptions from outside the function.     ASSERT ( this ) ;     ASSERT ( NULL != m_pcTreeControl ) ;         // A string that can be used for any user messages     CString   sMsg                    ;     // The state for the tree graphic     int       iState = STATE_NOTVALID ;     // A Boolean return value holder     BOOL      bRet                    ;         // Make sure the parameter is good.     ASSERT ( NULL != pImage ) ;     if ( NULL == pImage )     {         // Nothing much can happen with a bad pointer.         return ( FALSE ) ;     }         // Check to see whether this image is valid. If it is, make sure     // that it isn't already in the list and that it doesn't have     // a conflicting load address. If it isn't a valid image, I add     // it anyway because it isn't good form just to throw out user     // data. If the image is bad, I just show it with the invalid     // bitmap and don't load it into the symbol engine.     if ( TRUE == pImage->IsValidImage ( ) )     {             //  Here I walk through the items in the data array so that I can         // look for three problem conditions:         // 1. The binary image is already in the list. If so, I can         //    only abort.         // 2. The binary is going to load at an address that's already         //    in the list. If that's the case, I'll display the         //    Properties dialog box for the binary image so that its         //     load address can be changed before adding it to the list.         // 3. The project already includes an EXE image, and pImage is         //     also an executable.             //  I always start out assuming that the data in pImage is valid.         // Call me an optimist!         BOOL bValid = TRUE ;         INT_PTR iCount = m_cDataArray.GetSize ( ) ;         for ( INT_PTR i = 0 ; i < iCount ; i++ )         {             CBinaryImage * pTemp = (CBinaryImage *)m_cDataArray[ i ] ;                 ASSERT ( NULL != pTemp ) ;             if ( NULL == pTemp )             {                 // Not much can happen with a bad pointer!                 return ( FALSE ) ;             }                 // Do these two CString values match?             if ( pImage->GetFullName ( ) == pTemp->GetFullName ( ) )             {                 if ( FALSE == bIgnoreDups )                 {                     // Tell the user!!                     sMsg.FormatMessage ( IDS_DUPLICATEFILE      ,                                          pTemp->GetFullName ( )  ) ;                     AfxMessageBox ( sMsg ) ;                 }                 return ( FALSE ) ;             }                 // If the current image from the data structure isn't             // valid, I'm up a creek. Although I can check             // duplicate names above, it's hard to check load             //  addresses and EXE characteristics. If pTemp isn't valid,             // I have to skip these checks. Skipping them can lead             // to problems, but since pTemp is marked in the list as             // invalid, it's up to the user to reset the properties.             if ( TRUE == pTemp->IsValidImage ( FALSE ) )             {                     // Check that I don't add two EXEs to the project.                 if ( 0 == ( IMAGE_FILE_DLL &                             pTemp->GetCharacteristics ( ) ) )                 {                     if ( 0 == ( IMAGE_FILE_DLL &                                 pImage->GetCharacteristics ( ) ) )                         {                         // Tell the user!!                         sMsg.FormatMessage ( IDS_EXEALREADYINPROJECT ,                                              pImage->GetFullName ( ) ,                                              pTemp- >GetFullName ( )   ) ;                         AfxMessageBox ( sMsg ) ;                         //  Trying to load two images marked as EXEs will                         // automatically have the data thrown out for                         // pImage.                         return ( FALSE ) ;                     }                 }                     // Check for load address conflicts.                 if ( pImage->GetLoadAddress ( ) ==                      pTemp->GetLoadAddress( )      )                 {                     sMsg.FormatMessage ( IDS_DUPLICATELOADADDR      ,                                          pImage->GetFullName ( )    ,                                          pTemp- >GetFullName ( )      ) ;                         if ( IDYES == AfxMessageBox ( sMsg , MB_YESNO ) )                     {                         // The user wants to change the properties by                         // hand.                         pImage->SetProperties ( ) ;                             // Check that the load address really did                         //  change and that it doesn't now conflict with                         // another binary.                         int iIndex ;                         if ( TRUE ==                                 IsConflictingLoadAddress (                                                pImage->GetLoadAddress(),                                                iIndex                  ))                         {                             sMsg.FormatMessage                                           ( IDS_DUPLICATELOADADDRFINAL  ,                                             pImage- >GetFullName ( )    ,                   ((CBinaryImage*)m_cDataArray[iIndex])->GetFullName());                             AfxMessageBox ( sMsg ) ;                                 // The data in pImage isn't valid, so go                             //  ahead and exit the loop.                             bValid = FALSE ;                             break ;                         }                     }                     else                     {                         // The data in pImage isn't valid, so go                         // ahead and exit the loop.                         bValid = FALSE ;                         pImage->SetBinaryError ( eAddressConflict ) ;                         break ;                     }                 }             }         }         if ( TRUE == bValid )         {             // This image is good (at least up to the symbol load).             iState = STATE_VALIDATED ;         }         else         {             iState = STATE_NOTVALID ;         }     }     else     {         // This image isn't valid.         iState = STATE_NOTVALID ;     }         if ( STATE_VALIDATED == iState )     {         bRet = (BOOL)            m_cSymEng.SymLoadModule64 ( NULL                          ,                                        (PWSTR)pImage->                                                GetFullNameString ( ) ,                                        NULL                          ,                                        pImage->GetLoadAddress ( )    ,                                        0                               );         // Watch out. SymLoadModule returns the load address of the         // image, not TRUE.         ASSERT ( FALSE != bRet ) ;         if ( FALSE == bRet )         {             TRACE ( "m_cSymEng.SymLoadModule failed!!\n" ) ;             iState = STATE_NOTVALID ;         }         else         {             CImageHlp_Module cModInfo ;             BOOL bRet =                   m_cSymEng.SymGetModuleInfo64(pImage->GetLoadAddress(),                                                &cModInfo               );             ASSERT ( TRUE == bRet ) ;             if ( TRUE == bRet )             {                 // Check if the symbols type is not SymNone.                 if ( SymNone != cModInfo.SymType )                 {                     iState = STATE_VALIDATED ;                     // Set the image symbol information.                     pImage->SetSymbolInformation ( cModInfo ) ;                 }                 else                 {                     iState = STATE_NOTVALID ;                     // Unload the module. The symbol engine loads a                     // module even without symbols so I need to unload                     // them here. I only want good loads to happen.                     m_cSymEng.SymUnloadModule64(                                               pImage->GetLoadAddress());                     pImage->SetBinaryError ( eNoSymbolsAtAll ) ;                 }             }             else             {                 iState = STATE_NOTVALID ;             }         }     }         // Set the extra data value for pImage to the state of the symbol     // load.     if ( STATE_VALIDATED == iState )     {         pImage->SetExtraData ( TRUE ) ;     }     else     {         pImage->SetExtraData ( FALSE ) ;     }         // Put this item into the array.     m_cDataArray.Add ( pImage ) ;         // Does adding the item modify the document?     if ( TRUE == bModifiesDoc )     {         SetModifiedFlag ( ) ;     }         // Get the image into the tree.     bRet = m_cTreeDisplay.InsertImageInTree ( pImage ,                                               iState  ) ;     ASSERT ( bRet ) ;         // All OK, Jumpmaster!!      return ( bRet ) ; }
end example

The last point I want to mention is about CrashFinder's data architecture. The main data structure is a simple array of CBinaryImage classes. The CBinaryImage class represents a single binary image added to the project and serves up any core information about a single binary—details such as load address, binary properties, and name. When a binary image is added, the document adds CBinaryImage to the main data array and puts the pointer value for it into the tree node's item data slot. When selecting an item in the tree view, the tree view passes the node back to the document so that the document can get CBinaryImage and look up its symbol information.




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

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