Using CrashFinder

[Previous] [Next]

As you just saw, reading a MAP 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 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 filenames 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.

You'll probably need to have multiple CrashFinder projects for your application. 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.

Figure 8-2 shows the CrashFinder user interface with one of my personal projects loaded as a project. The left portion of the child window is a tree control that shows the executable and its associated DLLs. The check marks indicate that the symbols for each of the binary images have been loaded properly. If CrashFinder couldn't load the symbols, an X would indicate that there was a problem. The right side of the child window is an edit control that lists the symbol information about the currently selected binary image in the tree.

You add a binary image to a CrashFinder project through the Add Image command on the Edit menu. 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 I pointed out in Chapter 2, you should always set the base addresses of all your DLLs.

click to view at full size.

Figure 8-2 The CrashFinder user interface

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. In addition, it's a good idea to add any system DLLs that your project uses so that you can locate the problem when you crash in them. As I mentioned in Chapter 5, having the Windows 2000 debugging symbols installed can sometimes help you immensely when you have to step through the disassembly of a system module. Now you have an even better reason for installing the Windows 2000 debugging symbols—CrashFinder can use them, so you can look up crashes even in system modules.

CrashFinder's raison d'être is to turn a crash address into a function name, source file, and line number. Selecting the Find Crash command from the Edit menu brings up the Find Crash dialog box, shown in Figure 8-3. For each crash address you want to look up, all you need to do is type the hexadecimal address in the edit control and click the Find button.

Figure 8-3 Finding the location of a crash by using CrashFinder

The lower part of the Find Crash dialog box lists all the information about the last address looked up. Most of the fields in the lower part of the dialog box should be self-explanatory. The Fn Displacement field shows how many code bytes from the start of the function the address is. The Source Displacement field 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're programming in C++ and 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 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 8-2. 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 8-2 The CCrashFinderDoc::LoadAndShowImage function

 BOOL CCrashFinderDoc :: LoadAndShowImage ( CBinaryImage * pImage,                                            BOOL           bModifiesDoc ) { // 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 iCount = m_cDataArray.GetSize ( ) ;         for ( int 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 ( ) )             {                 // 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 ;                         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 )     {         // Try to load this image into the symbol engine.         bRet =            m_cSymEng.SymLoadModule(NULL                                ,                                    (PSTR)(LPCSTR)pImage->GetFullName() ,                                    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         {             iState = STATE_VALIDATED ;         }     }     // 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 ( ) ;     }     CCrashFinderApp * pApp = (CCrashFinderApp*)AfxGetApp ( ) ;     ASSERT ( NULL != pApp ) ;     // Put the string into the tree.     HTREEITEM hItem =         m_pcTreeControl->InsertItem ( pApp->ShowFullPaths ( )                                         ? pImage->GetFullName ( )                                         : pImage->GetName ( )       ,                                       iState                        ,                                       iState                         ) ;     ASSERT ( NULL != hItem ) ;     // Put a pointer to the image in the item data. The pointer      // makes it easy to update the module symbol information whenever      // the view changes.     bRet = m_pcTreeControl->SetItemData ( hItem , (DWORD)pImage ) ;     ASSERT ( bRet ) ;     // Force the item to be selected.     bRet = m_pcTreeControl->SelectItem ( hItem ) ;     // All OK, Jumpmaster!     return ( bRet ) ; } 

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 the 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 the CBinaryImage and look up its symbol information.



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