使用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文件。


参考资料:
http://www.codeproject.com/KB/threads/StackWalker.aspx
http://www.cnblogs.com/protalfox/articles/84723.html
http://www.codeproject.com/KB/debug/XCrashReportPt1.aspx
http://www.codeproject.com/KB/applications/visualleakdetector.aspx

ps,本文技术浅尝辄止,部分内容是否完全准确(正确)我个人都持保留态度,仅供参考。:

你可能感兴趣的:(help)