C++语言的运行时环境是基于栈的环境,堆栈跟踪(trace stack)就是程序运行时能够跟踪并打印所调用的函数、变量及返回地址等,C++异常中的堆栈跟踪就是当程序抛出异常时,能够把导致抛出异常的语句所在的文件名和行号打印出来,以及把调用抛出异常的语句的函数以及其它上层函数信息都打印出来。
1. 为什么需要堆栈跟踪
当你在开发程序时,你是否曾遇到过程序运行过程中突然当机,而你不知道哪一行代码出的问题;你是否曾遇到过程序调试过程中突然抛出异常,而你不知道哪一行代码出的问题;你是否曾遇到过当你在单步调试时突然抛出异常而你却忘了单步执行到哪一步时抛出的异常,于是你只好重来一次。Beta程序在客户那里试运行当中,突然当机,而你不能调试,只能依据客户报告的一些信息来找bug,而客户大多不熟悉程序开发,所以他们报告的信息太少使你感觉无从下手、一筹莫展。
如果你碰到过以上情况,你就只好痛苦地一条一条单步执行语句,看抛出异常的语句在哪,检查非法访问内存的语句在哪里,糟糕的是根据海森堡不确定原理,有时当你调试时又不出问题了。所以幸运的话,你能很快就找到bug,不幸的话,几小时或几天都不能找出问题所在,并将成为你的梦魇。我在程序开发过程中,就经常碰到以上这些情况。
众所周知,在程序开发中发现一个bug将比改正这个bug难度大很多。所以如果有一个方法能够在程序出错时把出错信息打印出来,这样将大大方便找到bug,加快程序开发速度,提高程序的质量。这样,当客户报告程序出错时,你只需要客户把日志发送给你,你根据这个日志里的异常堆栈信息就能轻松发现问题所在。
在java中就有堆栈跟踪功能,它能在程序抛出异常时,能够打印出能够把导致抛出异常的语句所在的文件名和行号,C#中也有这个功能。很多人认为用java开发程序比用C++开发程序要快,我认为java有抛出异常时能够跟踪堆栈这个功能是其中的一个重要原因。

2. 如何实现C++异常中的堆栈跟踪
要实现堆栈跟踪,必须依赖于底层机制即操作系统或虚拟平台,java与jvm虚拟平台绑定,C#与.NET虚拟平台绑定,它们都提供了堆栈跟踪的功能,而C++与操作系统或平台无关,所以没有提供这个功能,但是否能够利用操作系统的系统函数实现这个功能呢?下面简要介绍如何在Windows2000下实现C++异常中的堆栈跟踪。
在Windows中,C++异常底层的实现是通过Windows中的结构化异常SEH来实现的,结构化异常包括如除0溢出、非法内存访问、堆栈溢出等,虽然用catch( … )能够捕获结构化异常,但不能知道是哪种结构化异常,所以第一步就是要把结构化异常转化为C++异常,Windows中的_set_se_translator()函数可以实现这个功能。先建立一个转化函数:void _cdecl TranslateSEHtoCE( UINT code, PEXCEPTION_POINTERS pep ) ;在这个转化函数中抛出一个继承C++标准异常的类,如CRecoverableSEHException(可以恢复的结构化异常类)和CUnRecoverableSEHException(不可以恢复的结构化异常类),这两个类继承CSEHException,CSEHException继承标准C++异常的基类exception。然后在main函数开始处调用 _set_se_translator(TranslateSEHtoCE ),这样就可以把结构化异常转换为C++异常。
另外,由于VC中默认new失败时并不抛出异常,所以需要让new失败时抛出异常,这样可以统一处理,可以使用WINDOWS中的_set_new_handler( )转化,让new失败时抛出异常。同上,先建立一个转化函数 int NewHandler( size_t size ),在这个转化函数中抛出C++标准异常的类bad_alloc,在main函数开始处调用 _set_new_handler (NewHandler)。
接着在CSEHException的构造函数中跟踪堆栈,把导致抛出结构化异常的语句所在的文件名和行号打印出来,调用void ShowStack( HANDLE hThread, CONTEXT& c )。ShowStack函数封装了跟踪堆栈所需调用的各种系统API。它的功能就是根据参数c(线程的上下文),得到当前程序的路径,枚举所调用的系统动态连接库,然后按照从里到外的顺序打印出所有执行的函数名及其所在的文件名和行号。
创建自己的异常类使其具有堆栈跟踪的功能,定义自己使用的异常基类如CMyException(当然,如果你愿意,你可以修改其命名),令其继承标准C++异常类domain_error(当然也可以继承exception),然后在CMyException的构造函数中调用void ShowStack( HANDLE hThread, CONTEXT& c ),这样就可以实现堆栈跟踪,其它自定义的异常继承CMyException,就自动获得堆栈跟踪的功能。这样就形成了一个完整的类层次。

