调试和错误处理

第一章、调试和错误处理 [1] 调试

 

所谓调试器,是程序开发者用来观察和修改程序错误的一个程序,本文讲述 WIN32 API对调试器的支持。

 

Win32 API提供了几个函数,可以用来构建一个基础的、事件驱动的调试器。事件驱动的意思是每次当所调试的进程出现特定事件后,就通知调试器,从而允许调试器作适当的响应。

 

某些调试所需的函数,实际上是进程、线程、或者异常处理结构的一部分。下面介绍这些函数。


1、支持调试的进程相关函数

 

CreateProcess允许一个调试器启动一个进程然后调试它。参数 fdwCreate 用于给出调试操作的类型。如果设置了 DEBUG_PROCESS标志,则可以调试这个新的进程和它创建的子进程(线程)。除非创建子进程(线程)的时候没有给出这个标志。如果fdwCreate同时给出DEBUG_ONLY_THIS_PROCESS 标志,则仅仅调试当前进程。

 

一个调试器可以调试另外一个调试器,只需创建相应的进程,并且指定 DEBUG_PROCESS 标志。新创建的被调试进程必须重新创建一个新的进程并且指定该标志。

 

调试器可以通过 OpenProcess函数获取一个进程的id,然后函数DebugActiveProcess使用这个id将调试器和该进程相关联。一般,调试器打开进程的时候需要 PROCESS_VM_READ 和 PROCESS_VM_WRITE 标志,从而允许调试器读写该进程的虚拟内存空间(通过函数 ReadProcessMemory 和 WriteProcessMemory)

 

2、支持调试的线程相关函数

 

CreateThread 函数用来创建一个进程的新线程。调试器一般需要检查和改变线程寄存器的内容。要做到这一点,调试器必须用函数DuplicateHandle 复制线程的句柄,并且指定合适的访问权利(THREAD_GET_CONTEXT, THREAD_SET_CONTEXT,或者两者都用).

如果一个进程具有对一个线程的合适的访问权限,则它可以通过 GetThreadContext/ SetThreadContext 访问线程的寄存器。

 

对所调试的线程还可以设置 THREAD_SUSPEND_RESUME 权限,从而允许调试器通过函数 SuspendThread /ResumeThread 控制线程的运行。

 

3、用于调试的异常处理函数

 

如果一个正在被调试的进程内部出现异常,系统将异常传递给调试器,这叫做“第一次通知”( first-chance notification),系统然后挂起该进程的所有线程。

 

如果调试器没有处理这个异常,则系统开始查找合适的异常处理函数,如果无法找到,则系统再次通知调试器,这叫做“第二次通知”(last-chance notification),如果调试器仍然不予处理,则系统会终止该进程。

 

4、调试一个正在运行的进程

 

要调试一个正在运行的进程,调试器首先要通过OpenProcess取得进程的id,然后用DebugActiveProcess将调试器和进程相关联。这种情况下,只能调试当前活动的进程,无法调试它的子进程。当然,调试器需要拥有对该进程的访问权限。当调试器和进程关联之后,系统会把该进程中(以及/或者它的任何子进程中)出现的所有调试事件通知调试器。

 

5、写调试器的主循环

 

调试器在它的主循环的开始调用 WaitForDebugEvent,该函数将阻塞调试器,直到有调试事件出现。调试事件出现后,系统挂起所调试进程的所有线程,然后通知调试器处理调试事件。调试器可以通过函数 SetDebugErrorLevel 设置错误处理的最小级别,控制系统所传递的调试事件。

 

调试器可以通过下面的函数和用户交互,或者改变所调试的进程的状态。


GetThreadContext
GetThreadSelectorEntry //返回指定选择子或者线程的描述表入口
ReadProcessMemory
SetThreadContext
WriteProcessMemory

 

调试器利用描述表入口完成 段偏移地址到线性虚拟地址的转换。因为ReadProcessMemory和WriteProcessMemory 需要使用线性虚拟地址。

 

调试器会不断读取被调试的进程的内存,并把内存地址中保存的指令写入处理器的指令缓冲区。在写入指令后,调试器可以调用FlushInstructionCache 来执行缓冲区中的指令。

 

调试器在它的主循环的末尾调用函数ContinueDebugEvent ,该函数使被调试的进程继续执行。

 

6、和调试器通讯

 

被调试的进程可以通过函数 OutputDebugString 发送一个字符串给调试器,它产生一个OUTPUT_DEBUG_STRING_EVENT 调试事件。进程可以通过函数 IsDebuggerPresent判断是否存在一个调试器正在调试它。

 

