Windows用户态调试器原理

             Windows用户态调试器原理

 

    Windows操作系统提供了一组API来支持调试器。

    这些API可以分为三类:

       创建调试目标的API

       在调试循环中处理调试事件的API

       查看和修改调试目标的API

    接下来将会分别对这三种API进行介绍。

 

创建调试目标

 

     在调试器工作之前,需要创建调试目标。用户态调试器有两种创建调试目标的方法:一是创建新进程,二是附加到一个运行的进程。采用这两种方法中的任一种后,该进程就成为了调试目标。操作系统将调试器与调试目标关联起来。

     调试器创建调试目标是通过调用CreateProcess并传入DEBUG_PROCESS标志。

如:

 

STARTUPINFO si={0};

si.cb=sizeof(si);

PROCESS_INFORMATION pi={0};

bool ret=CreateProcesss(NULL,argv[1],NULL,NULL,false,

DEBUG_PROCESS,NULL,NULL,&si,&pi);


 

     调试器附加到一个运行的进程是通过调用DebugActiveProcess来实现的。

      DebugActiveProcess   

     此函数允许将调试器捆绑到一个正在运行的进程上。

 

BOOL DebugActiveProcess(DWORD dwProcessId )


 

     dwProcessId:欲捆绑进程的进程标识符

     如果函数成功,则返回非零值;如果失败,则返回零

    无论采用哪一种方法,调试器与操作系统的交互都是相同的。这种调试器被称为活动调试器(living debuger)。每个调试器只能有一个调试目标。

 

调试循环

     在初学Windows时我们一定接触过消息循环。调试循环与此类似。

while(当调试不结束时)

{

   //等待操作系统发送调试事件。

   //处理调试事件。

   //通知调试目标执行相应操作。

}

     在调试目标被调试时,进程执行的一些操作会以事件的方式通知调试器。例如动态库的加载与卸载、新线程的创建和销毁以及代码或处理器抛出的异常都会通知调试器。

     当有事件需要通知调试器时,操作系统会首先挂起调试目标的所有线程,然后把事件通知调试器。并且等待调试器通知其继续执行。

调试器会调用WaitForDebugEvent来等待事件通知的到来 。当有事件通知到来时此函数返回,返回的事件信息被封装在DEBUG_EVENT结构中。这个结构包含事件的类型等其他信息。

      事件类型有以下几种:

 

 

     WaitForDebugEvent

     此函数用来等待被调试进程发生调试事件。

 

BOOL WaitForDebugEvent(LPDEBUG_ENENT lpDebugEvent, DWORD dwMilliseconds)


 

      lpDebugEvent :指向接收调试事件信息的DEBUG_ ENENT结构的指针

      dwMilliseconds:指定用来等待调试事件发生的毫秒数,如果 这段时间内没有调试事件发生,函数将返回调用者;如果将该参数指定为INFINITE,函数将一直等待直到调试事件发生

      如果函数成功,则返回非零值;如果失败,则返回零

     在调试器调用WaitForDebugEvent返回后,得到事件通知,然后解析DEBUG_EVENT结构,并对事件进行响应,处理完成后调试器将会调用ContinueDebugEvent,并根据参数来通知调试目标执行相应操作。

 

      ContinueDebugEvent函数

 

     此函数允许调试器恢复先前由于调试事件而挂起的线程。

 

BOOL ContinueDebugEvent(DWORD dwProcessId,DWORD dwThreadId, DWORD dwContinueStatus )


 

      dwProcessId 为被调试进程的进程标识符

      dwThreadId  为欲恢复线程的线程标识符

      dwContinueStatus指定了该线程将以何种方式继续,包含两个定义值DBG_CONTINUEDBG_EXCEPTION_NOT_HANDLED

如果函数成功,则返回非零值;如果失败,则返回零。

具体实现为:

 

 

DWORD Condition=DBG_CONTINUE;

while(Condition)

