调试常用的 __FILE__, __FUNCTION__, __LINE__ 调试常用的 __FILE__, __FUNCTION__, __LINE__ 没想到 VC6 不支持 __FUNCTION__ 所以我写了如下的奇怪代码 //用来记录当前行和当前函数//也可说是记录 堆栈 void log_stack(const char *file, int line, const char * function); //当然还要对 __FUNCTION__ 宏作点修饰,因为这个宏只是在函数里面才起作用 //据说 VC6 也是不支持 __FUNCTION__ 的 #ifndef __FUNCTION__ #define __FUNCTION__ "Global" #endif #define DEBUG_NEW_HOOK #ifdef DEBUG_NEW_HOOK //就是先写跟踪信息再实际调用函数 #define debug_new_check_point(a) log_stack(__FILE__, __LINE__, __FUNCTION__); debug_new_check(a, false) #define debug_new_check_free(a) log_stack(__FILE__, __LINE__, __FUNCTION__); debug_new_check(a, true) //#define printfd2(a,b) printf(a,b) //#define printfd3(a,b,c) printf(a,b,c) //#define printfd4(a,b,c,d) printf(a,b,c,d) #else #endif 背景知识: -------------------------------------------------- 在C语言中以编程的方式获取函数名 http://www.sina.com.cn 2006年07月26日 09:03 天极yesky 作者:谢启东编译 仅仅为了获取函数名,就在函数体中嵌入硬编码的字符串,这种方法单调乏味还易导致错误,不如看一下怎样使用新的C99特性,在程序运行时获取函数名吧。 对象反射库、调试工具及代码分析器,经常会需要在运行时访问函数的名称,直到不久前,唯一能完成此项任务并且可移植的方法,是手工在函数体内嵌入一个带有该函数名的硬编码字符串,不必说,这种方法非常单调无奇,并且容易导致错误。本文将要演示怎样使用新的C99特性,在运行时获取函数名。 那么怎样以编程的方式从当前运行的函数中得到函数名呢? 答案是:使用__FUNCTION__ 及相关宏。 引出问题 通常,在调试中最让人心烦的阶段,是不断地检查是否已调用了特定的函数。对此问题的解决方法,一般是添加一个cout或printf()——如果你使用C语言,如下所示: void myfunc() { cout<<"myfunc()"< } 通常在一个典型的工程中,会包含有数千个函数,要在每个函数中都加入一条这样的输出语句,无疑难过上“蜀山”啊,因此,需要有一种机制,可以自动地完成这项操作。 获取函数名 作为一个C++程序员,可能经常遇到 __TIME__、__FILE__、__DATE__ 这样的宏,它们会在编译时,分别转换为包含编译时间、处理的转换单元名称及当前时间的字符串。 在最新的ISO C标准中,如大家所知的C99,加入了另一个有用的、类似宏的表达式__func__,其会报告未修饰过的(也就是未裁剪过的)、正在被访问的函数名。请注意,__func__不是一个宏,因为预处理器对此函数一无所知;相反,它是作为一个隐式声明的常量字符数组实现的: static const char __func__[] = "function-name"; 在function-name处,为实际的函数名。为激活此特性,某些编译器需要使用特定的编译标志,请查看相应的编译器文档,以获取具体的资料。 有了它,我们可免去大多数通过手工修改,来显示函数名的苦差事,以上的例子可如下所示进行重写: void myfunc() { cout<<"__FUNCTION__"< 官方C99标准为此目的定义的__func__标识符,确实值得大家关注,然而,ISO C++却不完全支持所有的C99扩展,因此,大多数的编译器提供商都使用 __FUNCTION__ 取而代之,而 __FUNCTION__ 通常是一个定义为 __func__ 的宏,之所以使用这个名字,是因为它已受到了大多数的广泛支持。 在Visual Studio 2005中,默认情况下,此特性是激活的,但不能与/EP和/P编译选项同时使用。请注意在IDE环境中,不能识别__func__ ,而要用__FUNCTION__ 代替。 Comeau的用户也应使用 __FUNCTION__ ,而不是 __func__ 。 C++ BuilderX的用户则应使用稍稍不同的名字:__FUNC__ 。 GCC 3.0及更高的版本同时支持 __func__ 和__FUNCTION__ 。 一旦可自动获取当前函数名,你可以定义一个如下所示显示任何函数名的函数: void show_name(const char * name) { cout< void myfunc() { show_name(__FUNCTION__); //输出:myfunc } void foo() { show_name(__FUNCTION__); //输出:foo } 因为 __FUNCTION__ 会在函数大括号开始之后就立即初始化,所以,foo()及myfunc()函数可在参数列表中安全地使用它,而不用担心重载。 签名与修饰名 __FUNCTION__ 特性最初是为C语言设计的,然而,C++程序员也会经常需要有关他们函数的额外信息,在Visual Studio 2005中,还支持另外两种非标准的扩展特性:__FUNCDNAME__ 与 __FUNCSIG__ ,其分别转译为一个函数的修饰名与签名。函数的修饰名非常有用,例如,在你想要检查两个编译器是否共享同样的ABI时,就可派得上用场,另外,它还能帮助你破解那些含义模糊的链接错误,甚至还可用它从一个DLL中调用另一个用C++链接的函数。在下例中,show_name()报告了函数的修饰名: void myfunc() { show_name(__FUNCDNAME__); //输出:?myfunc@@YAXXZ } 一个函数的签名由函数名、参数列表、返回类型、内含的命名空间组成。如果它是一个成员函数,它的类名和const/volatile限定符也将是签名的一部分。以下的代码演示了一个独立的函数与一个const成员函数签名间的不同之处,两个函数的名称、返回类型、参数完全相同: void myfunc() { show_name(__FUNCSIG__); // void __cdecl myfunc(void) } struct S { void myfunc() const { show_name(__FUNCSIG__); //void __thiscall S::myfunc(void) const } };
发表:我是马甲 时间:2007-8-9 13:44 |
||
再转一个超级牛的文章,我都没怎么看懂 :) 留以后研究吧. -------------------------------------------------- http://dev.gameres.com/Program/Other/ErrorDebug.htm -------------------------------------------------- 错误处理 | | 闲来无事作点翻译工作,今天要介绍的是关于错误处理的.以下内容大部分不是我的原创,我只是把他们收集到一起来了而已. 错误处理在一个系统里面算是一个比较底层的东西了.拥有一个稳定的错误处理系统,是一个良好的系统的基础.从发展的角度看,错误处理大体有下面几种方式. 比较基础的,使用返回值表示错误还是正确,比如使用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 我已经把这些种技术包含到了自己的新工程里面了,一个字---超级爽.... |