loop_in_codes

低调做技术__欢迎移步我的独立博客 codemaro.com 微博 kevinlynx

使用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, 0sizeof( 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 *= 0;
 
*= 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,本文技术浅尝辄止,部分内容是否完全准确(正确)我个人都持保留态度,仅供参考。:D

posted on 2008-03-28 16:37 Kevin Lynx 阅读(12523) 评论(10)  编辑 收藏 引用 所属分类: 通用编程

评论

# re: 使用dbghelp获取调用堆栈--release下的调试方法学 2008-03-29 12:45 Sil

不如用dr.watson  回复  更多评论   

# re: 使用dbghelp获取调用堆栈--release下的调试方法学 2008-03-29 15:00 yafare

一般的做法都是在程序里面设置一下最终异常处理,就是 SetUnhandledExceptionFilter,没有设置 SEH 的异常最终就会转移到这里处理了。

这个函数的参数是个需要自己实现的回调函数,Windows会把 EXCEPTION_POINTERS 作为参数传给这个回调,然后就可以用你文章里面的代码打印相应的信息了。
  回复  更多评论   

# re: 使用dbghelp获取调用堆栈--release下的调试方法学 2008-03-29 22:09 Kevin Lynx

@yafare
也行,不知道这个属于不属于矢量异常  回复  更多评论   

# re: 使用dbghelp获取调用堆栈--release下的调试方法学 2008-03-29 22:40 yafare

@Kevin Lynx

没用过VEH,刚查了下msdn,发现VEH是这样定义的:

[quote]

Vectored exception handlers are not frame-based handlers. Therefore, you can add a handler and ensure that it gets called regardless of where you are in a call frame. The handlers are called in the order that they were added, after the debugger gets a first chance notification, but before frame-based dispatching occurs.


To add a handler, use the AddVectoredExceptionHandler function. The handler is called for all future exceptions for the process. To remove a handler, use the RemoveVectoredExceptionHandler function.

[/quote]

看他的意思就是VEH 优先级高于SEH的,但是必须得显式的用AddVectoredExceptionHandler 来添加。

VEH 的handler,跟 SetUnhandledExceptionFilter 参数是一样的,貌似除了被调用的顺序不同,别的也都差不多。
  回复  更多评论   

# re: 使用dbghelp获取调用堆栈--release下的调试方法学 2008-03-30 21:59 Bugs

有没有办法处理Linux平台的异常处理?研究一下:)  回复  更多评论   

# re: 使用dbghelp获取调用堆栈--release下的调试方法学 2008-04-18 09:45 lbq1221119

Stack Frame 是托管代码里面,给EE提供执行体信息的东西.  回复  更多评论   

# re: 使用dbghelp获取调用堆栈--release下的调试方法学 2008-04-18 09:56 Kevin Lynx

@lbq1221119
native application里也应该有stack frame这个概念吧?  回复  更多评论   

# re: 使用dbghelp获取调用堆栈--release下的调试方法学 2008-04-18 10:34 lbq1221119

@Kevin Lynx
额 这个我倒是不是特别清楚..我一直研究.Net和托管代码的...
对于托管stack的Frame调试的时候看的比较多 呵呵

现在开始研究非托管的

__asm
{
mov [_ebp], ebp
}
也可以这样使用内联汇编来获取当前Frame 的Pointer呢  回复  更多评论   

# re: 使用dbghelp获取调用堆栈--release下的调试方法学 2010-10-20 12:14 Tori

您好啊 我想请问一下 为什么我按照您这里写的 SymGetLineFromAddr读不出东西来???
谢谢。  回复  更多评论   

# re: 使用dbghelp获取调用堆栈--release下的调试方法学 2013-08-10 08:41 王小亮

恩。很不错的。  回复  更多评论   


只有注册用户登录后才能发表评论。
【推荐】超50万行VC++源码: 大型组态工控、电力仿真CAD与GIS源码库
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理