函数 DebugBreak用来在当前进程中产生一个断点异常。一个断点是程序中的一个位置,程序运行到这个位置将停止运行,并允许调试器检查程序的代码、变量、以及寄存器值,然后进行修改,继续或者终止运行。如果当前这个进程没有被调试,则系统开始查找标准异常处理函数。大多数情况下,系统会报告“unhandled breakpoint exception”错误,并终止该进程。

 

函数 FatalExit 中止当前进程,然后把执行权力交给调试器。与 DebugBreak不同的是,它不产生异常。但是由于它常常无法释放进程的内存或者关闭文件等资源,所以尽可能不要使用它。

 
7、调试事件

 

调试事件是指导致系统通知调试器的进程内事件,包括创建进程、创建线程、加载dll,卸载dll,发送调试字符串以及产生异常等。

 

如果调试器正在等待的时候产生了一个调试事件,则系统用描述该事件的信息填充调试器调用的WaitForDebugEvent函数中所给出的那个 DEBUG_EVENT 结构变量。

 

系统通知调试器的同时,它挂起所调试进程的所有线程,直到调试器调用ContinueDebugEvent才恢复该进程的运行。

调试一个进程,可能出现下面的调试事件:


-----------------------------------------
调试事件  
-----------------------------------------
CREATE_PROCESS_DEBUG_EVENT 
当在所调试进程中创建新的进程,或者调试器开始调试一个活动进程时,出现这个事件。系统在进程开始运行于用户模式之前产生这个事件。DEBUG_EVENT 结构包含一个 CREATE_PROCESS_DEBUG_INFO结构,里面包含新进程的句柄,进程影像文件的句柄,进程初始线程的句柄以及其它进程相关信息。
进程句柄具有PROCESS_VM_READ和PROCESS_VM_WRITE访问权利。如果调试器拥有对线程的这些访问权限,则它可以通过ReadProcessMemory 和 WriteProcessMemory  读写进程的内存。
进程影像文件句柄具有GENERIC_READ访问权利。
初始线程句柄具有THREAD_GET_CONTEXT,THREAD_SET_CONTEXT,和THREAD_SUSPEND_RESUME访问权利。如果调试器拥有对线程的这些访问权限,则它可以通过GetThreadContext和SetThreadContext 读写线程的寄存器,通过SuspendThread 和 ResumeThread 挂起和恢复线程。
-----------------------------------------
CREATE_THREAD_DEBUG_EVENT 
  当所调试的进程中创建一个线程或者调试器开始调试活动进程的时候,当线程开始进入user模式之前,产生这个事件。

DEBUG_EVENT 结构包含一个CREATE_THREAD_DEBUG_INFO 结构,给出新的线程的句柄以及线程的起始地址。句柄具有THREAD_GET_CONTEXT,THREAD_SET_CONTEXT和THREAD_SUSPEND_RESUME访问权利。如果调试器拥有对线程的这些访问权限,则它可以通过GetThreadContext和SetThreadContext 读写线程的寄存器,通过SuspendThread 和 ResumeThread 挂起和恢复线程。

-----------------------------------------
EXCEPTION_DEBUG_EVENT

当所调试的进程中出现异常的时候,产生这个事件,可能的异常包括非法内存访问、执行断点、除以0或者其他结构化异常。  
DEBUG_EVENT结构包含一个EXCEPTION_DEBUG_INFO结构,里面给出了导致该事件的异常信息。除了标准异常外,在调试控制台进程的过程中,如果输入ctrl+c,系统会产生一个DBG_CONTROL_C异常,这个异常通常不是给程序处理的,它是专门给控制台程序的调试器的。程序一般不要使用异常处理函数来处理这个异常。

如果进程没有被调试,或者调试器没有处理DBG_CONTROL_C (通过命令 gn),则系统会搜索程序的异常处理函数表,详细参考SetConsoleCtrlHandler函数。

如果调试器处理了DBG_CONTROL_C (通过gh命令),则程序不会收到 ctrl+c。除非程序这么做:

 while ((inputChar = getchar()) != EOF) ...
或者
 while (gets(inputString)) ...

调试器无法停止这样的读循环等待。
----------------------------------------
EXIT_PROCESS_DEBUG_EVENT  
当所调试进程的最后一个线程退出后,在内核卸载进程的dll,并且更新进程的退出码之后,产生这个事件。

DEBUG_EVENT结构包含一个EXIT_PROCESS_DEBUG_INFO结构,里面给出进程的退出码(返回值)。调试器收到这个事件后,删除为该进程分配的所有内部结构变量。系统关闭调试器使用的该进程的句柄,以及该进程的所有线程的句柄。

----------------------------------
EXIT_THREAD_DEBUG_EVENT

当被调试进程的某个线程退出后,在系统更新该线程的退出码后立刻产生这个事件。
DEBUG_EVENT结构包含一个EXIT_THREAD_DEBUG_INFO结构,给出退出码。

