记录程序崩溃时的调用堆栈

最近有个用户遇到程序Crash问题,但我们的机器都不能重现,于是在网上搜了一把,发现有个MSJExceptionHandler类还比较好用,故整理了一下供大家参考。

这个类的使用方法很简单,只要把这个类加入到你的工程(不管是MFC,com,dll都可以)中一起编译就可以了,由于在这个类的实现文件中把定义了一个全局的类对象,所以不用加入任何代码,连#include都不需要。

一、VS2008创建一个基于对话框的工程testCrash

1.首先设置工程为Release编译,将msjexhnd.h和msjexhnd.cpp加入到这个工程

此时编译程序会提示错误fatal error C1010: unexpected end of file while lookingfor precompiled header. Did you forget to add '#include "stdafx.h"'to your source?


2.设置导入的.cpp文件不使用预编译头

工程中选中msjexhnd.cpp右键>属性,在c/c++>PrecompiledHeaders>Create/Use Precompiled Headers选择Not Using PrecompiledHeaders,Ok编译程序,成功。


3.右键工程属性设置生成cod、map文件

c/c++>OutputFiles>Assembler Output,选择Assembly, Machine Code and Source(/FAcs).这个选项将为每个源文件(*.cpp)生成机器码、汇编码和源代码的对应表,可以在“Release”目录下找到和查看这些文件。

Linker>Debugging>Generate Map File,选择Yes(/MAP),这个选项将生成编译后的函数地址和函数名的对应表。


rebuild此工程,可以在生成exe文件的Release目录找到testCrash.map,在生成中间临时文件的Release目录下生成testCrashDlg.cod


4.在工程中加入测试代码,并重新编译程序

void CtestCrashDlg::OnBnClickedOk()
{
	// TODO: Add your control notificationhandler code here
	int *p = NULL;
	*p = 0; //给空指针赋值

	OnOK();
}


二、查找Crash

详细介绍:查找崩溃


1.运行testCrash.exe,点击ok按钮,程序crash

此时会在exe同一目录下生成文件 testCrash.RPT,你可以自己定义此文件位置及名字,具体看MSJExceptionHandler的构造函数。


2.用文本方式打开testCrash.RPT,可以看到这一行

Call stack:
Address     Frame        Logical addr    Module
00401452  0037F888  01:00000452   d:\myown\test\testcrash\release\testCrash.exe

注意01:00000452就是程序崩溃的地址


3.打开testCrash.map,可以找到

0001:00000450      ?OnBnClickedOk@CtestCrashDlg@@QAEXXZ00401450 f  testCrashDlg.obj

0001:00000460      ?Create@CDialog@@UAEHIPAVCWnd@@@Z00401460 f i testCrashDlg.obj
由于崩溃地址是01:00000452,大于0001:00000450,小于0001:00000460,所以可以肯定是CtestCrashDlg::OnBnClickedOk里崩溃。

并且相对地址是00000452-00000450=2(16进制),代码对应在testCrashDlg.cod因为最后面显示的是testCrashDlg.obj


4.打开testCrashDlg.cod,找到

COMDAT  ?OnBnClickedOk@CtestCrashDlg@@QAEXXZ
_TEXT  SEGMENT
?OnBnClickedOk@CtestCrashDlg@@QAEXXZ PROC    ;CtestCrashDlg::OnBnClickedOk, COMDAT
; _this$ = ecx
; 155   :   //TODO: Add your control notification handler code here
; 156   :   int*p=NULL;
   00000  33c0    xor   eax, eax
; 157   :   *p =0;
   00002  8900    mov   DWORD PTR [eax], eax

; 158  : OnOK();


前面带分号的是注释,不带的是汇编代码,汇编代码前面5位数是代码在此函数的相对地址,00002就是偏移2,正是我们要找的崩溃的地方。

上面的一行是注释实际的源代码; 157  : *p = 0;

好,终于找到元凶!

      

测试工程(包含打印类):http://download.csdn.net/detail/qing666888/9718906


在程序release之后,不可避免的会存在一些bug,测试人员和最终用户如何在发现bug之后指导开发人员进行更正呢?在MS的网站上,有一篇名为"Under the hook"的文章,讲述了如何把程序崩溃时的函数调用情况记录为日志的方法,对此感兴趣的读者可以去看一看原文,那里提供源代码和原理的说明。