exception

logic_error runtime_error   
length_error
    out_of_range          bad_alloc          bad_cast            range_error
     invalid_argument        bad_exception                overflow_error
       domain_error                 ios_base:failure     underflow_error
    
CMyException(自定义异常基类)      CSEHException(结构化异常基类)      
         
                          CRecoverableSEHException   CUnRecoverableSEHException

CSocketException(与socket相关的异常)   
CConfigException(与配置文件相关的异常)
注:CMyException上面的异常类均为标准C++的异常类。
注:以上异常类的基类均为exception。

本人实现的具有堆栈跟踪的C++异常类库和测试程序可以从www.smiling.com.cn中的umlchina小组中下载StackTraceInC.zip文件。
3. 如何使用C++异常中的堆栈跟踪类库
下载的文件包括Exception.h Exception.cpp(具有堆栈跟踪功能的异常类库), main.cpp, Test1.h, Test1.cpp (测试代码)。
让我们先感受一下堆栈跟踪的威力,运行下载的示例程序,将打印出如下结果(因为输出太长,所以只节选了其中一部分)。主程序为:
void main(){

// 在每个线程函数的入口加上以下语句。
// 检查内存泄露。
CWinUtil::vCheckMemoryLeak();
// 使new函数失败时抛出异常。
CWinUtil::vSetThrowNewException();
// 把WINDOWS中的结构化异常转化为C++异常。
CWinUtil::vMapSEHtoCE();
// 初始化。
CWinUtil::vInitStackEnviroment();
try {
// 捕获非法访问内存的结构化异常。
int* pInt;  // 故意不分配内存
*pInt = 5;  // 应该显示出这一行出错。
}
// 捕获可恢复的结构化异常。
catch ( const CRecoverableSEHException &bug ) {
cout << bug.what() << endl;
}
// 捕获不可恢复的结构化异常。
catch ( const CUnRecoverableSEHException &bug ) {
cout << bug.what() << endl;
}
// 捕获标准C++异常。
catch ( const exception& e ) {
cout << e.what() << endl;
}
// 捕获其它不是继承exception的异常。
catch ( ... ) {
cout << " else exception." << endl;
}
try {
// 捕获自定义的异常。
throw CMyException( " my exception" ); // 应该显示出这一行出错。
}
catch ( const CRecoverableSEHException &bug ) {
cout << bug.what() << endl;
}
catch ( const CUnRecoverableSEHException &bug ) {
cout << bug.what() << endl;
}
catch ( const exception& e ) {
cout << e.what() << endl;
}
catch ( ... ) {
cout << " else exception." << endl;
}
try {
// 捕获函数中的异常。
vDivideByZero(); // 应该显示出这个函数抛出的异常。
int i = 1;
}
catch ( const CRecoverableSEHException &bug ) {
cout << bug.what() << endl;
}
catch ( const CUnRecoverableSEHException &bug ) {
cout << bug.what() << endl;
}
catch ( const exception& e ) {
cout << e.what() << endl;
}
catch ( ... ) {
cout << " else exception." << endl;
}
try {
// 捕获另一源文件Test1.cpp中的函数抛出的异常。
vTestVectorThrow();// 应该显示出在这个函数抛出的异常。
int i = 1;
}
catch ( const CRecoverableSEHException &bug ) {
cout << bug.what() << endl;
}
catch ( const CUnRecoverableSEHException &bug ) {
cout << bug.what() << endl;
}
catch ( const exception& e ) {
cout << e.what() << endl;
}
catch ( ... ) {
cout << "else exception." << endl;
}
int i;
cin >> i; // 防止无意中按键使程序退出。
}

 
对于第1个异常输出为:
0 .V 004066d5 0040779f 0012ff70 00000000 _main + 85 bytes
    Sig:  _main
    Decl: _main
    Line: H:\C++ Test\StackWalk\Test\main.cpp(50) + 3 bytes
    Mod:  Test[H:\C++ Test\StackWalk\Test\Debug\Test.exe], base: 0x00400000h