{

    DEBUG_EVENT DebugEvent={0};

WaitForDebugEvent(&DebugEvent,INFINITE);//等待调试事件

ProcessEvenet(DebugEvent)//处理调试事件。

ContinueDebugEvent(DebugEvent.dwProcessId,DebugEvent.dwThreadId,Condition);//通知调试目标继续执行。

}


 

      ProcessEvent用于对调试事件进行处理。它是用户自定义函数。 在该函数内会对DEBUG_EVENT结构进行解析。

      DEBUG_EVENT结构为:

 

typedef struct _DEBUG_EVENT {

  DWORD dwDebugEventCode;

  DWORD dwProcessId;

  DWORD dwThreadId;

  union {

    EXCEPTION_DEBUG_INFO      Exception;

    CREATE_THREAD_DEBUG_INFO  CreateThread;

    CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;

    EXIT_THREAD_DEBUG_INFO    ExitThread;

    EXIT_PROCESS_DEBUG_INFO   ExitProcess;

    LOAD_DLL_DEBUG_INFO       LoadDll;

    UNLOAD_DLL_DEBUG_INFO     UnloadDll;

    OUTPUT_DEBUG_STRING_INFO  DebugString;

    RIP_INFO                  RipInfo;

  } u;

} DEBUG_EVENT, *LPDEBUG_EVENT;


 

处理通知代码如下:

 

DWORD ProcessEvent(DEBUG_EVENT de)

{

   switch(de.dwDebugEvent.Code)

   {

      case EXCEPTION_DEBUG_EVENT:

        {

         }

         break;

     case CREATE_THREAD_DEBUG_EVENT:

        {

         }

         break;

     case CREATE_PROCESS_DEBUG_EVENT:

        {

         }

         break;

     case EXIT_THREAD_DEBUG_EVENT:

        {

         }

         break;

     case EXIT_PROCESS_DEBUG_EVENT:

        {

         }

         break;

      case LOAD_DLL_DEBUG_EVENT:

        {

         }

         break;

      case OUTPUT_DEBUG_STRING_EVENT:

        {

         }

         break;

       ......

}

return DBG_CONTINUE;

}


 

调试事件介绍

 

    OUTPUT_DEBUG_STRING_EVENT事件

     很多程序员在调试程序时喜欢将执行的结果或中间步骤输出,用以检查程序执行的正确与否。在很多系统中这是很不方便的。但我们可以使用调试输出命令,将某些需要显示的结果输出到输出窗口中。如vcTRACE宏。其实在TRACE宏内部是调用OutputDebugString来实现的 。调试器会把调试目标输出的字符串通过事件处理代码显示出来。在DEBUG_EVENT 结构中有一个DebugString成员。

该结构定义为:

 

 

typedef struct _OUTPUT_DEBUG_STRING_INFO {

  LPSTR lpDebugStringData;

  WORD  fUnicode;

  WORD  nDebugStringLength;

} OUTPUT_DEBUG_STRING_INFO, *LPOUTPUT_DEBUG_STRING_INFO;


 

     在此结构中有一个lpDebugStringData成员,它保存被输出字符串的地址。nDebugStringLength为字符串长度。fUnicode表示是ANSI还是UNICODE字符。

下面为处理OUTPUT_DEBUG_STRING_EVENT事件的代码:

 

case OUTPUT_DEBUG_STRING_EVENT:

 {

   OUTPUT_DEBUG_STRING_INFO oi=de.u.DebugString;

   WCHAR *msg=ReadRemoteString(调试目标句柄,

   oi.lpDebugStringData,oi.nDebugStringLength,oi.fUnicode);

   std::wcout<<msg;

    break;

 }


 

     ReadRemoteString是用户自定义函数。在此函数内部是调用ReadProcessMemory从调试目标进程内读取字符串。具体不再介绍。

     ReadProcessMemory

     读取指定进程的某区域内的数据。

 

BOOL ReadProcessMemory(HANDLE hProcess, LPCVOID lpBassAddress, LPVOID lpBuffer,  SIZE_T nSize, SIZE_T * lpNumberOfBytesRead)


 

     hProcess:进程的句柄

     lpBassAddress:欲读取区域的基地址

     lpBuffer:保存读取数据的缓冲的指针

     nSize:欲读取的字节数

     lpNumberOfBytesRead:存储已读取字节数的地址指针

     如果函数成功,则返回非零值;如果失败,则返回零

 

 处理EXCEPTION_DEBUG_EVENT事件

 

     当调试目标在调试时发生异常时,操作系统将会向调试器发送EXCEPTION_DEBUG_EVENT事件通知

     当发生此事件时,DEBUG_EVENT结构包含的是一个EXCEPTION_DEBUG_INFO结构。

 