调试器收到这个事件后,删除与该线程相关的所有内部结构变量,系统关闭调试器使用的该线程的句柄。

如果这个线程是进程的最后一个线程,则不产生这个事件,而是产生EXIT_PROCESS_DEBUG_EVENT 
--------------------------------------
LOAD_DLL_DEBUG_EVENT

当一个被调试的进程加载一个DLL时,(包括隐式链接方式通过系统加载或者调用LoadLibrary进行显式加载)产生这个事件。

DEBUG_EVENT结构包含一个LOAD_DLL_DEBUG_INFO 结构,给出该DLL的句柄,起始地址以及其它DLL相关信息。
  一般的做法是,调试器收到这个事件后,加载该dll的符号表。
---------------------------------------
OUTPUT_DEBUG_STRING_EVENT

当被调试的进程调用OutputDebugString函数的时候产生这个事件,DEBUG_EVENT 结构包含一个OUTPUT_DEBUG_STRING_INFO 结构,给出调试字符串的地址、长度和格式。
------------------------------------
UNLOAD_DLL_DEBUG_EVENT

当被调试的进程使用FreeLibrary卸载一个DLL的时候,产生这个事件。注意只有当该DLL真正从进程的地址空间中卸载的时候(即它的使用计数为0),才产生这个事件。

DEBUG_EVENT结构包含一个UNLOAD_DLL_DEBUG_INFO结构,给出DLL的基地址。一般的做法是,调试器收到这个事件后,卸载与该 DLL相关的符号表。

当一个进程退出后,系统自动卸载它的DLL,但是不产生UNLOAD_DLL_DEBUG_EVENT事件。

 

8、使用调试支持

 

下面的例子代码使用WaitForDebugEvent和 ContinueDebugEvent,给出了一个简单的调试器的基本框架结构:

 

DEBUG_EVENT DebugEv; //调试事件信息


DWORD dwContinueStatus = DBG_CONTINUE; //异常
 
for(;;) 

 
//等待调试事件出现,第二个参数给出无限期等待
    WaitForDebugEvent(&DebugEv, INFINITE); 
 
//处理调试事件
     switch (DebugEv.dwDebugEventCode) 
    { 
        case EXCEPTION_DEBUG_EVENT: 
        //处理异常。记住设置dwContinueStatus。该参数
        //被函数 ContinueDebugEvent使用
        switch (DebugEv.u.Exception.ExceptionRecord.ExceptionCode) 
            { 
                case EXCEPTION_ACCESS_VIOLATION: 
                // 第一次:传递给系统处理
                // 第二次:显示一个合适的错误信息
 
                case EXCEPTION_BREAKPOINT: 
                // 第一次:显示当前的指令和寄存器值
 
                case EXCEPTION_DATATYPE_MISALIGNMENT: 
                // 第一次:传递给系统处理
                // 第二次:显示一个合适的错误信息
 
                case EXCEPTION_SINGLE_STEP: 
                // 第一次:更新当前指令和寄存器值的显示
 
                case DBG_CONTROL_C: 
                // 第一次:传递给系统处理
                // 第二次:显示一个合适的错误信息
 
                //处理其它异常
            } 
 
        case CREATE_THREAD_DEBUG_EVENT: 
        // 根据所需,使用函数GetThreadContext
  // 或者SetThreadContext 
  // 检查或者改变线程的寄存器
        // 或者通过函数SuspendThread/ResumeThread 
 // 挂起或者恢复线程的运行。
 
        case CREATE_PROCESS_DEBUG_EVENT: 
        // 如果需要,通过函数GetThreadContext和
        // SetThreadContext 改变进程的初始线程的寄存器值,
        // 通过函数ReadProcessMemory和
        // WriteProcessMemory读写进程的虚拟内存空间。
        // 通过函数SuspendThread /ResumeThread 
        // 挂起或者恢复线程的运行
 
        case EXIT_THREAD_DEBUG_EVENT: 
        // 显示线程的退出码
 
        case EXIT_PROCESS_DEBUG_EVENT: 
        // 显示进程的退出码
 
        case LOAD_DLL_DEBUG_EVENT: 
        // 读当前加载的dll的调试信息
 
        case UNLOAD_DLL_DEBUG_EVENT: 
        // 显示DLL卸载信息
 
        case OUTPUT_DEBUG_STRING_EVENT: 
        // 显示输出的调试字符串
 
    } 
 
// 恢复线程的运行
 
ContinueDebugEvent(DebugEv.dwProcessId, 
    DebugEv.dwThreadId, dwContinueStatus); 
 

------------------------------------
END IWASWZQ 2005/6/15

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值