文章的作者提供了一个MSJExceptionHandler类来实现这一功能,这个类的使用方法很简单,只要把这个类加入到你的工程中并和你的程序一起编译就可以了,由于在这个类的实现文件中把自己定义为一个全局的类对象,所以,不用加入任何代码,#include都不需要。

当程序崩溃时,MSJExceptionHandler就会把崩溃时的堆栈调用情况记录在一个.rpt文件中,软件的测试人员或最终用户只要把这个文件发给你,而你使用记事本打开这个文件就可以查看崩溃原因了。你需要在发行软件的时候,为你的程序生成一个或几个map文件,用于定位出错的文件和函数。(我的另一篇blog中有关于生成map文件和定位错误的详细说明)为了方便使用,这里附上该类的完整代码:


// msjexhnd.h

#ifndef __MSJEXHND_H__
#define __MSJEXHND_H__

class MSJExceptionHandler
{
    public:
   
    MSJExceptionHandler( );
    ~MSJExceptionHandler( );
   
    void SetLogFileName( PTSTR pszLogFileName );

    private:

    // entry point where control comes on an unhandled exception
    static LONG WINAPI MSJUnhandledExceptionFilter(
                                PEXCEPTION_POINTERS pExceptionInfo );

    // where report info is extracted and generated
    static void GenerateExceptionReport( PEXCEPTION_POINTERS pExceptionInfo );

   // Helper functions
    static LPTSTR GetExceptionString( DWORD dwCode );
    static BOOL GetLogicalAddress(  PVOID addr, PTSTR szModule, DWORD len,
                                    DWORD& section, DWORD& offset );
    static void IntelStackWalk( PCONTEXT pContext );
    static int __cdecl _tprintf(const TCHAR * format, ...);

   // Variables used by the class
    static TCHAR m_szLogFileName[MAX_PATH];
    static LPTOP_LEVEL_EXCEPTION_FILTER m_previousFilter;
    static HANDLE m_hReportFile;
};

extern MSJExceptionHandler g_MSJExceptionHandler;  //  global instance of class

#endif

// msjexhnd.cpp

//==========================================
// Matt Pietrek
// Microsoft Systems Journal, April 1997
// FILE: MSJEXHND.CPP
//==========================================
#include< windows.h>
#include< tchar.h>
#include "msjexhnd.h"

//============================== Global Variables =============================

//
// Declare the static variables of the MSJExceptionHandler class
//
TCHAR MSJExceptionHandler::m_szLogFileName[MAX_PATH];
LPTOP_LEVEL_EXCEPTION_FILTER MSJExceptionHandler::m_previousFilter;
HANDLE MSJExceptionHandler::m_hReportFile;


MSJExceptionHandler g_MSJExceptionHandler;  // Declare global instance of class

//============================== Class Methods =============================

//=============
// Constructor
//=============
MSJExceptionHandler::MSJExceptionHandler( )
{
   // Install the unhandled exception filter function
    m_previousFilter = SetUnhandledExceptionFilter(MSJUnhandledExceptionFilter);

   // Figure out what the report file will be named, and store it away
    GetModuleFileName( 0, m_szLogFileName, MAX_PATH );

    // Look for the '.' before the "EXE" extension.  Replace the extension
    // with "RPT"
    PTSTR pszDot = _tcsrchr( m_szLogFileName, _T('.') );
    if ( pszDot )
    {
        pszDot++;   // Advance past the '.'
        if ( _tcslen(pszDot) >= 3 )
            _tcscpy( pszDot, _T("RPT") );   // "RPT" -> "Report"
    }
}

//============
// Destructor
//============
MSJExceptionHandler::~MSJExceptionHandler( )
{
    SetUnhandledExceptionFilter( m_previousFilter );
}

//==============================================================
// Lets user change the name of the report file to be generated
//==============================================================
void MSJExceptionHandler::SetLogFileName( PTSTR pszLogFileName )
{
    _tcscpy( m_szLogFileName, pszLogFileName );
}

//===========================================================
// Entry point where control comes on an unhandled exception
//===========================================================
LONG WINAPI MSJExceptionHandler::MSJUnhandledExceptionFilter(
                                    PEXCEPTION_POINTERS pExceptionInfo )
{
    m_hReportFile = CreateFile( m_szLogFileName,
                                GENERIC_WRITE,
                                0,
                                0,
                                OPEN_ALWAYS,
                                FILE_FLAG_WRITE_THROUGH,
                                0 );

    if ( m_hReportFile )
    {
        SetFilePointer( m_hReportFile, 0, 0, FILE_END );

        GenerateExceptionReport( pExceptionInfo );

        CloseHandle( m_hReportFile );
        m_hReportFile = 0;
    }

    if ( m_previousFilter )
        return m_previousFilter( pExceptionInfo );
    else
        return EXCEPTION_CONTINUE_SEARCH;
}

