首页 > 其他 > 详细

简易调试器的实现(一)

时间:2016-03-30 01:30:34      阅读:210      评论:0      收藏:0      [点我收藏+]

看过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函数,该函数声明如下:

1 BOOL WINAPI GetThreadContext(
2     HANDLE hThread,
3     LPCONTEXT lpContext
4 );

第一个参数是线程的句柄,第二个参数是指向CONTEXT结构的指针。要注意,调用该函数之前需要设置CONTEXT结构的ContextFlags字段,指明你想要获取哪部分寄存器的值。该字段的取值如下:

 

CONTEXT_CONTROL

获取EBPEIPCSEFLAGSESPSS寄存器的值。

CONTEXT_INTEGER

获取EAXEBXECXEDXESIEDI寄存器的值。

CONTEXT_SEGMENTS

获取DSESFSGS寄存器的值。

CONTEXT_FLOATING_POINT

获取有关浮点数寄存器的值。

CONTEXT_DEBUG_REGISTERS

获取DR0DR1DR2DR3DR6DR7寄存器的值。

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时,字符串指令每次操作后递减ESIEDI,为0时递增。

OF

11

溢出标志。有符号数发生溢出时,该标志为1,否则为0

 

 

读取进程的内存使用ReadProcessMemory函数,该函数声明如下:

 
1 BOOL WINAPI ReadProcessMemory(
2     HANDLE hProcess,                  //进程句柄
3     LPCVOID lpBaseAddress,            //要读取的地址
4     LPVOID lpBuffer,                  //一个缓冲区的指针,保存读取到的内容
5     SIZE_T nSize,                     //要读取的字节数
6     SIZE_T* lpNumberOfBytesRead       //一个变量的指针,保存实际读取到的字节数
7 );
 

 

要想成功读取到进程的内存,需要两个条件:一是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

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!