Sym:  type: PDB, file: H:\C++ Test\StackWalk\Test\Debug\Test.exe
从上面第4行可以知道在main.cpp文件的第50行抛出的异常,找到这一行就是*pInt = 5;然后检查上下文,哦,没有分配内存,于是“臭名昭著”的非法内存访问就轻易发现了!Is it powerful?
对于第2个异常输出为:
1 .V 0040683d 0040779f 0012ff70 00000000 _main + 445 bytes
    Sig:  _main
    Decl: _main
    Line: H:\C++ Test\StackWalk\Test\main.cpp(72) + 49 bytes
    Mod:  Test[H:\C++ Test\StackWalk\Test\Debug\Test.exe], base: 0x00400000h
Sym:  type: PDB, file: H:\C++ Test\StackWalk\Test\Debug\Test.exe
从上面第4行可以知道在main.cpp文件的第72行抛出的异常,找到这一行就是throw CMyException( " my exception" ); 哦,是自定义异常。
对于第3个异常输出为:
0 .V 004065f5 0040697c 0012fe98 00000000 void __cdecl vDivideByZero(void) + 37 b
ytes
    Sig:  ?vDivideByZero@@YAXXZ
    Decl: void __cdecl vDivideByZero(void)
    Line: H:\C++ Test\StackWalk\Test\main.cpp(26) + 6 bytes
    Mod:  Test[H:\C++ Test\StackWalk\Test\Debug\Test.exe], base: 0x00400000h
    Sym:  type: PDB, file: H:\C++ Test\StackWalk\Test\Debug\Test.exe

1 .V 0040697c 0040779f 0012ff70 00000000 _main + 764 bytes
    Sig:  _main
    Decl: _main
    Line: H:\C++ Test\StackWalk\Test\main.cpp(100) + 0 bytes
    Mod:  Test[H:\C++ Test\StackWalk\Test\Debug\Test.exe], base: 0x00400000h
    Sym:  type: PDB, file: H:\C++ Test\StackWalk\Test\Debug\Test.exe
从上面第5行可以知道在main.cpp文件的第26行抛出的异常,找到这一行就是int iRet = 5 / iZero; 哦,是除零异常。然后从上面第12行可以知道调用这个函数是在main.cpp文件的第100行,就是vDivideByZero();的下一行(注意因为vDivideByZero();函数已经调用了,所以显示的行数都是它的下一行)。这样,我们就可以知道一个异常发生的完整过程。
对于第4个异常输出为:
0 .V 004070ca 00406ab9 0012fe98 00000000 void __cdecl vTestVectorThrow(void) + 7
4 bytes
    Sig:  ?vTestVectorThrow@@YAXXZ
    Decl: void __cdecl vTestVectorThrow(void)
    Line: h:\c++ test\stackwalk\test\test1.cpp(13) + 10 bytes
    Mod:  Test[H:\C++ Test\StackWalk\Test\Debug\Test.exe], base: 0x00400000h
    Sym:  type: PDB, file: H:\C++ Test\StackWalk\Test\Debug\Test.exe

