dump call stack 的实现

   闲来无事作点翻译工作,今天要介绍的是关于错误处理的.以下内容大部分不是我的原创,我只是把他们收集到一起来了而已.

  错误处理在一个系统里面算是一个比较底层的东西了.拥有一个稳定的错误处理系统,是一个良好的系统的基础.从发展的角度看,错误处理大体有下面几种方式.

  比较基础的,使用返回值表示错误还是正确,比如使用int作为返回值,0表示正常1表示错误,这种算是c语言里面的办法了,比如windows的api都是使用这种方式进行错误信息的传递.很明显有的时候光是一个32位的值实在表示不了太复杂的东西,这个时候各个有各个的实现方法,比如windows使用的GetLastError函数.com也使用这种方式,大部分的函数都返回一个HRESULT,他目前是一个typedef,其实是一个32位的long,这个long被分成了好几个部分,每个部分表示不同的意义,这个可以在msdn或者是windows的头文件里面找到解释.这种方式是比较明显的错误传递方式.但是缺点也是很显然的.因为返回值用来传递错误信息,所以函数本身的信息返回就要使用其他的方式,c语言里面只能使用传地址的方式了,这个在windows的api里面也经常看到,另外,错误信息是考返回值传递的,所以错误的检查必须要调用者来完成,就得写下比if-else这样的测试语句,而且如果露掉了这样的语句就很可能发生想不到的事情,而且必须是层层返回错误信息,这样的方式不仅仅在程序实现本身上面,而且在整个代码的可读性上面都有很大损失.但是在c语言里面,这也是没有办法的办法.windows对这个问题也提供了一种解决方案,seh--结构化异常处理,说他能完全的处理错误也不尽然,他面向的不是程序语义方面的错误,而是程序的bug,比如说,我的一段程序要打开一个文件,但是这个文件由于某些原因损坏了,这个属于程序本身应该发现纠正的错误,而不是windows来完成的任务.windows只是捕获那些诸如内存访问非法,除0一类的错误,(当然你可以自己调用RaiseException来达到同样的目的).seh看起来像下面的样子:
  __try
  {
    int *p = 0;
    *p =0;
  }
  __except(EXCEPTION_EXECUTE_HANDLER)
  {
  }
  不要和c++的异常混淆了,windows提供的seh是一种操作系统层面的机制,而c++的异常却是语言层面的机制,虽然马上就能看到c++的异常和windows的seh有某些关系(指msvc实现的c++异常).

  windows的seh实现上面的细节可以参考msj的under the hood的一篇叫A Crash Course on the Depths of Win32 Structured Exception Handling.的文章,上面对windows的seh有比较详细的讲解,简单的说就是windows在每个线程的tib(thread information block)里面保存了一个链表,这个链表里面放了些发生异常的时候windows要调用的应用程序注册的回调函数,当异常发生的时候windows从链表的开头调用那些回调函数,回调函数返回适当的值表示自己是否处理了这个异常,如果没有,则windows移动的下一个链表节点,如果到了链表的结尾都没有人能处理这个异常,windows会转到一个默认的函数,这个函数就会在屏幕上面显示一个大家都应该见过的筐---应用程序发生了一个错误,将要关闭,同时有一个详细信息的按钮.

  在c++里面可能就不太会使用到这种返回值的方式了,我个人认为c++的程序员优先考虑的应该是异常,虽然很多人很排斥这个新的东西,c++的异常机制把程序员从小心检查返回值的地方解救出来,你再不用去检查函数的返回值(在以前是必须的,不管你关心不关系你调用的函数的返回值,你都必须要去检查,因为你有责任把这个返回值返回给调用你的人),在c++的异常机制的帮助下,你可以随意的写代码,而不用去管函数的返回值,所以的错误都应该被最能处理的人处理,那些不想处理错误也不能处理的函数就能当错误不存在一样.就像下面的代码段

  void AFunctionMayMeetSomeError()
  {
    //....
    // 错误发生了
    throw exception("meet an error");
  }

  void AFunctionDoNotCare()
  {
    //....
    AFunctionMayMeetSomeError();
    //....
  }

  void AFunctionWouldDealWithTheError()
  {
    //....
    try
    {
      //...
      AFunctionDoNotCare();
    }
    catch(exception& e)
    {
      // deal with the error
    }
  }
  第一个函数可能会产生一个错误,而第二函数会调用第一个函数,但是他去不想去处理这个错误(也许是程序本身的意图,也许是他不知道怎么去处理这个错误),而第3个函数才是真正的错误处理函数,他建立一个try-catch的结构来捕获这个错误.这样能省下很多的代码,而且代码在可读性上面还比较不错.

  利用try-catch结构能比较大的简化错误的处理的方式,我个人认为应该是很有用的东西,不过使用try-catch会带来额外的开销,这个开销主要是体现在代码的长度加大,运行的速度都没有什么太大的影响(这个可以从编译器的实现代码上面看出来,但是很多反对异常的人都任务他会降低运行速度.呵呵)

  作为c++的程序员,现在我们有了一个比较有力的错误处理工具,现在问题又来了,对于程序预料中的异常,是比较能处理的,对于那些程序中预料不到的异常,我们希望获得更加详细的信息,比如函数的调用堆栈,位于源代码的文件行数等等,更甚至,我们想知道当异常发生的时候,我们的程序的具体信息,局部变量,全局变量的值,然后我们可能对此产生一个crash.log文件,要用户返回这个log文件我们加以分析查找bug等等.这个时候c++的异常能作的事情就非常的少了.像源代码文件名行数这些信息我们还可以利用__FILE__,__LINE__,__FUNCTION__这样的编译的宏来获取到,但是其他的就不太可能依赖c++语言本身的东西了,这个时候你也许就要求助于seh,因为windows在异常发生的时候会准备足够的信息,然后调用我们注册的异常处理函数,在这些信息里面,你就能找到你想要的东西.
  这里才是我要介绍的内容的关键,上面的...嘿嘿...
  首先看看我们怎么不依赖其他的特性实现获取源代码的文件行函数的功能
  我们定义如下的异常类
  class CException
  {
    std::string m_strFile;
    std::string m_strFunction;
    std::string m_strDes;
    int m_nLine;
  public:
    CException(std::string const & strFile,std::string const &strFunc,int nLine,std::string strDes) : m_strFile(strFile),m_strFunction(strFunction),m_nLine(nLine),m_strDes(strDes){}

    LPCTSTR what() const
    {
      //返回你需要的错误信息
    }
  };

  然后我们定义下面的宏
  #define ThrowException(x) throw CException(__FILE__,__FUNCTION__,__LINE__,x)
  当然还要对__FUNCTION__宏作点修饰,因为这个宏只是在函数里面才起作用
  #ifndef __FUNCTION__
    #define __FUNCTION__ "Global"
  #endif

  这样我们的异常类里面就包含我们要的文件名,函数名,源代码行的信息了,使用vc的时候你还能使用一点小技巧,如果你把这个异常信息输出到vc的debug的output窗口的时候,能双击定位到发生异常的地方,就像你在编译的时候出的错误一样,双击就能定位到错误位置,方法很简单,你使用 "文件名字(行号)"的格式输出就ok了.

  但是我们并不满足这么一点小小的提示信息,我们需要更多的信息.这个时候,我们得借助windows的seh了.在异常发生的时候windows会调用到你设置的回调函数(这个函数并不是你自己设置的,而是编译器完成的,vc编译c++的try结构的时候设置的函数名字叫__CxxFrameHandler,编译__try结构的时候设置的是_exception_handle3),而c++的异常已经江朗才尽了,我们看看__try结构的时候,编译器都干了什么,编译器会执行我们写在__except后面括号里面的内容,在这个括号里面我们可以调用2个函数GetExceptionInformation()和GetExceptionCode(),这个两个函数能返回我们要的信息,注意,这两个函数只能在__except后面的括号里面调用.在这个括号里面还可以调用我们自己的函数,上面两个函数的返回值是能当作参数传递的,很明显,我们利用这个性质就能作很多的事情了.必须注意我们的函数必须要返回几个固定的值,来告诉windows这个异常我们处理还是不处理,我们建立下面的结构

  __try
  {
    //....
  }
  __except(CrashFilter(GetExceptionInformation(),GetExceptionCode()))
  {
  }

  真正作事情的是CrashFilter函数,这个函数里面我们就能为所欲为了.
  首先GetExceptionInformation()返回一个结构EXCEPTION_POINTERS的指针,他又包含两个成员,一个是PEXCEPTION_RECORD他是一个指针,记录作异常的基本情况,PCONTEXT也是一个指针,记录了异常发生的时候当时线程的所有寄存器的值(我们要dump全部寄存器的任务就落到他头上了),有了这两个东西,我们就能完成很多的事情了

  从CONTEXT里面获取到eip,从而定位到发生异常的模块(使用VirtualQuery先获取到这个内存地址(eip)所位于的内存块,然后利用这个内存块的起始地址调用GetModuleFileName就能获取到模块的名字,这个方面可以参考其他很多的例子,到google上面搜索怎样获取内存里的模块列表就能找到详细的方法).有了eip,我们还能读取到异常指令的内容,直接使用eip的值读就ok(因为windows使用的是flat地址模式),然后利用异常代码(可以从EXCEPTION_POINTERS里面获取也可以利用GetExceptionCode()来得到)来获取异常的信息,这个信息大部分能从msdn里面查找到,其他的可以在ntdll.dll里面去获取调(调用FormatMessage函数,指定ntdll.dll的模块句柄),然后也许你要收集目标计算机的cpu类型,内存状态,操作系统信息等等(这些能通过GetSystemInfo,GlobalMemoryStatus,GetVersionEx函数来获取,这些都能在google上面搜索到详细的方法).然后你可能会dump堆栈,这个时候有个小技巧了,win32下面线程的TIB总是放到fs指定的段里面而fs:[4]这个地方放的就是栈的top地址,而当前栈的地址在CONTEXT里面有记录,你要作的就是把context的esp指针到fs:[4]之间的内存全部dump出来就ok.然后也许你要列出当前进程里面的全部dll名字,和dll的信息,这个也落在VirtualQuery函数上面,基本的方法就是遍历4G的虚拟地址空间,反复的调用VirtualQuery函数,一旦发现是合法的内存地址空间就调用GetModuleFileName函数如果成功了就表示是一个dll,这个时候你就能获取到dll的dos文件头,进一步获取到nt文件头,接着获取到dll的全部...你要知道就是一个dll和exe的module handle其实是dll和exe文件在内存里面的开始的地址,而从这个地址开始的就dll和exe文件的dos文件头.

  有了这些东西其实也很无趣,你dump出来的东西要么用处不大,要么就是实在没有办法读取的信息,那些16进制的stack内容实在用处不大.接下来的东西就有点激动人心了,我们要dump出异常发生的时候函数的调用堆栈,dump出函数的局部变量,全局变量的值.

  这个艰巨的任务就落到了windows的debug api上面了,windows在nt以后的版本发行了一个叫dbghelp.dll的文件,这个文件就能完成我们要求的内容.具体的信息可以查看msdn,我下面要说的内容也能在msj里面的Under the Hood: Improved Error Reporting with DBGHELP 5.1一文中找到.注意要完成下面的内容你必须要有exe文件或者dll文件的pdb文件.vc会帮您产生这个文件的,他就是调试用的符号文件.

  关键部分在于5.1里面的几个新的函数,我们就能获取到这些想要的东西.这个文章不是讲解怎么使用dbghelp的文章,所以我跳过了他的使用方法.具体的可以到google上面搜索,或者查看msdn.

  想要dump出call stack,必然要查看stack,这个任务交给dbghelp lib的StackWalk函数完成,你要准备一个STACKFRAME结构,传递给StackWalk函数,其他的几个参数都是很容易获取到的,机器类型,进程句柄,线程句柄,context(刚刚的那个context就能拿来使用),两个回调函数(不用自己实现,使用dbghelp lib自己实现好的函数),然后StackWalk就填充好你传递的STACKFRAME结构,接下来你就利用这个结构调用SymFromAddr这个函数就能获取到当前栈位置的函数名字,同时还有当前pc位置相对于函数开始代码pc的偏移量.调用SymGetLineFromAddr函数获取源代码文件名和行的信息.这样就能完成call stack的处理过程,
  STACKFRAME sf;
  memset(&sf,0,sizeof(sf));

  // 初始化stackframe结构
  sf.AddrPC.Offset = pContext->Eip;
  sf.AddrPC.Mode = AddrModeFlat;
  sf.AddrStack.Offset = pContext->Esp;
  sf.AddrStack.Mode = AddrModeFlat;
  sf.AddrFrame.Offset = pContext->Ebp;
  sf.AddrFrame.Mode = AddrModeFlat;
  dwMachineType = IMAGE_FILE_MACHINE_I386;
  while(1)
  {
    // 获取下一个栈帧
    if(!StackWalk(dwMachineType,hProcess,GetCurrentThread(),&sf,pContext,0,SymFunctionTableAccess,SymGetModuleBase,0))
      break;

    // 检查帧的正确性
    if(0 == sf.AddrFrame.Offset)
      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,&symDisplacement,pSymbol))
    {
      WriteLog(hFile,TEXT("%4d Function : %hs"),i,pSymbol->Name);
    }
 }
  如此就能完成call stack的dump工作.
  接下来的事情就是显示变量的问题了
  这个可以在dump call stack的时候同步完成
  首先使用SymSetContext函数设置你的dump环境,这个很重要,因为局部变量都是有自己的生存环境的,他们都有自己的context,你在dump他们的时候必须要先设置这个context.这个也是很容易完成的.

  IMAGEHLP_STACK_FRAME imagehlpStackFrame;
  imagehlpStackFrame.InstructionOffset = sf.AddrPC.Offset;
  SymSetContext(hProcess,&imagehlpStackFrame, 0 );

  唯一你要设置的就是那个地址,简单的传递刚刚的stack frame的pc的offset就ok.切记这个值的不同,你获取的信息就可能不同.
  接下来调用SymEnumSymbols函数枚举全部的变量.他需要你提供一个回调函数,很显然,全部的工作都在一个函数里面完成.在枚举全局变量的使用也调用这个函数,唯一不同的时候全局函数不需要指定context.
  当dbghelp枚举到一个变量的时候,他就会准备好这个变量的基本信息,然后调用你的回调函数你的函数看起来像这个样子

  BOOL CALLBACK EnumerateSymbolsCallback(PSYMBOL_INFO pSymInfo,ULONG SymbolSize,PVOID UserContext)

  第一个就是符号的信息,你利用这个信息来获取你要要的结构,第二个是大小,基本可以忽略,最后一个是符号的context,紧记局部变量都是context向关的,都是使用[ebp-??]这样的来访问的.
  我们要作的事情就是利用info和context产生合适的输出
  首先判断这个符号的类型(info->Flags),我们只是跳过函数符号,而留下变量符号,接着判断符号的寻址方式(相对ebp寻址?绝对地址寻址?还是放到cpu的寄存器里面的?这个也是在那个Flags里面获取的).接下来我们就要判断这个符号的具体信息了,使用TI_GET_SYMNAME标志调用SymGetTypeInfo函数能获取到这个符号的名字(也就是变量的名字),他要求的参数都能在info里面找到.然后使用TI_GET_CHILDRENCOUNT再调用SymGetTypeInfo函数,获取符号的child的个数(复杂的c的结构有很多的子成员),如果他的child数目是0,就表示这个变量是一个基本变量(int形的?float形的?char形的?都属于这种基本变量),这个时候我们就能使用TI_GET_BASETYPE再调用SymGetTypeInfo函数就能获取到这个的基本类型了,然后你就能获得到这个变量的类型,配合上面的寻址方式,你就能在内存里面读取出他的值来.如果他的child数目不是0,这个时候你对每一个child重复递归的调用上面的步骤,最终会得到一个个的基本类型,然后输出落...

  到这里你已经获得了足够的信息了....整个事情就都完成了.

  嗯,上面的步骤我自己都感觉是自己写个看得懂的人看的-_-@.....
  写得太简陋了,看不懂的人还是一头雾水,看得懂的人就会说---这个找知道了...

  呵呵.要看懂上面的内容呢,你要有基本的汇编知识,要知道c语言编译器是大致上怎么工作的,有了这个基础,再了解一点windows系统的稍微底层一点知识再在msdn的帮助下面就能实现自己的crash dump函数了.

  推荐几个文章

  第一个是来自codeproject上面的一个叫How a C++ compiler implements exception handling的文章

  然后是来自msj的两个under the hood(专栏作者超牛...)
  A Crash Course on the Depths of Win32 Structured Exception Handling

  Improved Error Reporting with DBGHELP 5.1 APIs

  我已经把这些种技术包含到了自己的新工程里面了,一个字---超级爽....

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值