看过SEH结构化异常处理,看了<软件调试>这本书,觉得调试真是一件特别棒的事情,于是在网上搜索调试器怎么做,跟着大牛的脚步慢慢的往前走,于是借用大牛提供的代码,自己也开始慢慢做一个调试器
前期基本按照这个大牛的思路
http://www.cnblogs.com/zplutor/archive/2011/03/04/1971279.html
前期的分析也照着来点0.0
这一部分实现的功能是显示寄存器状态、显示字节码(感觉有一些问题,以后再修正),处理异常,断点处理(本来想在OEP断下来的,结果不知道为什么OEP始终没东西,一直下断点也没用,不过程序中的int 3断点可以断下来)。
程序使用VS2010开发,MFC程序(感觉特别不好,不过界面开发没学其他,,以后有时间肯定补上)。
这一部分大概包括了上面博客的1-7部分,我这里简要说一下,详细可以看大牛博客。
关于调试器原理,可以参考
http://bbs.pediy.com/showthread.php?t=206292
下面开始
一、创建调试进程,并等待调试事件的到达
Windows有一个调试子系统,所有的异常(包括CPU产生的异常)都会中断到调试子系统中,进程产生异常后,调试子系统会捕捉到这个异常,如果这个进程是以被调试状态创建,那么,调试子系统会将这个异常派发到产生异常的进程的父进程.
如果其父进程的代码用有函数WaitForDebugEvent(),那么,函数将会从等待状态中被唤醒,返回到其父进程的调用地点.并将异常信息保存到DEBUG_EVENT结构体中.
CreateProcess(/*创建调试线程*/ pszFilePath,//可执行模块路径 NULL,//命令行 NULL,//安全描述符 NULL,//线程属性是否可继承 FALSE,//否从调用进程处继承了句柄 DEBUG_ONLY_THIS_PROCESS,//启动方式,这里是以只调试的方式创建一个还没有运行的进程 NULL,//新进程的环境块 NULL,//新进程的当前工作路径(当前目录) &stcStartupInfo,//指定进程的主窗口特性 &stcProcInfo//接收新进程的识别信息 );
创建完子进程之后就可以等待调试事件的到达
while (WaitForDebugEvent(&debugEvent, INFINITE) == TRUE) { if (DispatchDebugEvent(&debugEvent) == TRUE) { ContinueDebugEvent(g_processID, g_threadID, DBG_EXCEPTION_NOT_HANDLED); } else { break; } } //根据调试事件的类型调用不同的处理函数。 BOOL DispatchDebugEvent(const DEBUG_EVENT* pDebugEvent) { switch (pDebugEvent->dwDebugEventCode) { case CREATE_PROCESS_DEBUG_EVENT: return OnProcessCreated(&pDebugEvent->u.CreateProcessInfo); case CREATE_THREAD_DEBUG_EVENT: return OnThreadCreated(&pDebugEvent->u.CreateThread); case EXCEPTION_DEBUG_EVENT: return OnException(&pDebugEvent->u.Exception); case EXIT_PROCESS_DEBUG_EVENT: return OnProcessExited(&pDebugEvent->u.ExitProcess); case EXIT_THREAD_DEBUG_EVENT: return OnThreadExited(&pDebugEvent->u.ExitThread); case LOAD_DLL_DEBUG_EVENT: return OnDllLoaded(&pDebugEvent->u.LoadDll); case OUTPUT_DEBUG_STRING_EVENT: return OnOutputDebugString(&pDebugEvent->u.DebugString); case RIP_EVENT: return OnRipEvent(&pDebugEvent->u.RipInfo); case UNLOAD_DLL_DEBUG_EVENT: return OnDllUnloaded(&pDebugEvent->u.UnloadDll); default: return FALSE; } }
其中等待的异常事件结构体为
ypedefstruct_DEBUG_EVENT{ DWORDdwDebugEventCode;//发生异常的是什么事 DWORDdwProcessId;//触发异常的进程ID(如果被调试进程有多个进程,这个ID有可能是其子进程的) DWORDdwThreadId;//触发异常的线程ID(如果被调试进程有多个线程,这个ID有可能是其中的一个线程的 union{ EXCEPTION_DEBUG_INFOException;//异常类型信息 CREATE_THREAD_DEBUG_INFOCreateThread;//创建线程时得到的信息结构体(有可能会创建多个线程) CREATE_PROCESS_DEBUG_INFOCreateProcessInfo;//创建进程时得到的信息结构体,有可能会得到多个 EXIT_THREAD_DEBUG_INFOExitThread;//线程退出的信息结构体 EXIT_PROCESS_DEBUG_INFOExitProcess;//进程退出的信息结构体 LOAD_DLL_DEBUG_INFOLoadDll;//加载模块的信息结构体 UNLOAD_DLL_DEBUG_INFOUnloadDll;//卸载模块的信息结构体 OUTPUT_DEBUG_STRING_INFODebugString;//输出调试字串的信息结构体 RIP_INFORipInfo;//系统调试错误时的信息结构体 }u;//这是一个联合体,dwDebugEventCode决定联合体中哪个字段是有用的. }DEBUG_EVENT,*LPDEBUG_EVENT;
派发的结构类型的结构体为
typedefstruct_EXCEPTION_DEBUG_INFO{ EXCEPTION_RECORDExceptionRecord;//一个结构体,里面保存的是异常发生的地址和原因 DWORDdwFirstChance;// }EXCEPTION_DEBUG_INFO,*LPEXCEPTION_DEBUG_INFO;
typedefstruct_EXCEPTION_RECORD{ DWORDExceptionCode;//异常发生的真相 DWORDExceptionFlags; struct_EXCEPTION_RECORD*ExceptionRecord; PVOIDExceptionAddress;//异常发生的地址 DWORDNumberParameters; ULONG_PTRExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; }EXCEPTION_RECORD;
二、关于异常。
根据异常发生时是否可以恢复执行,可以将异常分为三种类型,分别是错误异常,陷阱异常以及中止异常。
错误异常和陷阱异常一般都可以修复,并且在修复后程序可以恢复执行。两者的不同之处在于,错误异常恢复执行时,是从引发异常的那条指令开始执行;而陷阱异常是从引发异常那条指令的下一条指令开始执行。
下面来看一下异常的分发过程。为了突出重点,这里省略了很多细节:
1.程序发生了一个异常,Windows捕捉到这个异常,并转入内核态执行。
2.Windows检查发生异常的程序是否正在被调试,如果是,则发送一个EXCEPTION_DEBUG_EVENT调试事件给调试器,这是调试器第一次收到该事件;如果否,则跳到第4步。
3.调试器收到异常调试事件之后,如果在调用ContinueDebugEvent时第三个参数为DBG_CONTINUE,即表示调试器已处理了该异常,程序在发生异常的地方继续执行,异常分发结束;如果第三个参数为DBG_EXCEPTION_NOT_HANDLED,即表示调试器没有处理该异常,跳到第4步。
4.Windows转回到用户态中执行,寻找可以处理该异常的异常处理器。如果找到,则进入异常处理器中执行,然后根据执行的结果继续程序的执行,异常分发结束;如果没找到,则跳到第5步。
5.Windows又转回内核态中执行,再次检查发生异常的程序是否正在被调试,如果是,则再次发送一个EXCEPTION_DEBUG_EVENT调试事件给调试器,这是调试器第二次收到该事件;如果否,跳到第7步。
6.调试器第二次处理该异常,如果调用ContinueDebugEvent时第三个参数为DBG_CONTINUE,程序在发生异常的地方继续执行,异常分发结束;如果第三个参数为DBG_EXCEPTION_NOT_HANDLED,跳到第7步。
7.异常没有被处理,程序以“应用程序错误”结束。
简单来说就是有调试程序器存在的进程产生异常,会先给调试父进程通知,如果没处理则回到内核寻找(应该是SEH结构),内核没处理则再次转交给调试父进程,如果还没有处理则结束进程。
软件异常:是程序主动触发异常,比如int 3,是陷阱异常,恢复的时候eip会指向指令的下一个
硬件异常: 是cpu的异常,比如除0的异常,是错误异常,恢复的时候eip会指向从出错的地方,继续执行。
程序中,一种断点就是利用int 3让程序断下来,int 3的字节码是0xCC,我们将指令的第一个字节换成0xCC,当程序执行到此时就会触发异常,系统就会优先通知调试父进程,会走到OnException函数中,等到我们的处理,如果是一次性断点,我们恢复运行之后不需要再重新设置断点,如果不是一次性断点,则需要再次设置断点,那么什么时候设置断点呢?cpu中有个TF标志位,当设置为1的时候,cpu每执行一条指令都会产生一个中断,所以我们恢复int 3的时候设置一下TF,在TF触发的中断处理中再次可以设置之前被恢复的断点。TF位如何设置,我们下面会讲到。
至于硬件断点、内存断点本节还没有涉及。
三、寄存器和内存
每个线程都有一个Context结构保存着运行的上下文,包括一些寄存器。
可以通过GetThreadContext(g_hThread, pContext)函数获得Context信息,
也可以根据SetThreadContext(g_hThread,pContext)函数来设置Context信息。
获取某个线程的上下文环境需要使用GetThreadContext函数,该函数声明如下:
第一个参数是线程的句柄,第二个参数是指向CONTEXT结构的指针。要注意,调用该函数之前需要设置CONTEXT结构的ContextFlags字段,指明你想要获取哪部分寄存器的值。该字段的取值如下:
CONTEXT_CONTROL |
获取EBP,EIP,CS,EFLAGS,ESP和SS寄存器的值。 |
CONTEXT_INTEGER |
获取EAX,EBX,ECX,EDX,ESI和EDI寄存器的值。 |
CONTEXT_SEGMENTS |
获取DS,ES,FS和GS寄存器的值。 |
CONTEXT_FLOATING_POINT |
获取有关浮点数寄存器的值。 |
CONTEXT_DEBUG_REGISTERS |
获取DR0,DR1,DR2,DR3,DR6,DR7寄存器的值。 |
CONTEXT_FULL |
等于CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS |
调用GetThreadContext函数之后,CONTEXT结构相应的字段就会被赋值,此时就可以输出各个寄存器的值了。
对于其它寄存器来说,直接输出它的值就可以了,但是EFLAGS寄存器的输出比较麻烦,因为它的每一位代表不同的含义,我们需要将这些含义也输出来。一般情况下我们只需要了解以下标志:
标志 |
位 |
含义 |
CF |
0 |
进位标志。无符号数发生溢出时,该标志为1,否则为0。 |
PF |
2 |
奇偶标志。运算结果的最低字节中包含偶数个1时,该标志为1,否则为0。 |
AF |
4 |
辅助进位标志。运算结果的最低字节的第三位向高位进位时,该标志为1,否则为0。 |
ZF |
6 |
0标志。运算结果未0时,该标志为1,否则为0。 |
SF |
7 |
符号标志。运算结果未负数时,该标志为1,否则为0。 |
DF |
10 |
方向标志。该标志为1时,字符串指令每次操作后递减ESI和EDI,为0时递增。 |
OF |
11 |
溢出标志。有符号数发生溢出时,该标志为1,否则为0。 |
读取进程的内存使用ReadProcessMemory函数,该函数声明如下:
要想成功读取到进程的内存,需要两个条件:一是hProcess句柄具有PROCESS_VM_READ的权限;二是由lpBaseAddress和nSize指定的内存范围必须位于用户模式地址空间内,而且是已分配的。
对于调试器来说,第一个条件很容易满足,因为调试器对被调试进程具有完整的权限,可以对其进行任意操作。
第二个条件意味着我们不能读取进程任意地址的内存,而是有一个限制。Windows将进程的虚拟地址空间分成了四个分区,如下表所示:(来自《Windows核心编程(第5版)》)
分区 |
地址范围 |
空指针赋值分区 |
0x00000000~0x0000FFFF |
用户模式分区 |
0x00010000~0x7FFEFFFF |
64KB禁入分区 |
0x7FFF0000~0x7FFFFFFF |
内核模式分区 |
0x80000000~0xFFFFFFFF |
空指针赋值分区主要为了帮助程序员检测对空指针的访问,任何对这一分区的读取或写入操作都会引发异常。64KB禁入分区正如其名字所言,是禁止访问的,由Windows保留。内核模式分区由Windows的内核部分使用,运行于用户态的进程不能访问这一区域。进程只能访问用户模式分区的内存,对于其它分区的访问将会引发ACCESS_VIOLATION异常。
另外,并不是用户模式分区的任意部分都可以访问。我们知道,在32位保护模式下,进程的4GB地址空间是虚拟的,在物理内存中不存在。如果要使用某一部分地址空间的话,必须先向操作系统提交申请,让操作系统为这部分地址空间分配物理内存。只有经过分配之后的地址空间才是可访问的,试图访问未分配的地址空间仍然会引发ACCESS_VIOLATION异常。
这里大牛采用的是一个字节一个字节的读取。
四、调试符号及源代码的显示
这方面直接看大牛的吧,,我试验的时候没有得出源代码,可能是没有符号表的存在,我正在找一个比较好的反汇编引擎。。
http://www.cnblogs.com/zplutor/archive/2011/03/20/1989783.html
http://www.cnblogs.com/zplutor/archive/2011/03/27/1997198.html
五、断点的实现
维护一个链表保存着断点的地址、第一个字节(CC断点),类型等等,在设置断点的时候通过ReadProcessMemory和WriteProcessMemory函数写入CC,设置断点成功,等待中断,中断到了之后在链表中比较地址,恢复CC的内容,设置TF位,当TF断下来之后就可以再次设置之前恢复的CC,也可以不恢复(一次断点),这里要注意重复设断和取消断点的处理。
还有很多不完善的地方,以后会慢慢改过来
这里比较多的借鉴了各位大牛
代码:http://pan.baidu.com/s/1dFfkWJv
原文:http://www.cnblogs.com/aliflycoris/p/5335526.html