typedef struct _EXCEPTION_DEBUG_INFO {

  EXCEPTION_RECORD ExceptionRecord;

  DWORD            dwFirstChance;

} EXCEPTION_DEBUG_INFO, *LPEXCEPTION_DEBUG_INFO;


 

      ExceptionRecord成员包含了异常信息的一个副本。如异常码,异常引发地址以及异常参数等。定义如下:

typedef struct _EXCEPTION_RECORD { 

        DWORD ExceptionCode; 

        DWORD ExceptionFlags; 

        struct _EXCEPTION_RECORD *ExceptionRecord; 

        PVOID ExceptionAddress; 

        DWORD NumberParameters; 

        DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; 

} EXCEPTION_RECORD;


 

      dwFirstChance告诉调试器是否是第一轮通知这个异常。

      从操作系统的角度来看,调试器必须对异常进行解析,并且将DBG_CONTINUE或者是DBG_EXECPTION_NOT_HANDLED作为参数传递给ContinueDebugEvent。如果执行DBG_CONTINUE,则操作系统认为该异常已经被妥善处理了。因此从产生异常的地址开始回复程序的执行。如果传入DBG_EXCEPTION_NOT_HANDLED,则告诉操作系统该异常并未被处理,操作系统将继续分发异常。

 

case EXCEPTION_DBUG_EVENT:

{

  std::cout<<”异常码为”<<std::hex<<debugEvent.u.Exception.ExceptionRecord.ExceptionCode<<std::endl;

   //在switch判断异常类型,并执行相应操作。

   switch(debugEvent.u.Exception.ExceptionRecord.ExceptionCode)

  {

   case EXCEPTION_BREAKPOINT:

    break;

    case EXCEPTION_SINGLE_STEP:

     beak;

     return DBG_CONTINUE;

 }

   break;

}


 

      在调试循环中,从WaitForDebugEvent中返回以及调用ContinueDebugEvent之间的这段时间内,调试目标不会执行,因此它的状态也将保持不变。当调试目标被挂起时,调试器就进入了交互模式,接收用户的各种指令,并按照不同指令执行不同操作。

 

调试事件到来的顺序

 

       当我们启动调试目标时,调试器接收到的第一个事件是CREATE_THREAD_DEBUG_EVENT。接下来是加载dll的事件。每加载一个,都会产生一个这样的事件。

       当所有模块都被加载到进程地址空间后,调试目标就准备好运行了,调试器此时也做好了接收通知的准备。此时是设置断点的最佳时机。

       在调试目标退出之前调试器会收到 EXIT_DEBUG_PROCESS_EVENT通知。此后调试器不能收到加载到进程地址空间的dll从进程卸载的UNLOAD_DLL_DEBUG_EVENT通知。

       前面介绍的调试事件都是由Windows操作系统发出的,来通知调试器。但是调试目标也会发出自己的异常。调试器在处理这些异常时可以选择与其他调试事件一样的处理方式。

       Windows操作系统使用结构化异常处理(SEH)机制将处理器引发的异常传递给内核及用户态程序。每个SEH异常都有一个无符号整形的异常码来唯一标识。这个异常码是由系统在异常发生时指定的。这些异常码使用了操作系统开发人员定义的公开异常码。例如访问违规异常异常码为0xC0000005,断点异常为0xC80000003。为了方便记忆,这些异常码被定义为常量。其名字形如STATUS_XXX。如

#define STATUS_BREAKPOINT ((NTSTATUS)0x80000003L)

       由于异常码很难记忆,因此Windows调试器中包含了一些更容易记住的别名来控制调试器的行为。例如断点异常0x80000003 的别名是bpeC++异常码0xE06D7363别名为eh

 
                                             以上内容参考自《Windows高级调试》如有纰漏,请不吝赐教!
                                                       2013.1.29 于山西大同
  • 5
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值