现在的项目中遇到个大问题,就是有很多BUG,可我找不到原因.当然原因之一是那几十也可能几百万的代码里,居然没有一点说明文档.除了代码,我没任何可以参考的东西.原因之二,就是最近太累,读代码时,大脑根本不听使唤,太累.呵呵,从中我也明白一个道理.大脑是有情绪的,并不是你想做的东西,它就会听你的话去做.也许我还不知道,大脑里有理性的一部分,即知道我们该做什么,但大脑也有感性的一部分,即让它感到累或感到不能动(我认为想不出问题,大脑就是不动了)的时候,它就会拒绝去做.但我的大脑里感性大大的大于理性.所以经常无法做自己想做的事情.言归正传.
总结:为什么程序调试的进展很慢?1,睡的太晚,影响了记忆力和思考能力.2,还不会真正的调试程序.
到底要怎么样去调试程序呢?
在调试过程中经常会遇到这样的问题,变量的值在哪里设置的?一般变量都是在使用前被设置的,但是有时候程序需要读取硬件信息或外存的数据时,可能就会先去读好.但似乎还是很难找从所有代码中去找到答案.还有个问题就是我们找到了,数据是在哪里设置的,但是我们却跟踪不到那个出错的数据,在几万次的循环中,要确定那个导致BUG的数据在哪次出现是不可能的.当然其实我这个问题并没那么复杂,只到第二次就发生了,但是问题还是没解决,因为我是多线程,4个同样的线程再执行着同样一个函数,而且发生BUG时,我不知道是哪个线程出错了,这种多线程的调试问题该怎么样解决呢?DEBUG是可以设置条件的,当某个条件发生时,中断就会发生.
如何设置各种断点,如何打LOG(就是把一些有用的信息加到输出文件中),如何更随心所欲的知道想知道的信息,如何快速的搭建一个测试环境,这都成了现在要解决的东西.
http://blog.csdn.net/rick1126/relatedarticles/2704.aspx(这里好象全是在讲调试的东西).以下是摘要.
1. 利用工具(Parasoft C++ Test)检查你的代码,评估一下自己形成良好的习惯没有。 (这个一般也不会去用,但知道一下有这个软件也好.)
主动调试指在写代码的时候,通过加入适量的调试代码,帮助我们在软件错误发生的时候迅速弹出消息框,告知开发人员错误发生地点,并中止程序。
各种开发语言和开发工具都提供这些调试语句,标准C++提供了assert函数,MFC提供了ASSERT调试宏帮助我们进行主动调试,在实际工作中,建议统一使用MFC的ASSERT调试宏。
2.2.1 参数检查
对于编写的函数,除了明确的指定契约外,在函数开始处应该对传入的参数进行检查,确保非法参数传入时立即报告错误信息。例如:
BOOL GetPathItem ( int i , LPTSTR szItem , int iLen )
{ASSERT ( i > 0 ) ;ASSERT ( NULL != szItem ) ;ASSERT ( ( iLen > 0 ) && ( iLen < MAX_PATH ) ) ;
ASSERT ( FALSE == IsBadWriteStringPtr ( szItem , iLen ) ) ;}
对指针的检查尤其要注意,通常程序员会这样进行检查:
// An example of checking only a part of the error condition
BOOL EnumerateListItems ( PFNELCALLBACK pfnCallback )
{ ASSERT ( NULL != pfnCallback ) ; }
这样的检查只能够排除指针为空的情况,但是如果指针指向的是非法地址,或者指针指向的
对象并不是我们需要的类型,上面的例子就没有办法检查出来,而是统统认为是正确的。完
整的检查应该如下:
// An example of completely checking the error condition
BOOL EnumerateListItems ( PFNELCALLBACK pfnCallback )
{ASSERT ( FALSE == IsBadCodePtr ( pfnCallback ) ) ;}
2.2.2 内部检查
恰当地在代码中使用ASSERT,对bug检测和提高调试效率有极大的帮助,下面举个简单的例子加以说明。
switch( nType ){
case GK_ENTITY_POINT:// do something break;
case GK_ENTITY_PLINE:// do something break;
default: ASSERT( 0 ); }
在上面的例子中,switch语句仅仅处理了GK_ENTITY_POINT和GK_ENTITY_PLINE两种情况,应该是系统中当时只需要处理这两种情况,但是如果后期系统需要处理更多的情况,而此时上面这部分代码又没有及时更新,或者是因为开发人员一时疏忽遗漏了。一个可能导致系统错误或者崩溃的bug就出现了,而使用ASSERT可以及时地提醒开发人员他的疏忽,尽可能快的消灭这个bug。还有一些情况,在开发人员编写代码时,如果能够确信在某一点出现情况A就是错误的,那么就可以在该处加上ASSERT,排除情况A。综上所述,恰当、灵活的使用ASSERT进行主动调试,能够极大提高程序的稳定性和安全性,减少调试时间,提高工作效率。
3.2 调试过程
确定一个适用于解决所有错误的调试过程有一定的难度,但John Robbins提出的调试过程应该说是最实用的:
1. 复制错误 2. 描述错误3. 始终假定错误是自己的问题 4. 分解并解决错误 5. 进行有创见的思考6. 使用调试辅助工具
7. 开始调试工作8. 校验错误已被更正9. 学习和交流
按下Alt+F9快捷键弹出Breakpoints对话框,浏览一下对话框发现该对话框分为Location、Data和Messages三页,分别对应三种断点:
2. 表达式和变量断点: 调试器会让程序一直运行,直到满足所设的条件或者指定数据更改为止。在Intel CPU上,这两种断点都尽可能通过CPU的特定调试寄存器使用一个硬件断点,如果能够使用调试寄存器,那么程序将能够全速运行,否则调试器将单步执行每个汇编指令,并每步都检查条件,程序的运行速度将极其缓慢甚至无法运行。各种高级断点的设置在MSDN中有详细的介绍,请在Visual C++子集下搜索主题Using Breakpoints: Additional Information并阅读相关内容。
3.4 调用堆栈
有时候我们并不清楚应该在哪里设置断点,只知道程序正在运行就突然崩溃了,这时候如何定位到出错地点呢?这时的选择就是查看调用堆栈,调用堆栈可以帮助我们确定某一特定时刻,程序中各个函数之间的相互调用关系。方法是当程序执行到某断点处或者程序崩溃,控制权转到调试器后,按下Alt+7快捷键,弹出Call Stack窗,你可以看到当前函数调用情况,当前函数在最上面,下面的函数依次调用其上面的函数。在Call Stack窗口的弹出菜单上选择Parameter values和Parameter Types可以显示各个函数的参数类型和传入值。
3.5 使用跟踪工具
有些时候,我们希望了解程序中不同函数之间的协作关系,或者由于文档的缺失,希望能够确认函数在不同情况下被调用时的传入参数值。这时使用断点功能就过分麻烦,而调用堆栈只能查看当前函数的被调用情况,一种较好的方法就是使用TRACE宏以及相对应的工具。程序(Debug版)运行中,一旦运行到TRACE宏,就会向当前Windows系统的调试器输出TRACE宏内指定的字符串并显示出来,当在Visual C++环境中调试运行(按F5键)程序时,可以在Output窗口的Debug页看到TRACE宏的输出内容。实际上,TRACE宏是封装了Windows API函数OutputDebugString的功能,有些辅助工具可以在不惊动Visual C++调试器的前提下,拦截程序中TRACE宏的输出内容,比如《深入浅出MFC》的附录中提到的Microsoft System Journal(MSJ)1996年1月的C/C++专栏介绍的TraceWin工具(在较老版本的MSDN中可以找到源 代码和文档)以及功能强大的免费工具DebugView。使用TRACE宏,我们可以轻松了解程序中各个函数之间的相互协作关系和被调用的先后顺序和时间(批:真的可以吗?),进一步说,你能够完全掌握程序的执行流程。(批:真的就太好了.) 最后请注意,TRACE宏会对程序效率有所影响,所以,当前不用的TRACE宏最好删除或者注释
掉。
将该模块去除,然后重新编译连接程序,运行程序,看程序运行是否正常。但无论采用什么方法探究阅读程序,都不要指望能够不费任何气力,花费一两个钟头就能够将上万行的程序探究个明白。 (批:有同感.)
5 参考资料
《程序设计实践》,机械工业出版社 《高质量C++编程指南》,林锐 《应用程序调试技术》,John Robbins,清华大学出版社
《面向对象软件构造》,Bertrand Meyer,清华大学出版社
以前有一个Debug.com,它是第一个Windows调试器. 它属于标准MS-DOS包中的一部分 . 80年代末期,Softice出现在舞台上,给受保护程序及它们的开发者带来不少的麻烦.从那时候起,Softice就作为黑客调试工具传了下来.但最近Olly在年轻一代破解者中越来越常用. 现在由于分析软件的可能性,与黑客对抗就成了无用的挣扎.
调试器如何工作
所有的调试器都无非属于下面两类:
.使用处理器的调试能力
.独立的模仿处理器,监视被测试程序的运行
调试器会检查标志寄存器的陷阱位是否为1.如果是,就在每条指令后自动生成一个INT 1 调试异常,并且将控制权交给调试器.由此可知,代码可以通过分析标志寄存器来检测跟踪(调试).有四个调试寄存器:1. DR0 2. DR1 3. DR2 4. DR3 .
它们存储了四个检测点的线性地址.当然还有一个寄存器保存了每个点的条件,它就是DR7.当任何一个条件为真时,处理器就会抛出INT1异常,控制权也就交给了调试器,有四个条件:1.一条指令被执行2.某个内存地址的内容被改变了3.某个内存地址被读取或改变,但没有被执行4.一个输入输出端口被引用
现在要讨论的是软件断点.如果将一个字节的代码--0xCC插入到一条指令前,再试图执行它就会引发INT 0x3异常.为了发现是否至少有一个点被设置了断点,程序仅需计算其校验和就可以了.为了做到这一点,可以使用MOV, MOVS, LODS,POP, CMP, CMPS或其他任何指令;
windows平台的调试器主要分为两大类:1 用户模式(user-mode)调试器:它们都基于win32 Debugging API,有使用方便的界面,主要用于调试用户模式下的应用程序。这类调试器包括Visual C++调试器、WinDBG、BoundChecker、Borland C++ Builder调试器、NTSD等。2 内核模式(kernel-mode)调试器:内核调试器位于CPU和操作系统之间,一旦启动,操作系统也会中止运行,主要用于调试驱动程序或用户模式调试器不易调试的程序。这类调试器包括WDEB386、WinDBG和softice等。其中WinDBG和softice也可以调试用户模式代码。国外一位调试高手曾说,他70%调试时间是在用VC++,其余时间是使用WinDBG和softice。(批:很显然,VC6的调试器应该过时了,VS2005的调试器应该更先进.但是,使用方法上还是有点可以参考的. ) 由于是在循环体内,如果在E行设置断点,可能需要按F5(GO)许多次。这样手要不停的按,很痛苦。使用VC6断点修饰条件就可以轻易解决此问题。然后选择D行所在的断点,然后点击condition按钮,在弹出对话框的最下面一个编辑框中输入一个很大数目,具体视应用而定,这里1000就够了。3 按F5重新运行程序,程序中断。Ctrl+B打开断点框,发现此断点后跟随一串说明:...487 times remaining。意思是还剩下487次没有执行,那就是说执行到513(1000-487)次时候出错的。因此,我们按步骤2所讲,更改此断点的skip次数,将1000改为513。(批:此功能是新功能) .在“Enter the expression to be evaluated:”下面,可以输入一些条件,当这些条件满足时,断点才启动。譬如,刚才的程序,我们需要i为100时程序停下来,我们就可以输入在编辑框中输入“i==100”。另外,如果在此编辑框中如果只输入变量名称,则变量发生改变时,断点才会启动。这对检测一个变量何时被修改很方便,特别对一些大程序。
二 数据断点(Data Breakpoint):我们可以首先在A行设置普通断点,F5运行程序,程序停在A行。然后我们再设置一个数据断点。F5继续运行,程序停在B行,说明B处代码修改了szName1。B处明明没有修改szName1呀?但调试器指明是这一行,一般不会错,所以还是静下心来看看程序,哦,你发现了:szName2只有4个字节,而strcpy了7个字节,所以覆写了szName1。数据断点不只是对变量改变有效,还可以设置变量是否等于某个值。(批:这个功能和上面好象有点重复了.但可以看出,数据断点相对位置断点一个很大的区别是不用明确指明在哪一行代码设置断点。)
三 其他
1 在call stack窗口中设置断点,选择某个函数,按F9设置一个断点。这样可以从深层次的函数调用中迅速返回到需要的函数。
2 Set Next StateMent命令(debug过程中,右键菜单中的命令)
此命令的作用是将程序的指令指针(EIP)指向不同的代码行。譬如,你正在调试上面那段代码,运行在A行,但你不愿意运行B行和C行代码,这时,你就可以在D行,右键,然后“Set Next StateMent”。调试器就不会执行B、C行。只要在同一函数内,此指令就可以随意跳前或跳后执行。灵活使用此功能可以大量节省调试时间。
watch窗口支持丰富的数据格式化功能。如输入0x65,u,则在右栏显示101。实时显示windows API调用的错误:在左栏输入@err,hr。在watch窗口中调用函数。提醒一下,调用完函数后马上在watch窗口中清除它,否则,单步调试时每一步调试器都会调用此函数。 4 messages断点不怎么实用。基本上可以用前面讲述的断点代替。调试最重要的还是你要思考,要猜测你的程序可能出错的地方,然后运用你的调试器来证实你的猜测。
一.高级断点语法
高级断点语法由两部分组成:1.上下文部分.2.位置,表达式,变量或Windows消息条件.
用函数,源文件和二进制模块来指定上下文,上下文的表示方法:{[函数],[源文件],[二进制模块]}
如在TEST.CPP的20行设一位置断点,语法为:{,TEST.CPP,}.20,如A.DLL或B.DLL都使用了该行,又只想在B.DLL的调用中触发,则必须使用:{,TEST.CPP,B.DLL}.20.
VC调试器中可直接输入上下文语法:Breakpoints对话框的Location选项卡BreakAt编辑框中.更容易的方法是使用BreatAt框右的箭头打开菜单,选择Advanced项,然后在Context框中输入断点的相应信息. 如想在一个绝对地址上中断,直接在BreakAt框中输入地址就行.
将函数名输入BreadAt框中.如果是C++代码,同时还需要类限定符.支持重载了的函数,调试器会列出所有满足条件的函数供选择,如输入时提供足够的信息,完全可略过选择过程.如输入:"CString::operator=(const char *)"可唯一确定要中断的函数.(批:我个人觉得函数中断似乎意义不大.F9就可以了)
可能用DUMPBIN程序查看这个名称:DUMPBIN /EXPORTS DLLname.例:在LoadLibraryA中设置中断:"{,,Kernel32.dll}LoadLibraryA".
如装入了符号,则要根据输出函数和调用协议来计算函数名.如上例,LoadLibraryA使用__stdcall调用协议,据该协议,函数名以下划线为前缀,所跟有进栈的字节数为后缀的@号.一般说来,参数个数*4,就是参数占用栈空间的总字节数,LoadLibary的名称便是:_LoadLibraryA@4,故最后的语法是:或"{,,Kernel32.dll}_LoadLibraryA@4"
附:常用的调用协议
1、__stdcall调用约定相当于16位动态库中经常使用的PASCAL调用约定。在32位的VC++5.0中PASCAL调用约定不再被支持(实际上它已被定义为__stdcall。除了__pascal外,__fortran和__syscall也不被支持),取而代之的是__stdcall调用约定。两者实质上是一致的,即函数的参数自右向左通过栈传递,被调用的函数在返回前清理传送参数的内存栈,但不同的是函数名的修饰部分(关于函数名的修饰部分在后面将详细说明)。
_stdcall是Pascal程序的缺省调用方式,通常用于Win32 Api中,函数采用从右到左的压栈方式,自己在退出时清空堆栈。VC将函数编译后会在函数名前面加上下划线前缀,在函数名后加上"@"和参数的字节数。
2、C调用约定(即用__cdecl关键字说明)按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于传送参数的内存栈是由调用者来维护的(正因为如此,实现可变参数的函数只能使用该调用约定)。另外,在函数名修饰约定方面也有所不同。
_cdecl是C和C++程序的缺省调用方式。每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。函数采用从右到左的压栈方式。VC将函数编译后会在函数名前面加上下划线前缀。是MFC缺省调用约定。
3、__fastcall调用约定是“人”如其名,它的主要特点就是快,因为它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈),在函数名修饰约定方面,它和前两者均不同。
_fastcall方式的函数采用寄存器传递参数,VC将函数编译后会在函数名前面加上"@"前缀,在函数名后加上"@"和参数的字节数。
4、thiscall仅仅应用于“C++”成员函数。this指针存放于CX寄存器,参数从右到左压。thiscall不是关键词,因此不能被程序员指定。
5、naked call采用1-4的调用约定时,如果必要的话,进入函数时编译器会产生代码来保存ESI,EDI,EBX,EBP寄存器,退出函数时则产生代码恢复这些寄存器的内容。naked call不产生这样的代码。naked call不是类型修饰符,故必须和_declspec共同使用。
关键字 __stdcall、__cdecl和__fastcall可以直接加在要输出的函数前,也可以在编译环境的Setting...\C/C++ \Code Generation项选择。当加在输出函数前的关键字与编译环境中的选择不同时,直接加在输出函数前的关键字有效。它们对应的命令行参数分别为/Gz、/Gd和/Gr。缺省状态为/Gd,即__cdecl。
要完全模仿PASCAL调用约定首先必须使用__stdcall调用约定,至于函数名修饰约定,可以通过其它方法模仿。还有一个值得一提的是WINAPI宏,Windows.h支持该宏,它可以将出函数翻译成适当的调用约定,在WIN32中,它被定义为__stdcall。使用WINAPI宏可以创建自己的APIs。
(批:在64位系统中好象只剩一种了,但知道点也好.)
2.条件表达式.
只有表达式为真时触发.Breakpoint框Condition按钮,选第一个编辑框,输入表达式即可.规则:
.只可使用C类型比较运算符. .表达式中不能调用任何函数. .表达式中不能包含任何宏值. (批:上面没提到过,补充一下)
表达式为@TIB=Thread Infomation Block Linear Address,则程序只在该特定线程中才会中断.(批:这个好象太有用了,我现在就老是在线程间跳来跳去的.).例:线程@TIB地址值为0E000,则输入"@TIB==0xE000"(批:但是如何知道线程的地址值呢?可能只要随便设置个断点看看就可以了.),则在切换到该线程时中断.对W98,可用@FS=thread specific value. 如在某特定错误后中断,则可用@ERR,如"@ERR=2"表示在最后错误为ERROR_FILE_NOT_FOUND.除@CLK外,所有可在WATCH窗口中使用的伪寄存器均可用于条件表达式.
调试器可监控某一地址和该地址上的1,2或4字节的内容. 确保利用硬件调试寄存器的最好方法是使用表达式和数据更改位置的实际地址值.例如:g_szGlobal是全局数组指针,地址为0x5000,则在Breakpoint对话框中DATA选项卡中将表达式断点设为"*(char*))0x5000=='G'",但如果写为"WO(0x5000)=='G',则用不到硬件调试寄存器,会单步执行每条指令.
六.WINDOWS消息断点.
Breakpoint框的Message页.需要指定一个窗口过程,注意:MFC世界中AfxWndProc是多数窗口的一个窗口过程.
WinDbg:(它和VS里的调试器有什么区别呢?难道只是因为它不要钱?)
WinDbg是微软开发的免费源码级调试工具。Windbg可以用于Kernel模式调试和用户模式调试,还可以调试Dump文件。由于大部分程序员不需要做Kernel模式调试, 我在这篇文章中不会介绍Kernel模式调试。Kernel模式调试对学习Windows核心极有帮助。如果你对此感兴趣,可以阅读Inside Windows 2000和Windbg所带的帮助文件。在同一个进程中可能有多个线程。~命令可以用来显示和切换线程(批:似乎这是我唯一感兴趣的.看完之后,我只想说一局,OH,MY GOD,我想我一般不会去使用它)。
Visual C++的调试功能是从早期的调试工具CodeBase发展而成,这个名叫CodeBase调试工具研究成为一种工业标准。(批:呵,新鲜,第一次听到)
首先我们看一下使用异常处理的几种情况:
A. 用来处理非致命的错误
B. 对API函数的参数合法性的检验(假设参数都是合法的,只有遇到异常的时候进行合法性检验)
C. 处理致命错误(退出时最好的选择,但是有的时候可以用异常处理函数在程序退出前释放资源,删除临时文件等,甚至可以详细记录产生异常的指令位置和环境)
D. 处理“计划内”的异常(我们可能更关心这种情况)
接着我们看看Windows下异常处理的两种方式:1使用筛选器2 SEH异常处理
一、 使用筛选器(批:这个就稍微看一下吧.)
因为这里我要重点关注的是SEH的处理方式,所以还是简单的提一下筛选器处理方式。筛选器异常处理是通过异常回调函数来指定程序处理异常。这种方式的回调函数必须是唯一的,设置新的回调函数后以前的将失效。适用于进程范围。看一下这个函数的定义
Invoke SetUnhandledExecpionFilter,offset_Handler
Mov lpPrevHandler,eax
回调函数的格式:
_Handlerproc pExecptionInfo
看看pExecptionInfo这个指针参数指向的一个数据结构
EXCEPTION_POINTERS STRUCT (批:好象这里作者写错了,有时间可以去查查)
pExceptionRecord DWORD ?
ContextRecord DWORD ?
EXCEPTION_POINTERS ENDS
下面介绍 EXCEPTION_RECORD和CONTEXT结构的定义:
;//===================== 以下是两个成员的详细结构=========================
EXCEPTION_RECORD STRUCT
ExceptionCode DWORD ? ;//异常码
ExceptionFlags DWORD ? ;//异常标志
pExceptionRecord DWORD ? ;//指向另外一个EXCEPTION_RECORD的指针
ExceptionAddress DWORD ? ;//异常发生的地址
NumberParameters DWORD ? ;//下面ExceptionInformation所含有的dword数目
ExceptionInformation DWORD EXCEPTION_MAXIMUM_PARAMETERS dup(?)
EXCEPTION_RECORDENDS ;//EXCEPTION_MAXIMUM_PARAMETERS ==15
;//=============================具体解释================================
ExceptionCode 异常类型,SDK里面有很多类型,你可以在windows.inc里查找STATUS_来找到更多的异常类型,下面只给出hex值,具体标识定义请查阅windows.inc,你最可能遇到的几种类型如下:
C0000005h----读写内存冲突
C0000094h----非法除0
C00000FDh----堆栈溢出或者说越界
80000001h----由Virtual Alloc建立起来的属性页冲突
C0000025h----不可持续异常,程序无法恢复执行,异常处理例程不应处理这个异 常
C0000026h----在异常处理过程中系统使用的代码,如果系统从某个例程莫名奇妙的返回,则出现此代码, 如果RtlUnwind时没有Exception Record参数也同样会填入这个代码
80000003h----调试时因代码中int3中断
80000004h----处于被单步调试状态
注:也可以自己定义异常代码,遵循如下规则:
____________________________________________________________________
位: 31~30 29~28 27~16 15~0
____________________________________________________________________
含义: 严重程度 29位 功能代码 异常代码
0==成功 0==Mcrosoft MICROSOFT定义 用户定义
1==通知 1==客户
2==警告 28位
3==错误 被保留必须为0
ExceptionFlags 异常标志
0----可修复异常
1----不可修复异常
2----正在展开,不要试图修复什么,需要的话,释放必要的资源
pExceptionRecord 如果程序本身导致异常,指向那个异常结构
ExceptionAddress 发生异常的eip地址
ExceptionInformation 附加消息,在调用RaiseException可指定或者在异常号为C0000005h即内存异常时含义如下
第一个dword 0==读冲突 1==写冲突
第二个dword 读写冲突地址
;//================================解释结束============================
off.
CONTEXT STRUCT ; _
ContextFlags DWORD ? ; | +0
iDr0 DWORD ? ; | +4
iDr1 DWORD ? ; | +8
iDr2 DWORD ? ; >调试寄存器 +C
iDr3 DWORD ? ; | +10
iDr6 DWORD ? ; | +14
iDr7 DWORD ? ; _| +18
FloatSave FLOATING_SAVE_AREA <> ;浮点寄存器区 +1C~~~88h
regGs DWORD ? ;--| +8C
regFs DWORD ? ; |\段寄存器 +90
regEs DWORD ? ; |/ +94
regDs DWORD ? ;--| +98
regEdi DWORD ? ;____________ +9C
regEsi DWORD ? ; | 通用 +A0
regEbx DWORD ? ; | 寄 +A4
regEdx DWORD ? ; | 存 +A8
regEcx DWORD ? ; | 器 +AC
regEax DWORD ? ;_______|___组_ +B0
regEbp DWORD ? ;++++++++++++++++ +B4
regEip DWORD ? ; |控制 +B8
regCs DWORD ? ; |寄存 +BC
regFlag DWORD ? ; |器组 +C0
regEsp DWORD ? ; | +C4
regSs DWORD ? ;++++++++++++++++ +C8
ExtendedRegisters db MAXIMUM_SUPPORTED_EXTENSION dup(?)
CONTEXT ENDS
;//============================以上是两个成员的详细结构============
程序使用筛选器异常处理时可以通过查看上面结构中的regEip来找到产生异常的地址!调试的时候可以改变EIP的值以达到越过异常程序,转到“安全”的地方。
最后看一下筛选器异常处理回调函数的返回值
EXECPTION_EXECUTE_HANDLER 1;进程被终止,终止前不会出现提示错误的对话框
EXECPTION_CONTINUE_SEARCH 0;同样终止程序,显示错误对话框
EXECPTION_CONTINUE_EXECUTION -1;系统将CONTECT设置回去,继续执行程序
使用筛选器程序是最简单的处理异常方法,不足:1 不便于封装。2 处理是全局性的也就是无法对每个线程或子程序设置一个私有的异常处理程序进行异常处理。
进入正题:SEH异常处理
首先解释一下什么是SEH异常处理:SEH("Structured Exception Handling",即结构化异常处理.是操作系统提供给程序设计者的强有力的处理程序错误或异常的武器。
下面结合冷雨飘心的一个SEH异常处理程序来说明具体的用法:
;(批:注释)//====================================================================
;// ex. 2,by Hume,2001 线程相关的异常处理
;//====================================================================
.386(批:好象汇编程序都要写个头的,没怎么研究过汇编)
.model flat, stdcall
option casemap :none ; case sensitive
include hd.h ;//相关的头文件,你自己维护一个吧
;//============================
.data(批:可能是说以下是数据区吧,我也不太懂)
szCap db "By Hume[AfO],2001...",0
szMsgOK db "It's now in the Per_Thread handler!",0
szMsgERR1 db "It would never Get here!",0
buff db 200 dup(0)
.code(批:可能是说代码区)
_start: (批:函数的开始,这些应该都是给汇编编译器处理的吧,估计不涉及生成机器代码)
;//========prog begin====================
ASSUME FS:NOTHING
push offset perThread_Handler
push fs:[0]
mov fs:[0],esp ;//建立SEH的基本ERR结构,如果不明白,就仔细研究一下吧
xor ecx,ecx
mov eax,200
cdq ;//双字扩展到四个字节,因为是除法
div ecx
;//以下永远不会被执行
invoke MessageBox,NULL,addr szMsgERR1,addr szCap,MB_OK+MB_ICONINFORMATION
pop fs:[0]
add esp,4
invoke ExitProcess,NULL
;//============================
perThread_Handler: (批:函数名?)
invoke MessageBox,NULL,addr szMsgOK,addr szCap,MB_OK+MB_ICONINFORMATION
mov eax,1 ;//ExceptionContinueSearch,不处理,由其他例程或系统处理
;mov eax,0 ;//ExceptionContinueExecution,表示已经修复CONTEXT,可从异常发生处继续执行
ret ;//这里如果返回0,你会陷入死循环,不断跳出对话框....
;//=============================Prog Ends==============
end _start
程序本身很简单,注释也很详细。我们来看看是如何注册回调函数的
push offset perThread_Handler (批:回调函数的写法和普通函数有区别吗?)
push fs:[0]
mov fs:[0],esp
仅仅三个语句就解决了~那么为什么要用fs这个段寄存器呢?这里又涉及一个重要的内容:TIB(Thread Information Block线程信息块)。我们来看看这个重要的数据结构(引用了《罗聪浅谈利用SEB实现反跟踪》的部分内容)
TEB(Thread Environment Block) 在 Windows 9x 系列中被称为 TIB(Thread Information Block),它记录了线程的重要信息,而且每一个线程都会对应一个 TEB 结构。 Matt Pietrek 大牛已经给我们列出了它的结构,我就不多说啦,见下:(摘自 Matt Pietrek 的 Under The Hood - MSJ 1996)
//===========================================================
// file: TIB.H
// Author: Matt Pietrek
// From: Microsoft Systems Journal "Under the Hood", May 1996
//===========================================================
#pragma pack(1)
typedef struct _EXCEPTION_REGISTRATION_RECORD
{
struct _EXCEPTION_REGISTRATION_RECORD * pNext;
FARPROC pfnHandler;
} EXCEPTION_REGISTRATION_RECORD, *PEXCEPTION_REGISTRATION_RECORD;
typedef struct _TIB
{
PEXCEPTION_REGISTRATION_RECORD pvExcept; // 00h Head of exception record list
PVOID pvStackUserTop; // 04h Top of user stack
PVOID pvStackUserBase; // 08h Base of user stack
union // 0Ch (NT/Win95 differences)
{
struct // Win95 fields
{
WORD pvTDB; // 0Ch TDB
WORD pvThunkSS; // 0Eh SS selector used for thunking to 16 bits
DWORD unknown1; // 10h
} WIN95;
struct // WinNT fields
{
PVOID SubSystemTib; // 0Ch
ULONG FiberData; // 10h
} WINNT;
} TIB_UNION1;
PVOID pvArbitrary; // 14h Available for application use
struct _tib *ptibSelf; // 18h Linear address of TIB structure
union // 1Ch (NT/Win95 differences)
{
struct // Win95 fields
{
WORD TIBFlags; // 1Ch
WORD Win16MutexCount; // 1Eh
DWORD DebugContext; // 20h
DWORD pCurrentPriority; // 24h
DWORD pvQueue; // 28h Message Queue selector
} WIN95;
struct // WinNT fields
{
DWORD unknown1; // 1Ch
DWORD processID; // 20h
DWORD threadID; // 24h
DWORD unknown2; // 28h
} WINNT;
} TIB_UNION2;
PVOID* pvTLSArray; // 2Ch Thread Local Storage array
union // 30h (NT/Win95 differences)
{
struct // Win95 fields
{
PVOID* pProcess; // 30h Pointer to owning process database
} WIN95;
} TIB_UNION3;
} TIB, *PTIB;
#pragma pack()
让我们抬头看看上面的 Matt Pietrek 的代码,其中有这么一行:
PEXCEPTION_REGISTRATION_RECORD pvExcept; // 00h Head of exception record list
注意到 PEXCEPTION_REGISTRATION_RECORD 这个定义,它表示 pvExcept 这个变量正是 exception record list 的入口,这个入口位于整个结构的 0 偏移处。同时,在 M 的 Intel i386 Windows NT/2K/XP 内核中,每当创建一个线程,OS 均会为每个线程分配 TEB ,而且 TEB 永远放在 fs 段选择器指定的数据段的 0 偏移处。
这样一来,你就明白了 SEH 注册的偏移为什么是在 fs:[0] 了吧?
事实上 Windows 系统都是通过这种方法来为应用程序提供信息的,比如有这样的例子:
struct _tib *ptibSelf; // 18h Linear address of TIB structure (批:程序的例子里怎么都是说TIB,好象是TEB才对.)
DWORD threadID; // 24h
Windows 提供了一个 API :GetCurrentThreadID(),它的内部工作原理其实是这样的:(利用了上面的这两个地址)
mov eax, fs:[18h] ;因为 18h 偏移处是 TIB 结构的线性偏移地址(批:取TIB结构?)
mov eax, [eax + 24h] ;因为 24h 偏移处是 threadID 的地址(批:取threadID 的地址?)
ret ;把 eax 中储存的 threadID 地址返回
注:为什么要声明assume fs:nothing?因为masm编译器默认将fs段寄存器定义为error,所以程序在使用fs前必须将它启动!
接下来看看SEH的回调函数
_Handler proc _lpExecptionRecord, _lpSEH,lp_context,lp_DispatcherContext
_lpExecptionRecord指向一个EXECPTION_RECORD结构。
lp_context 指向一个CONTEXT结构。
_lpSEH 指向注册回调函数时使用的EXXCEPTION_REGISTRATION结构的地址。
返回值有四种取值:
ExecptionContinueExecution ( 0 :系统将线程环境设置为_lpContext指向的CONTEXT结构并继续执行。
ExceptionContinueSearch(1):回调函数拒绝处理这个异常,系统通过EXECPTION_REGISTRATION结构的prev字段得到前一个回调函数的地址并调用它。
ExecptionNestedExecption (2):发生异常嵌套。
ExecptionCollidedUnwind (3):异常展开操作。这一个部分不做多讲,有兴趣的可以看看罗云彬的书,其实是很重要的一部分。
如果一个程序既有筛选器异常处理又有SEH异常处理,而且系统还有默认的异常处理机制,那么他们被调用的先后次序是怎么样的呢?
发生异常时系统的处理顺序(by Jeremy Gordon):
1.系统首先判断异常是否应发送给目标程序的异常处理例程,如果决定应该发送,并且目标程序正在被调试,则系统挂起程序并向调试器发送EXCEPTION_DEBUG_EVENT消息.呵呵,这不是正好可以用来探测调试器的存在吗?
2.如果你的程序没有被调试或者调试器未能处理异常,系统就会继续查找你是否安装了线程相关的异常处理例程,如果你安装了线程相关的异常处理例程,系统就把异常发送给你的程序seh处理例程,交由其处理.
3.每个线程相关的异常处理例程可以处理或者不处理这个异常,如果他不处理并且安装了多个线程相关的异常处理例程,可交由链起来的其他例程处理.
4.如果这些例程均选择不处理异常,如果程序处于被调试状态,操作系统仍会再次挂起程序通知debugger.
5.如果程序未处于被调试状态或者debugger没有能够处理,并且你调用SetUnhandledExceptionFilter安装了最后异 常处理例程的话,系统转向对它的调用.
6.如果你没有安装最后异常处理例程或者他没有处理这个异常,系统会调用默认的系统处理程序,通常显示一个对话框, 你可以选择关闭或者最后将其附加到调试器上的调试按钮.如果没有调试器能被附加于其上或者调试器也处理不了,系统就调用ExitProcess终结程序.
7.不过在终结之前,系统仍然对发生异常的线程异常处理句柄来一次展开,这是线程异常处理例程最后清理的机会.
说了这么多你也许会问SEH异常处理到底有什么用处呢?呵呵,且听小生慢慢道来~~~
第一道菜:病毒程序巧用SEH
这里简单的说一下如何利用SEH异常处理程序来躲避下毒软件的反病毒引擎。一个反病毒引擎在一个程序运行的时候会模拟程序的代码,当发现程序代码的疑点比较多的时候会报告成病毒。看看下面这段程序:
start:call Set_SEH;这句其实就是 push offset CONTINUE
; JMP Set_SEH
CONTINUE:mov esp, [esp+8]; [ESP+8]存储的是旧的堆栈地址。
push offset Start_Virus ;----_ 把Start_Virus 的地址压栈,当作返回地址
ret;----跳到Start_Virus去,是不是很magic?
Set_SEH:sub edx, edx ;Edx =0
Assume fs:nothing
push dword ptr fs:[edx];把指去 _EXCEPTIONAL_REGISTRATION_RECORD 结构的指针入栈
mov fs:[edx], esp;安装一个seh
mov [edx],edx;引起一个内存读写冲突,发生异常因为edx=0
;如果反病毒引擎不处理异常,不进入seh 处理程序(即 CONTINUE: ,继续模
;拟下个指令,也就是jmp start,那么就进入一个死循环,可能会引起死机。
jmp start
Start_Virus: .....
是不是很简单呢?就是让反病毒引擎不处理这个人为的异常时进入死循环~!!
第二道菜:TEB反跟踪初探
如果你的记性够好的话一定记得上面介绍过的TEB(TIB)线程信息块结构中有这么一句:(批:TEB与TIB相同?)
PVOID* pProcess; // 30h Pointer to owning process database
这个偏移地址处的内容非常有用,它指向本线程的拥有者的 PDB(Process Database) 的线性地址。当你用动态调试器,例如 OllyDbg 的时候,调试器是把调试的对象作为一个子线程进行跟踪的,在这种情况下,被调试的对象的“拥有者”就是调试器本身,也就是说,它的 TEB 的 30h 处的偏移指向的内容肯定不为 0 ,这样,我们就可以利用这一点,判断 30h 偏移指向的内容,来判断是否有调试器跟踪。
最后给出一个 Anti-Debug 的例子程序,用 MASM 编译完成后,请用 OllyDbg 来加载调试一下,看看与正常的运行结果有什么不同。
;*********************************************************
;程序名称:演示利用 TEB 结构进行 Anti-Debug
; 请用 OllyDbg 进行调试
;适用OS:Windows NT/2K/XP
;作者:罗聪
;日期:2003-2-9
;出处:http://www.LuoCong.com(老罗的缤纷天地)
;注意事项:如欲转载,请保持本程序的完整,并注明:
;转载自“老罗的缤纷天地”(http://www.LuoCong.com)
;*********************************************************
.386
.model flat, stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
.data
szCaption db "Anti-Debug Demo by LC, 2003-2-9", 0
szDebugged db "Hah, let me guess... U r dEBUGGINg me! ", 0
szFine db "Good boy, no dEBUGGEr detected!", 0
.code
main:
assume fs:nothing
mov eax, fs:[30h] ;指向 PDB(Process Database)
movzx eax, byte ptr [eax + 2h];无符号数带零扩展
or al, al
jz _Fine
_Debugged:
push MB_OK or MB_ICONHAND
push offset szCaption
push offset szDebugged
jmp _Output
_Fine:
push MB_OK or MB_ICONINformATION
push offset szCaption
push offset szFine
_Output:
push NULL
call MessageBoxA
invoke ExitProcess, 0
end main
第三道菜:利用SEH执行shellcode
假设异常处理例程入口00401053,程序刚开始执行时esp是0012ffc4,以前的fs:[0]是0012ffe0
建立了TIB结构的第一个成员后堆栈的情况如下:
内存低地址
| E0 |12ffbc(esp)
| FF |
| 12 | --ERR结构的第一个成员
|_00_|
| 53 |12ffc0
| 10 |
| 40 | --ERR结构的第二个成员
| 00 |
内存高地址
好了然后程序CALL一个函数,函数里面有一个局部变量并且在往其分配的空间中写入的数据时产生溢出.这时堆栈如下
____
| |12f000 局部变量分配的空间,并且向12ffc0方向溢出了.
| |
....
....
|_EBP|12ffb4 函数中保存老的EBP
| xx |
| xx |
| xx |
|_EIP|12ffb8 call函数时EIP进栈
| xx |
| xx |
|_xx_|
| E0 |12ffbc(esp) {当SEH起作用的时候EBX刚好指向这个地址(也可说总是指向当前ERR结构)}
| FF |
| 12 | --ERR结构的第一个成员
|_00_|
| 53 |12ffc0
| 10 |
| 40 | --ERR结构的第二个成员
|_00_|
| |12ffc4
继续看,假设溢出代码一直到了12ffc4,然后call的函数该返回了,因为保存的EIP被溢出代码代替所以程序出错(不会不出错吧?),这样ESH开始起作用了(注:在这期间系统要执行一些操作,所以EBX才会指向当前ERR).这样一来程序就会跳到12ffc0里的地址去执行!而12ffc0里的东东早已不是原来的00401053了.这样我们不就改变了程序的流向了么.12ffc0中该写入什么内容呢?应是内存中JMP EBX的代码的地址.这样跳了3下后最终就会跳到12ffbc去执行.这个四字节可是宝贵的啊现在假设JMP EBX这个指令在内存中的地址是0x77e33f4d
那下具体看一下现在堆栈的情况:
| EB |12ffbc(esp) {当ESH起作用的时候EBX刚好指向这个地址(也可说总是指向当前ERR结构)}
| 06 |
| 90 | --ERR结构的第一个成员,执行JMP EBX后就到这儿来执行了(EB 06是短跳转JMP 12FFC4的机器码)
|_90_| 后面的90是nop空指令的机器码.
| 4D |12ffc0
| 3F |
| E3 | --ERR结构的第二个成员,出错处理函数的入口地址(现在成了JMP EBX的地址)
|_77_|
| |12ffc4
....
好现在来看看12ffc4里面有些什么代码.(简单的说这段代码的作用是计算真正的shellcode的起始地址,然后跳过去执行.
低地址
| |12f000(shellcode开始地址)
....
....
| 81 |12ffc4
| C3 | add ebx,FFFFF03Ch(ebx=12ffc4,指令长度6,作用计算计算shellcode地址)
| 3C |
| F0 |
| FF |
| FF |
| FF |12ffca jmp ebx
| D3 |
高地址
测试程序
-------------------------SEH.ASM------------------
.386
.model flat,stdcall
option casemap:none
include ../include/user32.inc
includelib ../lib/user32.lib
include ../include/kernel32.inc
includelib ../lib/kernel32.lib
.data
hello db '利用一个读INI文件的API来演示WIN2000本地溢出',0
lpFileName db '.\seh.ini',0
lpAppName db 'iam',0
lpKeyName db 'czy',0
lpDefault db 'ddd',0
szCap db "SEH TEST",0
szMsgOK db "OK,the exceptoin was handled by final handler!",0
szMsgERR1 db "It would never Get here!",0
.code
testov proc
local lpReturnedString[2224] : byte ;返回的字串搞成本地变量这样就和C语言一样了,它是在栈中
invoke GetPrivateProfileString,offset lpAppName,offset,lpKeyName,offset lpDefault,ADDR lpReturnedString,2249,offset lpFileName
invoke MessageBox,0,addr lpReturnedString,addr lpReturnedString,1
ret
testov endp
start:
ASSUME fs:NOTHING
invoke MessageBox,0,addr szMsgERR1,addr szCap,30h+1000h ;下断点
push offset Final_Handler ;压入正常的出错处理程序入口地址
push FS:[0] ;把前一个TIB的地址压入
mov fs:[0],esp
call testov
pop fs:[0] ;还原FS:[0]
Final_Handler: ;由于溢出了下面的代码不会被执行.
invoke MessageBox,0,addr szMsgOK,addr szCap,30h
invoke ExitProcess,0
mov eax,1
ret
end start
?
-----------------end-------------
1如何更好的在内存中找JMP EBX的代码:
在softice中执行S 10:0 L FFFFFFFF FF D3就可以了,但实际上这样找到的
地址可能不能执行代码.所以用下面的方法:
map32 kernel32(在当前进程中查找映射的kernel32 DLL的信息)
一般有如下显示:
Owner Obj Name Obj# Address Size TYPE
kernel32 .text 0001 001b:77b61000 0005d1ae code RO
......
然后
S 77b61000 L 5d1ae FF D3
如果显示如下说明找到了:
Pattern Found at 0023:77e61674 ....
2)关于缓冲区的大小的问题:
利用SEH的办法就起码要设成1000个字节多,你的shellcode才不会被不知哪来的数据覆盖!
这道菜czy做的不好吃:(我感觉理解起来有些困难~!因为关于缓冲区溢出自己接触的太少,不过好东西要保留的,以后回过头看!
第四道菜:用 SEH 技术实现 API Hook
这一部分不想展开了,给大家一个链接吧。
http://www.luocong.com/articles/show_article.asp?Article_ID=25
在SEH异常处理链中最后一个被装载的SEH异常处理程序总是被第一个调用,想想如果自己花了一个星期才写出来一个异常处理程序,能够完美处理所有异常,并希望异常全部由你来处理,但很不幸,比如你调用了一个外部模块,而这个模块自己安装了一个ugly的seh处理例程,他的动作是只要有异常发生就简单地终止程序,哈哈,那就死悄悄了。又比如你想在你的加壳程序里面加密目标程序代码段,然后发生无效指令异常的时候用你自己安装的处理句柄来解密代码段继续执行,听起来这的确是一个好主意,但遗憾的是大多数C/C++代码都用_try{}_except{}块来保证其正确运行,而这些异常处理例程是在你壳注册的例程之后安装的,因而也就在链的前面,无效指令一执行,首先是C/C++编译器本身提供的处理例程或者程序其他的异常处理例程来处理,可能简单结束程序或者....
好象和调试有点跑题了,但是现在的程序里,线程一般是少不了的,对调试也有好处,其实我也只是做个记录,很多细节也没看,只是觉得以后会要用的.所以写记录在这里.
总结:为什么程序调试的进展很慢?1,睡的太晚,影响了记忆力和思考能力.2,还不会真正的调试程序.
到底要怎么样去调试程序呢?
在调试过程中经常会遇到这样的问题,变量的值在哪里设置的?一般变量都是在使用前被设置的,但是有时候程序需要读取硬件信息或外存的数据时,可能就会先去读好.但似乎还是很难找从所有代码中去找到答案.还有个问题就是我们找到了,数据是在哪里设置的,但是我们却跟踪不到那个出错的数据,在几万次的循环中,要确定那个导致BUG的数据在哪次出现是不可能的.当然其实我这个问题并没那么复杂,只到第二次就发生了,但是问题还是没解决,因为我是多线程,4个同样的线程再执行着同样一个函数,而且发生BUG时,我不知道是哪个线程出错了,这种多线程的调试问题该怎么样解决呢?DEBUG是可以设置条件的,当某个条件发生时,中断就会发生.
如何设置各种断点,如何打LOG(就是把一些有用的信息加到输出文件中),如何更随心所欲的知道想知道的信息,如何快速的搭建一个测试环境,这都成了现在要解决的东西.
http://blog.csdn.net/rick1126/relatedarticles/2704.aspx(这里好象全是在讲调试的东西).以下是摘要.
1. 利用工具(Parasoft C++ Test)检查你的代码,评估一下自己形成良好的习惯没有。 (这个一般也不会去用,但知道一下有这个软件也好.)
主动调试指在写代码的时候,通过加入适量的调试代码,帮助我们在软件错误发生的时候迅速弹出消息框,告知开发人员错误发生地点,并中止程序。
各种开发语言和开发工具都提供这些调试语句,标准C++提供了assert函数,MFC提供了ASSERT调试宏帮助我们进行主动调试,在实际工作中,建议统一使用MFC的ASSERT调试宏。
2.2.1 参数检查
对于编写的函数,除了明确的指定契约外,在函数开始处应该对传入的参数进行检查,确保非法参数传入时立即报告错误信息。例如:
BOOL GetPathItem ( int i , LPTSTR szItem , int iLen )
{ASSERT ( i > 0 ) ;ASSERT ( NULL != szItem ) ;ASSERT ( ( iLen > 0 ) && ( iLen < MAX_PATH ) ) ;
ASSERT ( FALSE == IsBadWriteStringPtr ( szItem , iLen ) ) ;}
对指针的检查尤其要注意,通常程序员会这样进行检查:
// An example of checking only a part of the error condition
BOOL EnumerateListItems ( PFNELCALLBACK pfnCallback )
{ ASSERT ( NULL != pfnCallback ) ; }
这样的检查只能够排除指针为空的情况,但是如果指针指向的是非法地址,或者指针指向的
对象并不是我们需要的类型,上面的例子就没有办法检查出来,而是统统认为是正确的。完
整的检查应该如下:
// An example of completely checking the error condition
BOOL EnumerateListItems ( PFNELCALLBACK pfnCallback )
{ASSERT ( FALSE == IsBadCodePtr ( pfnCallback ) ) ;}
2.2.2 内部检查
恰当地在代码中使用ASSERT,对bug检测和提高调试效率有极大的帮助,下面举个简单的例子加以说明。
switch( nType ){
case GK_ENTITY_POINT:// do something break;
case GK_ENTITY_PLINE:// do something break;
default: ASSERT( 0 ); }
在上面的例子中,switch语句仅仅处理了GK_ENTITY_POINT和GK_ENTITY_PLINE两种情况,应该是系统中当时只需要处理这两种情况,但是如果后期系统需要处理更多的情况,而此时上面这部分代码又没有及时更新,或者是因为开发人员一时疏忽遗漏了。一个可能导致系统错误或者崩溃的bug就出现了,而使用ASSERT可以及时地提醒开发人员他的疏忽,尽可能快的消灭这个bug。还有一些情况,在开发人员编写代码时,如果能够确信在某一点出现情况A就是错误的,那么就可以在该处加上ASSERT,排除情况A。综上所述,恰当、灵活的使用ASSERT进行主动调试,能够极大提高程序的稳定性和安全性,减少调试时间,提高工作效率。
3.2 调试过程
确定一个适用于解决所有错误的调试过程有一定的难度,但John Robbins提出的调试过程应该说是最实用的:
1. 复制错误 2. 描述错误3. 始终假定错误是自己的问题 4. 分解并解决错误 5. 进行有创见的思考6. 使用调试辅助工具
7. 开始调试工作8. 校验错误已被更正9. 学习和交流
按下Alt+F9快捷键弹出Breakpoints对话框,浏览一下对话框发现该对话框分为Location、Data和Messages三页,分别对应三种断点:
2. 表达式和变量断点: 调试器会让程序一直运行,直到满足所设的条件或者指定数据更改为止。在Intel CPU上,这两种断点都尽可能通过CPU的特定调试寄存器使用一个硬件断点,如果能够使用调试寄存器,那么程序将能够全速运行,否则调试器将单步执行每个汇编指令,并每步都检查条件,程序的运行速度将极其缓慢甚至无法运行。各种高级断点的设置在MSDN中有详细的介绍,请在Visual C++子集下搜索主题Using Breakpoints: Additional Information并阅读相关内容。
3.4 调用堆栈
有时候我们并不清楚应该在哪里设置断点,只知道程序正在运行就突然崩溃了,这时候如何定位到出错地点呢?这时的选择就是查看调用堆栈,调用堆栈可以帮助我们确定某一特定时刻,程序中各个函数之间的相互调用关系。方法是当程序执行到某断点处或者程序崩溃,控制权转到调试器后,按下Alt+7快捷键,弹出Call Stack窗,你可以看到当前函数调用情况,当前函数在最上面,下面的函数依次调用其上面的函数。在Call Stack窗口的弹出菜单上选择Parameter values和Parameter Types可以显示各个函数的参数类型和传入值。
3.5 使用跟踪工具
有些时候,我们希望了解程序中不同函数之间的协作关系,或者由于文档的缺失,希望能够确认函数在不同情况下被调用时的传入参数值。这时使用断点功能就过分麻烦,而调用堆栈只能查看当前函数的被调用情况,一种较好的方法就是使用TRACE宏以及相对应的工具。程序(Debug版)运行中,一旦运行到TRACE宏,就会向当前Windows系统的调试器输出TRACE宏内指定的字符串并显示出来,当在Visual C++环境中调试运行(按F5键)程序时,可以在Output窗口的Debug页看到TRACE宏的输出内容。实际上,TRACE宏是封装了Windows API函数OutputDebugString的功能,有些辅助工具可以在不惊动Visual C++调试器的前提下,拦截程序中TRACE宏的输出内容,比如《深入浅出MFC》的附录中提到的Microsoft System Journal(MSJ)1996年1月的C/C++专栏介绍的TraceWin工具(在较老版本的MSDN中可以找到源 代码和文档)以及功能强大的免费工具DebugView。使用TRACE宏,我们可以轻松了解程序中各个函数之间的相互协作关系和被调用的先后顺序和时间(批:真的可以吗?),进一步说,你能够完全掌握程序的执行流程。(批:真的就太好了.) 最后请注意,TRACE宏会对程序效率有所影响,所以,当前不用的TRACE宏最好删除或者注释
掉。
将该模块去除,然后重新编译连接程序,运行程序,看程序运行是否正常。但无论采用什么方法探究阅读程序,都不要指望能够不费任何气力,花费一两个钟头就能够将上万行的程序探究个明白。 (批:有同感.)
5 参考资料
《程序设计实践》,机械工业出版社 《高质量C++编程指南》,林锐 《应用程序调试技术》,John Robbins,清华大学出版社
《面向对象软件构造》,Bertrand Meyer,清华大学出版社
以前有一个Debug.com,它是第一个Windows调试器. 它属于标准MS-DOS包中的一部分 . 80年代末期,Softice出现在舞台上,给受保护程序及它们的开发者带来不少的麻烦.从那时候起,Softice就作为黑客调试工具传了下来.但最近Olly在年轻一代破解者中越来越常用. 现在由于分析软件的可能性,与黑客对抗就成了无用的挣扎.
调试器如何工作
所有的调试器都无非属于下面两类:
.使用处理器的调试能力
.独立的模仿处理器,监视被测试程序的运行
调试器会检查标志寄存器的陷阱位是否为1.如果是,就在每条指令后自动生成一个INT 1 调试异常,并且将控制权交给调试器.由此可知,代码可以通过分析标志寄存器来检测跟踪(调试).有四个调试寄存器:1. DR0 2. DR1 3. DR2 4. DR3 .
它们存储了四个检测点的线性地址.当然还有一个寄存器保存了每个点的条件,它就是DR7.当任何一个条件为真时,处理器就会抛出INT1异常,控制权也就交给了调试器,有四个条件:1.一条指令被执行2.某个内存地址的内容被改变了3.某个内存地址被读取或改变,但没有被执行4.一个输入输出端口被引用
现在要讨论的是软件断点.如果将一个字节的代码--0xCC插入到一条指令前,再试图执行它就会引发INT 0x3异常.为了发现是否至少有一个点被设置了断点,程序仅需计算其校验和就可以了.为了做到这一点,可以使用MOV, MOVS, LODS,POP, CMP, CMPS或其他任何指令;
windows平台的调试器主要分为两大类:1 用户模式(user-mode)调试器:它们都基于win32 Debugging API,有使用方便的界面,主要用于调试用户模式下的应用程序。这类调试器包括Visual C++调试器、WinDBG、BoundChecker、Borland C++ Builder调试器、NTSD等。2 内核模式(kernel-mode)调试器:内核调试器位于CPU和操作系统之间,一旦启动,操作系统也会中止运行,主要用于调试驱动程序或用户模式调试器不易调试的程序。这类调试器包括WDEB386、WinDBG和softice等。其中WinDBG和softice也可以调试用户模式代码。国外一位调试高手曾说,他70%调试时间是在用VC++,其余时间是使用WinDBG和softice。(批:很显然,VC6的调试器应该过时了,VS2005的调试器应该更先进.但是,使用方法上还是有点可以参考的. ) 由于是在循环体内,如果在E行设置断点,可能需要按F5(GO)许多次。这样手要不停的按,很痛苦。使用VC6断点修饰条件就可以轻易解决此问题。然后选择D行所在的断点,然后点击condition按钮,在弹出对话框的最下面一个编辑框中输入一个很大数目,具体视应用而定,这里1000就够了。3 按F5重新运行程序,程序中断。Ctrl+B打开断点框,发现此断点后跟随一串说明:...487 times remaining。意思是还剩下487次没有执行,那就是说执行到513(1000-487)次时候出错的。因此,我们按步骤2所讲,更改此断点的skip次数,将1000改为513。(批:此功能是新功能) .在“Enter the expression to be evaluated:”下面,可以输入一些条件,当这些条件满足时,断点才启动。譬如,刚才的程序,我们需要i为100时程序停下来,我们就可以输入在编辑框中输入“i==100”。另外,如果在此编辑框中如果只输入变量名称,则变量发生改变时,断点才会启动。这对检测一个变量何时被修改很方便,特别对一些大程序。
二 数据断点(Data Breakpoint):我们可以首先在A行设置普通断点,F5运行程序,程序停在A行。然后我们再设置一个数据断点。F5继续运行,程序停在B行,说明B处代码修改了szName1。B处明明没有修改szName1呀?但调试器指明是这一行,一般不会错,所以还是静下心来看看程序,哦,你发现了:szName2只有4个字节,而strcpy了7个字节,所以覆写了szName1。数据断点不只是对变量改变有效,还可以设置变量是否等于某个值。(批:这个功能和上面好象有点重复了.但可以看出,数据断点相对位置断点一个很大的区别是不用明确指明在哪一行代码设置断点。)
三 其他
1 在call stack窗口中设置断点,选择某个函数,按F9设置一个断点。这样可以从深层次的函数调用中迅速返回到需要的函数。
2 Set Next StateMent命令(debug过程中,右键菜单中的命令)
此命令的作用是将程序的指令指针(EIP)指向不同的代码行。譬如,你正在调试上面那段代码,运行在A行,但你不愿意运行B行和C行代码,这时,你就可以在D行,右键,然后“Set Next StateMent”。调试器就不会执行B、C行。只要在同一函数内,此指令就可以随意跳前或跳后执行。灵活使用此功能可以大量节省调试时间。
watch窗口支持丰富的数据格式化功能。如输入0x65,u,则在右栏显示101。实时显示windows API调用的错误:在左栏输入@err,hr。在watch窗口中调用函数。提醒一下,调用完函数后马上在watch窗口中清除它,否则,单步调试时每一步调试器都会调用此函数。 4 messages断点不怎么实用。基本上可以用前面讲述的断点代替。调试最重要的还是你要思考,要猜测你的程序可能出错的地方,然后运用你的调试器来证实你的猜测。
一.高级断点语法
高级断点语法由两部分组成:1.上下文部分.2.位置,表达式,变量或Windows消息条件.
用函数,源文件和二进制模块来指定上下文,上下文的表示方法:{[函数],[源文件],[二进制模块]}
如在TEST.CPP的20行设一位置断点,语法为:{,TEST.CPP,}.20,如A.DLL或B.DLL都使用了该行,又只想在B.DLL的调用中触发,则必须使用:{,TEST.CPP,B.DLL}.20.
VC调试器中可直接输入上下文语法:Breakpoints对话框的Location选项卡BreakAt编辑框中.更容易的方法是使用BreatAt框右的箭头打开菜单,选择Advanced项,然后在Context框中输入断点的相应信息. 如想在一个绝对地址上中断,直接在BreakAt框中输入地址就行.
将函数名输入BreadAt框中.如果是C++代码,同时还需要类限定符.支持重载了的函数,调试器会列出所有满足条件的函数供选择,如输入时提供足够的信息,完全可略过选择过程.如输入:"CString::operator=(const char *)"可唯一确定要中断的函数.(批:我个人觉得函数中断似乎意义不大.F9就可以了)
可能用DUMPBIN程序查看这个名称:DUMPBIN /EXPORTS DLLname.例:在LoadLibraryA中设置中断:"{,,Kernel32.dll}LoadLibraryA".
如装入了符号,则要根据输出函数和调用协议来计算函数名.如上例,LoadLibraryA使用__stdcall调用协议,据该协议,函数名以下划线为前缀,所跟有进栈的字节数为后缀的@号.一般说来,参数个数*4,就是参数占用栈空间的总字节数,LoadLibary的名称便是:_LoadLibraryA@4,故最后的语法是:或"{,,Kernel32.dll}_LoadLibraryA@4"
附:常用的调用协议
1、__stdcall调用约定相当于16位动态库中经常使用的PASCAL调用约定。在32位的VC++5.0中PASCAL调用约定不再被支持(实际上它已被定义为__stdcall。除了__pascal外,__fortran和__syscall也不被支持),取而代之的是__stdcall调用约定。两者实质上是一致的,即函数的参数自右向左通过栈传递,被调用的函数在返回前清理传送参数的内存栈,但不同的是函数名的修饰部分(关于函数名的修饰部分在后面将详细说明)。
_stdcall是Pascal程序的缺省调用方式,通常用于Win32 Api中,函数采用从右到左的压栈方式,自己在退出时清空堆栈。VC将函数编译后会在函数名前面加上下划线前缀,在函数名后加上"@"和参数的字节数。
2、C调用约定(即用__cdecl关键字说明)按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于传送参数的内存栈是由调用者来维护的(正因为如此,实现可变参数的函数只能使用该调用约定)。另外,在函数名修饰约定方面也有所不同。
_cdecl是C和C++程序的缺省调用方式。每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。函数采用从右到左的压栈方式。VC将函数编译后会在函数名前面加上下划线前缀。是MFC缺省调用约定。
3、__fastcall调用约定是“人”如其名,它的主要特点就是快,因为它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈),在函数名修饰约定方面,它和前两者均不同。
_fastcall方式的函数采用寄存器传递参数,VC将函数编译后会在函数名前面加上"@"前缀,在函数名后加上"@"和参数的字节数。
4、thiscall仅仅应用于“C++”成员函数。this指针存放于CX寄存器,参数从右到左压。thiscall不是关键词,因此不能被程序员指定。
5、naked call采用1-4的调用约定时,如果必要的话,进入函数时编译器会产生代码来保存ESI,EDI,EBX,EBP寄存器,退出函数时则产生代码恢复这些寄存器的内容。naked call不产生这样的代码。naked call不是类型修饰符,故必须和_declspec共同使用。
关键字 __stdcall、__cdecl和__fastcall可以直接加在要输出的函数前,也可以在编译环境的Setting...\C/C++ \Code Generation项选择。当加在输出函数前的关键字与编译环境中的选择不同时,直接加在输出函数前的关键字有效。它们对应的命令行参数分别为/Gz、/Gd和/Gr。缺省状态为/Gd,即__cdecl。
要完全模仿PASCAL调用约定首先必须使用__stdcall调用约定,至于函数名修饰约定,可以通过其它方法模仿。还有一个值得一提的是WINAPI宏,Windows.h支持该宏,它可以将出函数翻译成适当的调用约定,在WIN32中,它被定义为__stdcall。使用WINAPI宏可以创建自己的APIs。
(批:在64位系统中好象只剩一种了,但知道点也好.)
2.条件表达式.
只有表达式为真时触发.Breakpoint框Condition按钮,选第一个编辑框,输入表达式即可.规则:
.只可使用C类型比较运算符. .表达式中不能调用任何函数. .表达式中不能包含任何宏值. (批:上面没提到过,补充一下)
表达式为@TIB=Thread Infomation Block Linear Address,则程序只在该特定线程中才会中断.(批:这个好象太有用了,我现在就老是在线程间跳来跳去的.).例:线程@TIB地址值为0E000,则输入"@TIB==0xE000"(批:但是如何知道线程的地址值呢?可能只要随便设置个断点看看就可以了.),则在切换到该线程时中断.对W98,可用@FS=thread specific value. 如在某特定错误后中断,则可用@ERR,如"@ERR=2"表示在最后错误为ERROR_FILE_NOT_FOUND.除@CLK外,所有可在WATCH窗口中使用的伪寄存器均可用于条件表达式.
调试器可监控某一地址和该地址上的1,2或4字节的内容. 确保利用硬件调试寄存器的最好方法是使用表达式和数据更改位置的实际地址值.例如:g_szGlobal是全局数组指针,地址为0x5000,则在Breakpoint对话框中DATA选项卡中将表达式断点设为"*(char*))0x5000=='G'",但如果写为"WO(0x5000)=='G',则用不到硬件调试寄存器,会单步执行每条指令.
六.WINDOWS消息断点.
Breakpoint框的Message页.需要指定一个窗口过程,注意:MFC世界中AfxWndProc是多数窗口的一个窗口过程.
WinDbg:(它和VS里的调试器有什么区别呢?难道只是因为它不要钱?)
WinDbg是微软开发的免费源码级调试工具。Windbg可以用于Kernel模式调试和用户模式调试,还可以调试Dump文件。由于大部分程序员不需要做Kernel模式调试, 我在这篇文章中不会介绍Kernel模式调试。Kernel模式调试对学习Windows核心极有帮助。如果你对此感兴趣,可以阅读Inside Windows 2000和Windbg所带的帮助文件。在同一个进程中可能有多个线程。~命令可以用来显示和切换线程(批:似乎这是我唯一感兴趣的.看完之后,我只想说一局,OH,MY GOD,我想我一般不会去使用它)。
Visual C++的调试功能是从早期的调试工具CodeBase发展而成,这个名叫CodeBase调试工具研究成为一种工业标准。(批:呵,新鲜,第一次听到)
首先我们看一下使用异常处理的几种情况:
A. 用来处理非致命的错误
B. 对API函数的参数合法性的检验(假设参数都是合法的,只有遇到异常的时候进行合法性检验)
C. 处理致命错误(退出时最好的选择,但是有的时候可以用异常处理函数在程序退出前释放资源,删除临时文件等,甚至可以详细记录产生异常的指令位置和环境)
D. 处理“计划内”的异常(我们可能更关心这种情况)
接着我们看看Windows下异常处理的两种方式:1使用筛选器2 SEH异常处理
一、 使用筛选器(批:这个就稍微看一下吧.)
因为这里我要重点关注的是SEH的处理方式,所以还是简单的提一下筛选器处理方式。筛选器异常处理是通过异常回调函数来指定程序处理异常。这种方式的回调函数必须是唯一的,设置新的回调函数后以前的将失效。适用于进程范围。看一下这个函数的定义
Invoke SetUnhandledExecpionFilter,offset_Handler
Mov lpPrevHandler,eax
回调函数的格式:
_Handlerproc pExecptionInfo
看看pExecptionInfo这个指针参数指向的一个数据结构
EXCEPTION_POINTERS STRUCT (批:好象这里作者写错了,有时间可以去查查)
pExceptionRecord DWORD ?
ContextRecord DWORD ?
EXCEPTION_POINTERS ENDS
下面介绍 EXCEPTION_RECORD和CONTEXT结构的定义:
;//===================== 以下是两个成员的详细结构=========================
EXCEPTION_RECORD STRUCT
ExceptionCode DWORD ? ;//异常码
ExceptionFlags DWORD ? ;//异常标志
pExceptionRecord DWORD ? ;//指向另外一个EXCEPTION_RECORD的指针
ExceptionAddress DWORD ? ;//异常发生的地址
NumberParameters DWORD ? ;//下面ExceptionInformation所含有的dword数目
ExceptionInformation DWORD EXCEPTION_MAXIMUM_PARAMETERS dup(?)
EXCEPTION_RECORDENDS ;//EXCEPTION_MAXIMUM_PARAMETERS ==15
;//=============================具体解释================================
ExceptionCode 异常类型,SDK里面有很多类型,你可以在windows.inc里查找STATUS_来找到更多的异常类型,下面只给出hex值,具体标识定义请查阅windows.inc,你最可能遇到的几种类型如下:
C0000005h----读写内存冲突
C0000094h----非法除0
C00000FDh----堆栈溢出或者说越界
80000001h----由Virtual Alloc建立起来的属性页冲突
C0000025h----不可持续异常,程序无法恢复执行,异常处理例程不应处理这个异 常
C0000026h----在异常处理过程中系统使用的代码,如果系统从某个例程莫名奇妙的返回,则出现此代码, 如果RtlUnwind时没有Exception Record参数也同样会填入这个代码
80000003h----调试时因代码中int3中断
80000004h----处于被单步调试状态
注:也可以自己定义异常代码,遵循如下规则:
____________________________________________________________________
位: 31~30 29~28 27~16 15~0
____________________________________________________________________
含义: 严重程度 29位 功能代码 异常代码
0==成功 0==Mcrosoft MICROSOFT定义 用户定义
1==通知 1==客户
2==警告 28位
3==错误 被保留必须为0
ExceptionFlags 异常标志
0----可修复异常
1----不可修复异常
2----正在展开,不要试图修复什么,需要的话,释放必要的资源
pExceptionRecord 如果程序本身导致异常,指向那个异常结构
ExceptionAddress 发生异常的eip地址
ExceptionInformation 附加消息,在调用RaiseException可指定或者在异常号为C0000005h即内存异常时含义如下
第一个dword 0==读冲突 1==写冲突
第二个dword 读写冲突地址
;//================================解释结束============================
off.
CONTEXT STRUCT ; _
ContextFlags DWORD ? ; | +0
iDr0 DWORD ? ; | +4
iDr1 DWORD ? ; | +8
iDr2 DWORD ? ; >调试寄存器 +C
iDr3 DWORD ? ; | +10
iDr6 DWORD ? ; | +14
iDr7 DWORD ? ; _| +18
FloatSave FLOATING_SAVE_AREA <> ;浮点寄存器区 +1C~~~88h
regGs DWORD ? ;--| +8C
regFs DWORD ? ; |\段寄存器 +90
regEs DWORD ? ; |/ +94
regDs DWORD ? ;--| +98
regEdi DWORD ? ;____________ +9C
regEsi DWORD ? ; | 通用 +A0
regEbx DWORD ? ; | 寄 +A4
regEdx DWORD ? ; | 存 +A8
regEcx DWORD ? ; | 器 +AC
regEax DWORD ? ;_______|___组_ +B0
regEbp DWORD ? ;++++++++++++++++ +B4
regEip DWORD ? ; |控制 +B8
regCs DWORD ? ; |寄存 +BC
regFlag DWORD ? ; |器组 +C0
regEsp DWORD ? ; | +C4
regSs DWORD ? ;++++++++++++++++ +C8
ExtendedRegisters db MAXIMUM_SUPPORTED_EXTENSION dup(?)
CONTEXT ENDS
;//============================以上是两个成员的详细结构============
程序使用筛选器异常处理时可以通过查看上面结构中的regEip来找到产生异常的地址!调试的时候可以改变EIP的值以达到越过异常程序,转到“安全”的地方。
最后看一下筛选器异常处理回调函数的返回值
EXECPTION_EXECUTE_HANDLER 1;进程被终止,终止前不会出现提示错误的对话框
EXECPTION_CONTINUE_SEARCH 0;同样终止程序,显示错误对话框
EXECPTION_CONTINUE_EXECUTION -1;系统将CONTECT设置回去,继续执行程序
使用筛选器程序是最简单的处理异常方法,不足:1 不便于封装。2 处理是全局性的也就是无法对每个线程或子程序设置一个私有的异常处理程序进行异常处理。
进入正题:SEH异常处理
首先解释一下什么是SEH异常处理:SEH("Structured Exception Handling",即结构化异常处理.是操作系统提供给程序设计者的强有力的处理程序错误或异常的武器。
下面结合冷雨飘心的一个SEH异常处理程序来说明具体的用法:
;(批:注释)//====================================================================
;// ex. 2,by Hume,2001 线程相关的异常处理
;//====================================================================
.386(批:好象汇编程序都要写个头的,没怎么研究过汇编)
.model flat, stdcall
option casemap :none ; case sensitive
include hd.h ;//相关的头文件,你自己维护一个吧
;//============================
.data(批:可能是说以下是数据区吧,我也不太懂)
szCap db "By Hume[AfO],2001...",0
szMsgOK db "It's now in the Per_Thread handler!",0
szMsgERR1 db "It would never Get here!",0
buff db 200 dup(0)
.code(批:可能是说代码区)
_start: (批:函数的开始,这些应该都是给汇编编译器处理的吧,估计不涉及生成机器代码)
;//========prog begin====================
ASSUME FS:NOTHING
push offset perThread_Handler
push fs:[0]
mov fs:[0],esp ;//建立SEH的基本ERR结构,如果不明白,就仔细研究一下吧
xor ecx,ecx
mov eax,200
cdq ;//双字扩展到四个字节,因为是除法
div ecx
;//以下永远不会被执行
invoke MessageBox,NULL,addr szMsgERR1,addr szCap,MB_OK+MB_ICONINFORMATION
pop fs:[0]
add esp,4
invoke ExitProcess,NULL
;//============================
perThread_Handler: (批:函数名?)
invoke MessageBox,NULL,addr szMsgOK,addr szCap,MB_OK+MB_ICONINFORMATION
mov eax,1 ;//ExceptionContinueSearch,不处理,由其他例程或系统处理
;mov eax,0 ;//ExceptionContinueExecution,表示已经修复CONTEXT,可从异常发生处继续执行
ret ;//这里如果返回0,你会陷入死循环,不断跳出对话框....
;//=============================Prog Ends==============
end _start
程序本身很简单,注释也很详细。我们来看看是如何注册回调函数的
push offset perThread_Handler (批:回调函数的写法和普通函数有区别吗?)
push fs:[0]
mov fs:[0],esp
仅仅三个语句就解决了~那么为什么要用fs这个段寄存器呢?这里又涉及一个重要的内容:TIB(Thread Information Block线程信息块)。我们来看看这个重要的数据结构(引用了《罗聪浅谈利用SEB实现反跟踪》的部分内容)
TEB(Thread Environment Block) 在 Windows 9x 系列中被称为 TIB(Thread Information Block),它记录了线程的重要信息,而且每一个线程都会对应一个 TEB 结构。 Matt Pietrek 大牛已经给我们列出了它的结构,我就不多说啦,见下:(摘自 Matt Pietrek 的 Under The Hood - MSJ 1996)
//===========================================================
// file: TIB.H
// Author: Matt Pietrek
// From: Microsoft Systems Journal "Under the Hood", May 1996
//===========================================================
#pragma pack(1)
typedef struct _EXCEPTION_REGISTRATION_RECORD
{
struct _EXCEPTION_REGISTRATION_RECORD * pNext;
FARPROC pfnHandler;
} EXCEPTION_REGISTRATION_RECORD, *PEXCEPTION_REGISTRATION_RECORD;
typedef struct _TIB
{
PEXCEPTION_REGISTRATION_RECORD pvExcept; // 00h Head of exception record list
PVOID pvStackUserTop; // 04h Top of user stack
PVOID pvStackUserBase; // 08h Base of user stack
union // 0Ch (NT/Win95 differences)
{
struct // Win95 fields
{
WORD pvTDB; // 0Ch TDB
WORD pvThunkSS; // 0Eh SS selector used for thunking to 16 bits
DWORD unknown1; // 10h
} WIN95;
struct // WinNT fields
{
PVOID SubSystemTib; // 0Ch
ULONG FiberData; // 10h
} WINNT;
} TIB_UNION1;
PVOID pvArbitrary; // 14h Available for application use
struct _tib *ptibSelf; // 18h Linear address of TIB structure
union // 1Ch (NT/Win95 differences)
{
struct // Win95 fields
{
WORD TIBFlags; // 1Ch
WORD Win16MutexCount; // 1Eh
DWORD DebugContext; // 20h
DWORD pCurrentPriority; // 24h
DWORD pvQueue; // 28h Message Queue selector
} WIN95;
struct // WinNT fields
{
DWORD unknown1; // 1Ch
DWORD processID; // 20h
DWORD threadID; // 24h
DWORD unknown2; // 28h
} WINNT;
} TIB_UNION2;
PVOID* pvTLSArray; // 2Ch Thread Local Storage array
union // 30h (NT/Win95 differences)
{
struct // Win95 fields
{
PVOID* pProcess; // 30h Pointer to owning process database
} WIN95;
} TIB_UNION3;
} TIB, *PTIB;
#pragma pack()
让我们抬头看看上面的 Matt Pietrek 的代码,其中有这么一行:
PEXCEPTION_REGISTRATION_RECORD pvExcept; // 00h Head of exception record list
注意到 PEXCEPTION_REGISTRATION_RECORD 这个定义,它表示 pvExcept 这个变量正是 exception record list 的入口,这个入口位于整个结构的 0 偏移处。同时,在 M 的 Intel i386 Windows NT/2K/XP 内核中,每当创建一个线程,OS 均会为每个线程分配 TEB ,而且 TEB 永远放在 fs 段选择器指定的数据段的 0 偏移处。
这样一来,你就明白了 SEH 注册的偏移为什么是在 fs:[0] 了吧?
事实上 Windows 系统都是通过这种方法来为应用程序提供信息的,比如有这样的例子:
struct _tib *ptibSelf; // 18h Linear address of TIB structure (批:程序的例子里怎么都是说TIB,好象是TEB才对.)
DWORD threadID; // 24h
Windows 提供了一个 API :GetCurrentThreadID(),它的内部工作原理其实是这样的:(利用了上面的这两个地址)
mov eax, fs:[18h] ;因为 18h 偏移处是 TIB 结构的线性偏移地址(批:取TIB结构?)
mov eax, [eax + 24h] ;因为 24h 偏移处是 threadID 的地址(批:取threadID 的地址?)
ret ;把 eax 中储存的 threadID 地址返回
注:为什么要声明assume fs:nothing?因为masm编译器默认将fs段寄存器定义为error,所以程序在使用fs前必须将它启动!
接下来看看SEH的回调函数
_Handler proc _lpExecptionRecord, _lpSEH,lp_context,lp_DispatcherContext
_lpExecptionRecord指向一个EXECPTION_RECORD结构。
lp_context 指向一个CONTEXT结构。
_lpSEH 指向注册回调函数时使用的EXXCEPTION_REGISTRATION结构的地址。
返回值有四种取值:
ExecptionContinueExecution ( 0 :系统将线程环境设置为_lpContext指向的CONTEXT结构并继续执行。
ExceptionContinueSearch(1):回调函数拒绝处理这个异常,系统通过EXECPTION_REGISTRATION结构的prev字段得到前一个回调函数的地址并调用它。
ExecptionNestedExecption (2):发生异常嵌套。
ExecptionCollidedUnwind (3):异常展开操作。这一个部分不做多讲,有兴趣的可以看看罗云彬的书,其实是很重要的一部分。
如果一个程序既有筛选器异常处理又有SEH异常处理,而且系统还有默认的异常处理机制,那么他们被调用的先后次序是怎么样的呢?
发生异常时系统的处理顺序(by Jeremy Gordon):
1.系统首先判断异常是否应发送给目标程序的异常处理例程,如果决定应该发送,并且目标程序正在被调试,则系统挂起程序并向调试器发送EXCEPTION_DEBUG_EVENT消息.呵呵,这不是正好可以用来探测调试器的存在吗?
2.如果你的程序没有被调试或者调试器未能处理异常,系统就会继续查找你是否安装了线程相关的异常处理例程,如果你安装了线程相关的异常处理例程,系统就把异常发送给你的程序seh处理例程,交由其处理.
3.每个线程相关的异常处理例程可以处理或者不处理这个异常,如果他不处理并且安装了多个线程相关的异常处理例程,可交由链起来的其他例程处理.
4.如果这些例程均选择不处理异常,如果程序处于被调试状态,操作系统仍会再次挂起程序通知debugger.
5.如果程序未处于被调试状态或者debugger没有能够处理,并且你调用SetUnhandledExceptionFilter安装了最后异 常处理例程的话,系统转向对它的调用.
6.如果你没有安装最后异常处理例程或者他没有处理这个异常,系统会调用默认的系统处理程序,通常显示一个对话框, 你可以选择关闭或者最后将其附加到调试器上的调试按钮.如果没有调试器能被附加于其上或者调试器也处理不了,系统就调用ExitProcess终结程序.
7.不过在终结之前,系统仍然对发生异常的线程异常处理句柄来一次展开,这是线程异常处理例程最后清理的机会.
说了这么多你也许会问SEH异常处理到底有什么用处呢?呵呵,且听小生慢慢道来~~~
第一道菜:病毒程序巧用SEH
这里简单的说一下如何利用SEH异常处理程序来躲避下毒软件的反病毒引擎。一个反病毒引擎在一个程序运行的时候会模拟程序的代码,当发现程序代码的疑点比较多的时候会报告成病毒。看看下面这段程序:
start:call Set_SEH;这句其实就是 push offset CONTINUE
; JMP Set_SEH
CONTINUE:mov esp, [esp+8]; [ESP+8]存储的是旧的堆栈地址。
push offset Start_Virus ;----_ 把Start_Virus 的地址压栈,当作返回地址
ret;----跳到Start_Virus去,是不是很magic?
Set_SEH:sub edx, edx ;Edx =0
Assume fs:nothing
push dword ptr fs:[edx];把指去 _EXCEPTIONAL_REGISTRATION_RECORD 结构的指针入栈
mov fs:[edx], esp;安装一个seh
mov [edx],edx;引起一个内存读写冲突,发生异常因为edx=0
;如果反病毒引擎不处理异常,不进入seh 处理程序(即 CONTINUE: ,继续模
;拟下个指令,也就是jmp start,那么就进入一个死循环,可能会引起死机。
jmp start
Start_Virus: .....
是不是很简单呢?就是让反病毒引擎不处理这个人为的异常时进入死循环~!!
第二道菜:TEB反跟踪初探
如果你的记性够好的话一定记得上面介绍过的TEB(TIB)线程信息块结构中有这么一句:(批:TEB与TIB相同?)
PVOID* pProcess; // 30h Pointer to owning process database
这个偏移地址处的内容非常有用,它指向本线程的拥有者的 PDB(Process Database) 的线性地址。当你用动态调试器,例如 OllyDbg 的时候,调试器是把调试的对象作为一个子线程进行跟踪的,在这种情况下,被调试的对象的“拥有者”就是调试器本身,也就是说,它的 TEB 的 30h 处的偏移指向的内容肯定不为 0 ,这样,我们就可以利用这一点,判断 30h 偏移指向的内容,来判断是否有调试器跟踪。
最后给出一个 Anti-Debug 的例子程序,用 MASM 编译完成后,请用 OllyDbg 来加载调试一下,看看与正常的运行结果有什么不同。
;*********************************************************
;程序名称:演示利用 TEB 结构进行 Anti-Debug
; 请用 OllyDbg 进行调试
;适用OS:Windows NT/2K/XP
;作者:罗聪
;日期:2003-2-9
;出处:http://www.LuoCong.com(老罗的缤纷天地)
;注意事项:如欲转载,请保持本程序的完整,并注明:
;转载自“老罗的缤纷天地”(http://www.LuoCong.com)
;*********************************************************
.386
.model flat, stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
.data
szCaption db "Anti-Debug Demo by LC, 2003-2-9", 0
szDebugged db "Hah, let me guess... U r dEBUGGINg me! ", 0
szFine db "Good boy, no dEBUGGEr detected!", 0
.code
main:
assume fs:nothing
mov eax, fs:[30h] ;指向 PDB(Process Database)
movzx eax, byte ptr [eax + 2h];无符号数带零扩展
or al, al
jz _Fine
_Debugged:
push MB_OK or MB_ICONHAND
push offset szCaption
push offset szDebugged
jmp _Output
_Fine:
push MB_OK or MB_ICONINformATION
push offset szCaption
push offset szFine
_Output:
push NULL
call MessageBoxA
invoke ExitProcess, 0
end main
第三道菜:利用SEH执行shellcode
假设异常处理例程入口00401053,程序刚开始执行时esp是0012ffc4,以前的fs:[0]是0012ffe0
建立了TIB结构的第一个成员后堆栈的情况如下:
内存低地址
| E0 |12ffbc(esp)
| FF |
| 12 | --ERR结构的第一个成员
|_00_|
| 53 |12ffc0
| 10 |
| 40 | --ERR结构的第二个成员
| 00 |
内存高地址
好了然后程序CALL一个函数,函数里面有一个局部变量并且在往其分配的空间中写入的数据时产生溢出.这时堆栈如下
____
| |12f000 局部变量分配的空间,并且向12ffc0方向溢出了.
| |
....
....
|_EBP|12ffb4 函数中保存老的EBP
| xx |
| xx |
| xx |
|_EIP|12ffb8 call函数时EIP进栈
| xx |
| xx |
|_xx_|
| E0 |12ffbc(esp) {当SEH起作用的时候EBX刚好指向这个地址(也可说总是指向当前ERR结构)}
| FF |
| 12 | --ERR结构的第一个成员
|_00_|
| 53 |12ffc0
| 10 |
| 40 | --ERR结构的第二个成员
|_00_|
| |12ffc4
继续看,假设溢出代码一直到了12ffc4,然后call的函数该返回了,因为保存的EIP被溢出代码代替所以程序出错(不会不出错吧?),这样ESH开始起作用了(注:在这期间系统要执行一些操作,所以EBX才会指向当前ERR).这样一来程序就会跳到12ffc0里的地址去执行!而12ffc0里的东东早已不是原来的00401053了.这样我们不就改变了程序的流向了么.12ffc0中该写入什么内容呢?应是内存中JMP EBX的代码的地址.这样跳了3下后最终就会跳到12ffbc去执行.这个四字节可是宝贵的啊现在假设JMP EBX这个指令在内存中的地址是0x77e33f4d
那下具体看一下现在堆栈的情况:
| EB |12ffbc(esp) {当ESH起作用的时候EBX刚好指向这个地址(也可说总是指向当前ERR结构)}
| 06 |
| 90 | --ERR结构的第一个成员,执行JMP EBX后就到这儿来执行了(EB 06是短跳转JMP 12FFC4的机器码)
|_90_| 后面的90是nop空指令的机器码.
| 4D |12ffc0
| 3F |
| E3 | --ERR结构的第二个成员,出错处理函数的入口地址(现在成了JMP EBX的地址)
|_77_|
| |12ffc4
....
好现在来看看12ffc4里面有些什么代码.(简单的说这段代码的作用是计算真正的shellcode的起始地址,然后跳过去执行.
低地址
| |12f000(shellcode开始地址)
....
....
| 81 |12ffc4
| C3 | add ebx,FFFFF03Ch(ebx=12ffc4,指令长度6,作用计算计算shellcode地址)
| 3C |
| F0 |
| FF |
| FF |
| FF |12ffca jmp ebx
| D3 |
高地址
测试程序
-------------------------SEH.ASM------------------
.386
.model flat,stdcall
option casemap:none
include ../include/user32.inc
includelib ../lib/user32.lib
include ../include/kernel32.inc
includelib ../lib/kernel32.lib
.data
hello db '利用一个读INI文件的API来演示WIN2000本地溢出',0
lpFileName db '.\seh.ini',0
lpAppName db 'iam',0
lpKeyName db 'czy',0
lpDefault db 'ddd',0
szCap db "SEH TEST",0
szMsgOK db "OK,the exceptoin was handled by final handler!",0
szMsgERR1 db "It would never Get here!",0
.code
testov proc
local lpReturnedString[2224] : byte ;返回的字串搞成本地变量这样就和C语言一样了,它是在栈中
invoke GetPrivateProfileString,offset lpAppName,offset,lpKeyName,offset lpDefault,ADDR lpReturnedString,2249,offset lpFileName
invoke MessageBox,0,addr lpReturnedString,addr lpReturnedString,1
ret
testov endp
start:
ASSUME fs:NOTHING
invoke MessageBox,0,addr szMsgERR1,addr szCap,30h+1000h ;下断点
push offset Final_Handler ;压入正常的出错处理程序入口地址
push FS:[0] ;把前一个TIB的地址压入
mov fs:[0],esp
call testov
pop fs:[0] ;还原FS:[0]
Final_Handler: ;由于溢出了下面的代码不会被执行.
invoke MessageBox,0,addr szMsgOK,addr szCap,30h
invoke ExitProcess,0
mov eax,1
ret
end start
?
-----------------end-------------
1如何更好的在内存中找JMP EBX的代码:
在softice中执行S 10:0 L FFFFFFFF FF D3就可以了,但实际上这样找到的
地址可能不能执行代码.所以用下面的方法:
map32 kernel32(在当前进程中查找映射的kernel32 DLL的信息)
一般有如下显示:
Owner Obj Name Obj# Address Size TYPE
kernel32 .text 0001 001b:77b61000 0005d1ae code RO
......
然后
S 77b61000 L 5d1ae FF D3
如果显示如下说明找到了:
Pattern Found at 0023:77e61674 ....
2)关于缓冲区的大小的问题:
利用SEH的办法就起码要设成1000个字节多,你的shellcode才不会被不知哪来的数据覆盖!
这道菜czy做的不好吃:(我感觉理解起来有些困难~!因为关于缓冲区溢出自己接触的太少,不过好东西要保留的,以后回过头看!
第四道菜:用 SEH 技术实现 API Hook
这一部分不想展开了,给大家一个链接吧。
http://www.luocong.com/articles/show_article.asp?Article_ID=25
在SEH异常处理链中最后一个被装载的SEH异常处理程序总是被第一个调用,想想如果自己花了一个星期才写出来一个异常处理程序,能够完美处理所有异常,并希望异常全部由你来处理,但很不幸,比如你调用了一个外部模块,而这个模块自己安装了一个ugly的seh处理例程,他的动作是只要有异常发生就简单地终止程序,哈哈,那就死悄悄了。又比如你想在你的加壳程序里面加密目标程序代码段,然后发生无效指令异常的时候用你自己安装的处理句柄来解密代码段继续执行,听起来这的确是一个好主意,但遗憾的是大多数C/C++代码都用_try{}_except{}块来保证其正确运行,而这些异常处理例程是在你壳注册的例程之后安装的,因而也就在链的前面,无效指令一执行,首先是C/C++编译器本身提供的处理例程或者程序其他的异常处理例程来处理,可能简单结束程序或者....
好象和调试有点跑题了,但是现在的程序里,线程一般是少不了的,对调试也有好处,其实我也只是做个记录,很多细节也没看,只是觉得以后会要用的.所以写记录在这里.