個人檔案Quake3 启示录相片部落格清單更多 工具 說明

部落格


3月6日

使用dbghelp获取调用堆栈--release下的调试方法学

Author : Kevin Lynx

当软件作为release模式被发布给用户时,当程序崩溃时我们很难去查找原因。常见的手法是输出LOG文件,根据LOG文件分析
程序崩溃时的运行情况。我们可以通过SEH来捕获程序错误,然后输出一些有用的信息作为我们分析错误的资料。一般我们需要
输出的信息包括:系统信息、CPU寄存器信息、堆栈信息、调用堆栈等。而调用堆栈则是最有用的部分,它可以直接帮我们定位
到程序崩溃时所处的位置(在何处崩溃)。(codeproject上关于这个专题的常见开场白 = =#)

要获取call stack(所谓的调用堆栈),就需要查看(unwind)stack的内容。We could conceivably attempt to unwind the
stack ourselves using inline assembly. But stack frames can be organized in different ways, depending on compiler
optimizations and calling conventions, so it could become complicated to do it that way.(摘自vld文档)要获取栈的
内容,我们可以自己使用内联汇编获取,但是考虑到兼容性,内联汇编并不是一个好的解决方案。我们可以使用微软的dbghelp
中的StackWalk64来获取栈的内容。

StackWalk64声明如下:
BOOL StackWalk64(
  DWORD MachineType,
  HANDLE hProcess,
  HANDLE hThread,
  LPSTACKFRAME64 StackFrame,
  PVOID ContextRecord,
  PREAD_PROCESS_MEMORY_ROUTINE64 ReadMemoryRoutine,
  PFUNCTION_TABLE_ACCESS_ROUTINE64 FunctionTableAccessRoutine,
  PGET_MODULE_BASE_ROUTINE64 GetModuleBaseRoutine,
  PTRANSLATE_ADDRESS_ROUTINE64 TranslateAddress
);

具体每个参数的含义可以参见MSDN。这里说下ContextRecord参数,该参数指定了CPU各个寄存器的内容。StackFrame指定了stack frame的内容。stack frame是什么,我也不知道。(= =) StackWalk64函数需要用户指定当前frame的地址,以及当前程序的指令地址。这两个信息都被填充进ContextRecord,然后传进StackWalk64函数。

那么如何获取当前的stack frame地址和当前程序指令地址呢?如前所说,你可以使用内联汇编。(对于程序指令地址,因为要获取EIP寄存器的内容,而该寄存器不能被软件访问)也可以使用GetThreadContext一次性获取当前线程当前运行情况下的CPU各个寄存器内容。补充下,当前frame地址被放在EBP寄存器里,当前程序指令地址放在EIP寄存器里。但是,如同MSDN对GetThreadContext函数的说明一样,该函数可能获取到错误的寄存器内容(You cannot get a valid context for a running thread)。

另一种获取Context(包含EBP and EIP)的方法就是使用SEH(结构化异常处理),在__except中使用GetExceptionInformation获取。 GetExceptionInformation 传回一个LPEXCEPTION_POINTERS指针,该指针指向一个EXCEPTION_POINTERS结构,该结构里包含一个Context的指针,即达到目标,可以使用StackWalk函数。

补充一下,你可以直接使用StackWalk函数,StackWalk被define为StackWalk64(windows平台相关)。 unwind栈后,可以进一步获取一个stack frame的内容,例如函数名。这里涉及到SymFromAddr函数,该函数可以根据一个地址返回符号名(函数名)。还有一个有意思的函数:SymGetLineFromAddr,可以获取函数对应的源代码的文件名和行号。

当然,这一切都依赖于VC产生的程序数据库文件(pdb),以及提供以上API函数的dbghelp.dll。

参考一段简单的代码:

#include <windows.h>
#include <stdio.h>
#include <dbghelp.h>

#pragma comment( lib, "dbghelp.lib" )

void dump_callstack( CONTEXT *context )
{
    STACKFRAME sf;
    memset( &sf, 0, sizeof( STACKFRAME ) );

    sf.AddrPC.Offset = context->Eip;
    sf.AddrPC.Mode = AddrModeFlat;
    sf.AddrStack.Offset = context->Esp;
    sf.AddrStack.Mode = AddrModeFlat;
    sf.AddrFrame.Offset = context->Ebp;
    sf.AddrFrame.Mode = AddrModeFlat;

    DWORD machineType = IMAGE_FILE_MACHINE_I386;

    HANDLE hProcess = GetCurrentProcess();
    HANDLE hThread = GetCurrentThread();

    for( ; ; )
    {
        if( !StackWalk(machineType, hProcess, hThread, &sf, context, 0, SymFunctionTableAccess, SymGetModuleBase, 0 ) )
        {
            break;
        }

        if( sf.AddrFrame.Offset == 0 )
        {
            break;
        }
        BYTE symbolBuffer[ sizeof( SYMBOL_INFO ) + 1024 ];
        PSYMBOL_INFO pSymbol = ( PSYMBOL_INFO ) symbolBuffer;

        pSymbol->SizeOfStruct = sizeof( symbolBuffer );
        pSymbol->MaxNameLen = 1024;

        DWORD64 symDisplacement = 0;
        if( SymFromAddr( hProcess, sf.AddrPC.Offset, 0, pSymbol ) )
        {
            printf( "Function : %s\n", pSymbol->Name );
        }
        else
        {
            printf( "SymFromAdd failed!\n" );
        }

        IMAGEHLP_LINE lineInfo = { sizeof(IMAGEHLP_LINE) };
        DWORD dwLineDisplacement;

        if( SymGetLineFromAddr( hProcess, sf.AddrPC.Offset, &dwLineDisplacement, &lineInfo ) )
        {
            printf( "[Source File : %s]\n", lineInfo.FileName );
            printf( "[Source Line : %u]\n", lineInfo.LineNumber );
        }
        else
        {
            printf( "SymGetLineFromAddr failed!\n" );
        }
    }
}

DWORD excep_filter( LPEXCEPTION_POINTERS lpEP )
{
    /**//// init dbghelp.dll
    if( SymInitialize( GetCurrentProcess(), NULL, TRUE ) )
    {
        printf( "Init dbghelp ok.\n" );
    }

    dump_callstack( lpEP->ContextRecord );

    if( SymCleanup( GetCurrentProcess() ) )
    {
        printf( "Cleanup dbghelp ok.\n" );
    }

    return EXCEPTION_EXECUTE_HANDLER;
}

void func1( int i )
{
    int *p = 0;
    *p = i;
}

void func2( int i )
{
    func1( i - 1 );
}

void func3( int i )
{
    func2( i - 1 );
}

void test( int i )
{
    func3( i - 1 );
}

int main()
{
    __try
    {
        test( 10 );
    }
    __except( excep_filter( GetExceptionInformation() ) )
    {
        printf( "Some exception occures.\n" );
    }

    return 0;
}

以上代码在release模式下需要关掉优化,否则调用堆栈显示不正确(某些函数被去掉了?),同时需要pdb文件。当用户使用时可以只抛出挂掉时候的地址,然后开发者通过crashfinder这样的软件,启动拥有pdb的程序

回應

請稍候...
很抱歉,您輸入的回應過長。請縮短您的回應。
您尚未輸入內容,請再試一次。
很抱歉,目前無法新增您的回應,請稍後再試。
若要新增回應,您的父母必須先給您權限。要求權限
您的家長已關閉回應功能。
很抱歉,目前無法刪除您的回應,請稍後再試。
您已超過每日回應上限次數,請於 24 小時後再試一次。
由於系統顯示您可能傳送垃圾郵件給其他使用者,因此您帳號中的回應功能已遭停用。 如果您認為自己帳號遭錯誤停用,請連絡 Windows Live 支援
請完成下列安全檢查,以完成回應。
您輸入的安全檢查字元必須與圖片或音訊中的字元相符。

若要新增回應,請以您的 Windows Live ID 登入 (若您使用 Hotmail、Messenger 或 Xbox LIVE,則您已擁有 Windows Live ID)。登入


沒有 Windows Live ID?註冊

引用通告

此內容的引用通告是:
http://topameng.spaces.live.com/blog/cns!F962D4854A8233D!534.trak
引述這則內容的部落格