分类 调试 下的文章

windows系统里,为了保证系统内核的强壮和稳定,为了保证用户程序的强壮和稳定,提供了异常处理机制,来帮助程序员和系统使用人员处理异常。简单来说,当CPU执行代码时,发生异常,会把异常告知操作系统,操作系统首先会让程序自身处理这个异常,程序自身有能力(程序中注册的有异常处理函数)处理,程序就继续运行;程序自身没有能力处理(程序中没有注册异常处理函数),这个异常还没有被处理,就有操作系统来处理,就提示用户是调用调试器调试还是结束程序。如果在有调试器参与的情况下,程序出现异常,操作系统不会把异常处理权交给程序自身而是调试器,调试器可以把处理权继续转交给程序,也可以自己处理。为了完成前面讲的过程,Windwos信息提供了SEH和VEH两种处理机制。下面分别简单介绍。
一、SEH异常处理机制

SEH是windows操作系统异常处理机制,叫结构化异常处理,在程序源代码中使用__try,__except,__finally关键字来具体实现。

Windows操作系统(自Windows95起),对每个用户线程,都设立一个异常处理帧链表来处理异常事件。该链表的每个异常处理帧由两个成员组成,分别是链表上一项地址、当前异常处理器地址,组成了结构_EXCEPTION_REGISTRATION_RECORD。异常处理器是指一个处理异常的回调函数(callback function)。线程信息块(thread information block)的开始处(即FS:[0]指向的内存,FS是CPU的一个段寄存器)保存了异常处理帧链表的表头项的地址。程序执行遇到异常事件而中断时,操作系统的RtlDispatchException函数会从FS:[0]指向的链表表头依次调用每个节点包含异常处理回调函数,直到某个异常处理回调函数的返回值为0表示已经处理该异常,该线程可以恢复执行。链表最末一项是操作系统在装入线程时设置的指向kernel32!UnhandledExceptionFilter函数,该函数总是向用户显示“Application error”对话框。上述异常处理器程序及链表,是由用户程序自己安装的。链表各节点保存在程序调用栈(call stack)上。Windows异常处理机制支持嵌套异常的处理,即在执行异常处理回调函数时再次发生异常。这种情况下仍遵照普通异常处理机制,操作系统RtlDispatchException函数再入处理新出现的嵌套的异常。嵌套的异常的处理函数得到DispatcherContext参数值即为在执行时发生了新异常的异常帧的地址。

当我们用Windbg打开一个exe时,调试器第一次中断:

 

输入kb查看当前栈如下:

0:000>kb#ChildEBP RetAddr  Args to Child
00 00fff8bc 77d498e0 5e7dcb19 0105b000 00000000 ntdll!LdrpDoDebuggerBreak+0x2b
01 00fffb18 77d05257 5e7dcb71 00000000 00000000 ntdll!LdrpInitializeProcess+0x1b20
02 00fffb70 77d05151 00000000 00000000 00000000 ntdll!_LdrpInitialize+0xb0
03 00fffb7c 00000000 00fffb90 77ca0000 00000000 ntdll!LdrInitializeThunk+0x11

一、什么是异常

异常指的是在程序运行过程中发生的异常事件,通常是由外部问题(如硬件错误、输入错误)所导致的。简单来说异常就是对于非预期状况的处理,当我们在运行某个程序时出现了异常状况,就会进入异常处理流程。

二、异常分类

根据异常来源分,可以分为如下两种异常:

  • 硬件异常
    硬件异常是由cpu产生,其中硬件异常又和中断、系统调用等行为有着密切的联系。硬件异常可以分为三种:fault,在处理此类异常时,操作系统会将遭遇异常时的“现场”保存下来(比如EIP、CS等寄存器的值),然后将调用相应的异常处理函数,如果对异常的处理成功了(没成功的情况会在下文中提到),那就恢复到原始现场,继续执行。最经典的fault例子莫过于Page Fault了,在分页机制下,当我们读到某个还未载入到内存的页时,就会触发该异常,操作系统会将该页载入内存,然后重新执行读取该页的指令,这是分页机制实现的重要机制;trap,在处理此类异常时,操作系统会将异常的“下文”保存,在处理异常后,直接执行导致异常的指令的下一条指令。我们在调试过程中常用的断点操作就是基于这类异常的,当我们在某处下断点时调试器会将原本此处的指令对应的十六进制保存下来,然后替换第一个字节替换为0xCC的,也就是int 3,造成断点异常,中断(此处的中断用的是break,而我们一般说的中断是interrupt,请读者务必区分清楚)到调试器,程序在运行到此处就会停止等待下一步的指令,而当我们继续执行时调试器就会将该指令替换为原来的指令,程序也就恢复正常执行了。不知道大家有没有注意过,在进行程序调试时经常会看见hex界面显示大量的“烫烫烫”,这其实是0xcc对应的中文字符,因为这些地址的内容程序并不想让我们访问,一旦我们访问这些地址,就会读到0xcc,程序也就“中断”了;abort,中止异常,主要是处理严重的硬件错误等,这类异常不会恢复执行,会强制性退出。
  • 软件异常
    软件异常是由操作系统或应用程序产生的,它又包含了windows为我们定义好的异常处理和我们自己写的异常处理(各种编程语言中的try-catch结构)。这类异常追根溯源都是基于RaiseException这个用户态API和NtRaiseException的内核服务建立起来的。

三、异常和错误的区别

我们平时编程过程中,异常和错误我们都会遇到。一般在编译期,我们会遇到很多编译错误,在链接期可能也会产生链接错误,这些错误严格来说还算不上程序错误。层序运行时,Windwos系统的各种API执行失败返回时,通过线程局部存储保存的error信息是错误,一般这种错误不会改变线程的执行路径,当然,如果我们不检测处理,最终会导致异常退出。异常一定是程序运行期产生的,异常发生时一定会改变线程的执行路径。