1 .V 00406ab9 0040779f 0012ff70 00000000 _main + 1081 bytes
    Sig:  _main
    Decl: _main
    Line: H:\C++ Test\StackWalk\Test\main.cpp(118) + 0 bytes
    Mod:  Test[H:\C++ Test\StackWalk\Test\Debug\Test.exe], base: 0x00400000h
    Sym:  type: PDB, file: H:\C++ Test\StackWalk\Test\Debug\Test.exe
从上面第5行可以知道在test1.cpp文件的第13行抛出的异常,找到这一行就是 vectInt[ 3 ] = 100; 检查上下文,发现没有给vectInt分配空间。然后从上面第12行可以知道调用这个函数是在main.cpp文件的第118行,就是vTestVectorThrow();的下一行。
那么如何使用这个类库呢?对于新工程,首先把exception.h和exception.cpp加入工程,你需要把自定义的异常类继承自CMyException,这样自定义的异常类就具有堆栈跟踪功能,其次在每个线程的入口函数加上以下几个函数调用(注意:必须在每个线程的入口都要调用,当然如CWinUtil::vInitStackEnviroment();不需要,只需要在main入口即可,但如果在每个线程的入口都要调用也不会有副作用):
` // 在每个线程函数的入口加上以下语句。
// 检查内存泄露。
CWinUtil::vCheckMemoryLeak();
// 使new函数失败时抛出异常。
CWinUtil::vSetThrowNewException();
// 把WINDOWS中的结构化异常转化为C++异常。
CWinUtil::vMapSEHtoCE();
// 初始化。
CWinUtil::vInitStackEnviroment();
然后如下所示捕获异常:
try {
vTest();// 假设要捕获vTest()函数可能抛出的异常。
}
catch ( const CRecoverableSEHException &bug ) {// 用于捕获可恢复的结构化异常。
cout << bug.what() << endl;
}
catch ( const CUnRecoverableSEHException &bug ) {// 用于捕获不可恢复的结构化异常。
cout << bug.what() << endl;
}
catch ( const exception& e ) {// 用于捕获标准C++异常及其子类。
cout << e.what() << endl;
}
catch ( ... ) { // 用于捕获那些抛出非结构化异常和不是继承exception的异常。
cout << " else exception." << endl;
}
当然你对于结构化异常没有其它特别的处理策略,也可以简化为:
try {
vTest();// 假设要捕获vTest()函数可能抛出的异常。
}
// 用于捕获标准C++异常及其子类。因为结构化异常继承自exception,所以这里也能捕获//   结构化异常。
catch ( const exception& bug ) {
cout << bug.what() << endl;
}

对于已有的工程,首先把exception.h和exception.cpp加入工程,把原来的自定义的异常类继承自CMyException,然后同上的方法捕获异常,每个线程入口增加初始化函数即可,可以与你原来的异常处理完美集成。
对于在MFC中的用法,可以按如下方式捕获异常:
try  {
vTest();
}
catch ( const exception& e ) {
cout << e.what() << endl;
}
// CException是MFC中异常基类,MFC中的异常通常从堆中分配,所以应通过指针捕获,// 而且使用完之后还应该调用delete函数清除内存。
catch ( CException* e ) {
// hadle exception
e->delete();
}
即在MFC异常上加一层捕获标准C++异常和结构化异常以及自定义异常。另外由于MFC中已经自动有了处理内存泄露的机制,所以需要删除exception.h文件的第34行到第40行(有关内存泄露的说明见下面),由于在MFC中每个.cpp文件开始处都要包含stdafx.h,所以还需要在exception.cpp文件开始处加上#include “stdafx.h”,不然会编译不通过。
如果你希望把堆栈信息输出在文件中,以防丢失,可以使用IO重定向功能(有关IO重定向可参考Jim Hyslop and Herb Sutter的文章http://www.cuj.com/experts/1903/hyslop.htm ),即在main()函数开头加入以下语句:
ofstream ofLog( "exception.txt", ios_base::app );
streambuf *outbuf = cout.rdbuf( ofLog.rdbuf() );
    这样,所有输出到console的信息就重定向到exception.txt文件中了。如果你想恢复,则可以加入以下语句:
  // restore the buffers
    cout.rdbuf( outbuf );
对于release版本,如果你运行,你会发现程序不能捕获非法内存访问、除零等结构化异常,这是因为VC在release版默认是同步异常,不捕获结构化异常,只能捕获C++的异常,所以你需要修改编译选项,采用异步异常模型,在project->setting->c/c++->project options框中增加/EHa的编译选项。另外,release版默认不生成调试符号文件,这样你就不能不能打印出抛出异常的代码的行号等信息,所以你需要修改编译配置,方法如下:Project->Settings->c/c++页中的debug info列表选项中选择program database项。这样release版本也能实现堆栈跟踪。当然,这样会使release版本减慢速度,而且还要带一个debug info文件,因为有些bug只有在release版本中才会出现,而且release版是真正给客户使用的,所以必须测试release版,可以考虑release的beta1和beta2版本带这些调试信息,这样的话,因为debug版和release版都测试通过,发行给客户的最终正式版可以通过设置一个宏注释掉这些调试信息,恢复成同步异常模型,即恢复成VC默认的release版配置。
4. 其他需要注意的问题
本类库还有检查内存泄露的功能,只要你在每个.cpp文件的所有#include之后,加上以下语句:
// 以下几行是能够定义到发生内存泄露的代码行。在每个.cpp文件都应该声明。
#include “Exception.h”
#ifdef _DEBUG 
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
然后以debug方式启动程序,当程序正常退出或关闭时(注意不能用stop debug的命令停止,否则将不会打印出内存泄露的信息),在VC的debug窗口将会打印出有可能产生内存泄露的源代码信息,包括文件名和行号。由于MFC程序自动会生成这些代码,所以在MFC程序中不需要手工添加这些代码。例如,当你以debug方式运行下载的测试程序,当程序正常退出后,在debug窗口会显示如下语句:
Detected memory leaks!
Dumping objects ->
H:\C++ Test\StackWalk\Test\main.cpp(88) : {184} normal block at 0x00632D50, 100 bytes long.
 Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD 
Object dump complete.
从上面第3行可以知道在main.cpp文件的第88行产生了内存泄露,找到这一行就是 char* pcLeak = new char[ 100 ]; 检查上下文,发现果然没有释放内存。
当然如同你使用Purify、BoundsChecker等工具检查内存泄露一样,它也会谎报军情,有些不会内存泄露的地方,它也告诉你内存泄露了,尤其当你使用了大量STL类库时,这就需要你细心检查上下文,以确定是否是内存泄露了。
本类库由于使用了一些VC中特有调试符号特性,所以可能不能在其它编译器下通过。另外,本文讨论的堆栈跟踪实现都是基于Windows 2000以上,Win98和Win95将不能输出导致抛出异常的语句所在的文件名和行号。本类库也不能在Unix或Linux下

C++异常中的堆栈跟踪的更多相关文章

  1. 在程序异常中记录堆栈信息(使用ExWatcher)

    在我们编写程序的时候可通过IDE自带的调试环境捕捉到异常(Except)错误,并能查看到相关的信息以便我们修正程序中的问题.但当软件被发布出去后,因为所部署运行的环境与我们的调试环境有很大区别,即使在 ...

  2. 异常 Exception 堆栈跟踪 异常捕获 MD

    Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...

  3. 【转】堆栈跟踪中收到一个UnhandledExceptionFilter调用时,如何查找问题异常堆栈

    定义没有异常处理程序来处理引发的异常时调用UnhandledExceptionFilter函数.函数通常将异常传递到捕获并处理它所尝试的 Ntdll.dll 文件. 在某些情况下,在其中存在的进程内存 ...

  4. “全栈2019”Java异常第十三章:访问异常堆栈跟踪信息

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java异 ...

  5. Xdebug文档(三)堆栈跟踪

    当xdebug激活时,PHP一旦要显示通知.警告或错误时,xdebug 显示堆栈跟踪信息.这个堆栈信息能跟据你的需要来配置显示. Xdebug显示的堆栈跟踪都是以保守数量状态显示信息.因为大量的信息处 ...

  6. 使用StackTrace堆栈跟踪记录详细日志(可获取行号)

    上一篇我们提到使用.NET自带的TraceSource实现简单的日志,具体请看<轻松背后的N+疲惫——系统日志>,这一篇注意想讲的是日志的详细记录,包含请求开始到结束的过程中调用的方法链以 ...

  7. StackTrace堆栈跟踪记录详细日志

    使用StackTrace堆栈跟踪记录详细日志(可获取行号) 2014-04-25 22:30 by 螺丝钉想要螺丝帽, 350 阅读, 3 评论, 收藏, 编辑 上一篇我们提到使用.NET自带的Tra ...

  8. 几个简单的例子让你读懂什么是JAVA的堆栈跟踪

      简单的来说,堆栈跟踪就是我们的程序在抛出异常时使用的方法调用列表. 简单的例子 通过问题中给出的示例,我们可以准确地确定应用程序中抛出异常的位置. 我们来看看堆栈跟踪: Exception in ...

  9. Dynamics AX 2012 R2 堆栈跟踪:不能对客户端调用'unchecked'

    有一个Custom Service一直在正常使用.今天,Reinhard尝试在JOB中以X++代码Debug Custom Service的Method时,收到以下错误提示: 'unchecked' ...

随机推荐

  1. URAL 1792. Hamming Code (枚举)

    1792. Hamming Code Time limit: 1.0 second Memory limit: 64 MB Let us consider four disks intersectin ...

  2. POJ 1287:Networking(最小生成树Kruskal)

    id=1287">Networking Time Limit: 1000MS   Memory Limit: 10000K Total Submissions: 5976   Acce ...

  3. SAP 标准单价、移动单价在 AP 中的影响--(详细)

    今天我将向大家介绍下SAP中两种单价模式在系统中所产生的影响,先主要讲讲在AP中影响,它主要有两个方面产生影响(物料收货migo,发票校验miro). 演示背景(假设以下都为本位币交易): 库存(单价 ...

  4. C#由变量捕获引起对闭包

    C#由变量捕获引起对闭包的思考   前言 偶尔翻翻书籍看看原理性的东西确实有点枯燥,之前有看到园中有位园友说到3-6年工作经验的人应该了解的.NET知识,其中就有一点是关于C#中的闭包,其实早之前在看 ...

  5. 程序启动报错:ORA-12505;PL/SQL却可以登录的解决方法

    一.异常{ ORA-12505, TNS:listener does not currently know of SID given in connect descriptor The Connect ...

  6. http://www.cutt.com/

    简网APP工场-服务介绍 服务介绍

  7. Why Python?

    Python is object-oriented Structure supports such concepts as polymorphism , operation overloading , ...

  8. 推荐国内外优秀+免费CDN加速站点及公共cdn加速库

    -----------------------------------------------------------------免费CDN加速站点 1.CloudFlare CloudFlare可能 ...

  9. Javascript关闭详细说明

    在我的博客:http://blog.csdn.net/u011043843/article/details/26148265中也有对闭包的解释 在javascript中闭包是一个非常不好理解的概念.可 ...

  10. JQuery AJAX Demo

    JQuery AJAX Demo APP发展集团:347072638(HTML5,APP) 1.先看一个JQuery AJAX Demo HTML端: <!DOCTYPE html PUBLIC ...