//===========================================================================
// Open the report file, and write the desired information to it.  Called by
// MSJUnhandledExceptionFilter                                              
//===========================================================================
void MSJExceptionHandler::GenerateExceptionReport(
    PEXCEPTION_POINTERS pExceptionInfo )
{
    // Start out with a banner
    _tprintf( _T("//=====================================================\n") );

    PEXCEPTION_RECORD pExceptionRecord = pExceptionInfo->ExceptionRecord;

   // First print information about the type of fault
    _tprintf(   _T("Exception code: %08X %s\n"),
                pExceptionRecord->ExceptionCode,
                GetExceptionString(pExceptionRecord->ExceptionCode) );

   // Now print information about where the fault occured
    TCHAR szFaultingModule[MAX_PATH];
    DWORD section, offset;
    GetLogicalAddress(  pExceptionRecord->ExceptionAddress,
                        szFaultingModule,
                        sizeof( szFaultingModule ),
                        section, offset );

    _tprintf( _T("Fault address:  %08X %02X:%08X %s\n"),
                pExceptionRecord->ExceptionAddress,
                section, offset, szFaultingModule );

    PCONTEXT pCtx = pExceptionInfo->ContextRecord;

   // Show the registers
    #ifdef _M_IX86  // Intel Only!
    _tprintf( _T("\nRegisters:\n") );

    _tprintf(_T("EAX:%08X\nEBX:%08X\nECX:%08X\nEDX:%08X\nESI:%08X\nEDI:%08X\n"),
            pCtx->Eax, pCtx->Ebx, pCtx->Ecx, pCtx->Edx, pCtx->Esi, pCtx->Edi );

    _tprintf( _T("CS:EIP:%04X:%08X\n"), pCtx->SegCs, pCtx->Eip );
    _tprintf( _T("SS:ESP:%04X:%08X  EBP:%08X\n"),
                pCtx->SegSs, pCtx->Esp, pCtx->Ebp );
    _tprintf( _T("DS:%04X  ES:%04X  FS:%04X  GS:%04X\n"),
                pCtx->SegDs, pCtx->SegEs, pCtx->SegFs, pCtx->SegGs );
    _tprintf( _T("Flags:%08X\n"), pCtx->EFlags );

   // Walk the stack using x86 specific code
    IntelStackWalk( pCtx );

    #endif

    _tprintf( _T("\n") );
}

//======================================================================
// Given an exception code, returns a pointer to a static string with a
// description of the exception                                        
//======================================================================
LPTSTR MSJExceptionHandler::GetExceptionString( DWORD dwCode )
{
    #define EXCEPTION( x ) case EXCEPTION_##x: return _T(#x);

    switch ( dwCode )
    {
        EXCEPTION( ACCESS_VIOLATION )
        EXCEPTION( DATATYPE_MISALIGNMENT )
        EXCEPTION( BREAKPOINT )
        EXCEPTION( SINGLE_STEP )
        EXCEPTION( ARRAY_BOUNDS_EXCEEDED )
        EXCEPTION( FLT_DENORMAL_OPERAND )
        EXCEPTION( FLT_DIVIDE_BY_ZERO )
        EXCEPTION( FLT_INEXACT_RESULT )
        EXCEPTION( FLT_INVALID_OPERATION )
        EXCEPTION( FLT_OVERFLOW )
        EXCEPTION( FLT_STACK_CHECK )
        EXCEPTION( FLT_UNDERFLOW )
        EXCEPTION( INT_DIVIDE_BY_ZERO )
        EXCEPTION( INT_OVERFLOW )
        EXCEPTION( PRIV_INSTRUCTION )
        EXCEPTION( IN_PAGE_ERROR )
        EXCEPTION( ILLEGAL_INSTRUCTION )
        EXCEPTION( NONCONTINUABLE_EXCEPTION )
        EXCEPTION( STACK_OVERFLOW )
        EXCEPTION( INVALID_DISPOSITION )
        EXCEPTION( GUARD_PAGE )
        EXCEPTION( INVALID_HANDLE )
    }

    // If not one of the "known" exceptions, try to get the string
    // from NTDLL.DLL's message table.

    static TCHAR szBuffer[512] = { 0 };

    FormatMessage(  FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_FROM_HMODULE,
                    GetModuleHandle( _T("NTDLL.DLL") ),
                    dwCode, 0, szBuffer, sizeof( szBuffer ), 0 );

    return szBuffer;
}

//==============================================================================
// Given a linear address, locates the module, section, and offset containing 
// that address.                                                              
//                                                                            
// Note: the szModule paramater buffer is an output buffer of length specified
// by the len parameter (in characters!)                                      
//==============================================================================
BOOL MSJExceptionHandler::GetLogicalAddress(
        PVOID addr, PTSTR szModule, DWORD len, DWORD& section, DWORD& offset )
{
    MEMORY_BASIC_INFORMATION mbi;

    if ( !VirtualQuery( addr,& mbi, sizeof(mbi) ) )
        return FALSE;

    DWORD hMod = (DWORD)mbi.AllocationBase;

    if ( !GetModuleFileName( (HMODULE)hMod, szModule, len ) )
        return FALSE;

   // Point to the DOS header in memory
    PIMAGE_DOS_HEADER pDosHdr = (PIMAGE_DOS_HEADER)hMod;

   // From the DOS header, find the NT (PE) header
    PIMAGE_NT_HEADERS pNtHdr = (PIMAGE_NT_HEADERS)(hMod + pDosHdr->e_lfanew);

    PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION( pNtHdr );

    DWORD rva = (DWORD)addr - hMod; // RVA is offset from module load address

    // Iterate through the section table, looking for the one that encompasses
    // the linear address.
    for (   unsigned i = 0;
            i< pNtHdr->FileHeader.NumberOfSections;
            i++, pSection++ )
    {
        DWORD sectionStart = pSection->VirtualAddress;
        DWORD sectionEnd = sectionStart
                    + max(pSection->SizeOfRawData, pSection->Misc.VirtualSize);

       // Is the address in this section???
        if ( (rva >= sectionStart)&& (rva<= sectionEnd) )
        {
            // Yes, address is in the section.  Calculate section and offset,
            // and store in the "section"& "offset" params, which were
            // passed by reference.
            section = i+1;
            offset = rva - sectionStart;
            return TRUE;
        }
    }

    return FALSE;   // Should never get here!
}

//============================================================
// Walks the stack, and writes the results to the report file
//============================================================
void MSJExceptionHandler::IntelStackWalk( PCONTEXT pContext )
{
    _tprintf( _T("\nCall stack:\n") );

    _tprintf( _T("Address   Frame     Logical addr  Module\n") );

    DWORD pc = pContext->Eip;
    PDWORD pFrame, pPrevFrame;
   
    pFrame = (PDWORD)pContext->Ebp;

    do
    {
        TCHAR szModule[MAX_PATH] = _T("");
        DWORD section = 0, offset = 0;

        GetLogicalAddress((PVOID)pc, szModule,sizeof(szModule),section,offset );

        _tprintf( _T("%08X  %08X  %04X:%08X %s\n"),
                    pc, pFrame, section, offset, szModule );

        pc = pFrame[1];

        pPrevFrame = pFrame;

        pFrame = (PDWORD)pFrame[0]; // precede to next higher frame on stack

        if ( (DWORD)pFrame& 3 )    // Frame pointer must be aligned on a
            break;                 // DWORD boundary.  Bail if not so.

        if ( pFrame<= pPrevFrame )
            break;

       // Can two DWORDs be read from the supposed frame address?         
        if ( IsBadWritePtr(pFrame, sizeof(PVOID)*2) )
            break;

    } while ( 1 );
}

//============================================================================
// Helper function that writes to the report file, and allows the user to use
// printf style formating                                                    
//============================================================================
int __cdecl MSJExceptionHandler::_tprintf(const TCHAR * format, ...)
{
    TCHAR szBuff[1024];
    int retValue;
    DWORD cbWritten;
    va_list argptr;
         
    va_start( argptr, format );
    retValue = wvsprintf( szBuff, format, argptr );
    va_end( argptr );

    WriteFile( m_hReportFile, szBuff, retValue * sizeof(TCHAR),& cbWritten, 0 );

    return retValue;
}


你可能感兴趣的:(C/C++,MFC)