现在的位置: 首页 > 综合 > 正文

Windows用户态调试器原理

2014年08月28日 ⁄ 综合 ⁄ 共 6275字 ⁄ 字号 评论关闭

Windows操作系统提供了一组API来支持调试器。
    这些API可以分为三类:
    创建调试目标的API;
    在调试循环中处理调试事件的API.
    查看和修改调试目标的API.
    接下来将会分别对这三种API进行介绍。
    创建调试目标
    在调试器工作之前,需要创建调试目标。用户态调试器有两种创建调试目标的方法:一是创建新进程,二是附加到一个运行的进程。采用这两种方法中的任一种后,该进程就成为了调试目标。操作系统将调试器与调试目标关联起来。
    调试器创建调试目标是通过调用CreateProcess并传入DEBUG_PROCESS标志。
    如:
    [cpp]
    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
    此函数允许将调试器捆绑到一个正在运行的进程上。
    [cpp]
    BOOL DebugActiveProcess(DWORD dwProcessId )
    dwProcessId:欲捆绑进程的进程标识符
    如果函数成功,则返回非零值;如果失败,则返回零
    无论采用哪一种方法,调试器与操作系统的交互都是相同的。这种调试器被称为活动调试器(living debuger)。每个调试器只能有一个调试目标。
    调试循环
    在初学Windows时我们一定接触过消息循环。调试循环与此类似。
    while(当调试不结束时)
    {
    //等待操作系统发送调试事件。
    //处理调试事件。
    //通知调试目标执行相应操作。
    }
    在调试目标被调试时,进程执行的一些操作会以事件的方式通知调试器。例如动态库的加载与卸载、新线程的创建和销毁以及代码或处理器抛出的异常都会通知调试器。
    当有事件需要通知调试器时,操作系统会首先挂起调试目标的所有线程,然后把事件通知调试器。并且等待调试器通知其继续执行。
    调试器会调用WaitForDebugEvent来等待事件通知的到来 .当有事件通知到来时此函数返回,返回的事件信息被封装在DEBUG_EVENT结构中。这个结构包含事件的类型等其他信息。
    事件类型有以下几种:
    WaitForDebugEvent
    此函数用来等待被调试进程发生调试事件。
    [cpp]
    BOOL WaitForDebugEvent(LPDEBUG_ENENT lpDebugEvent, DWORD dwMilliseconds)
    lpDebugEvent :指向接收调试事件信息的DEBUG_ ENENT结构的指针
    dwMilliseconds:指定用来等待调试事件发生的毫秒数,如果 这段时间内没有调试事件发生,函数将返回调用者;如果将该参数指定为INFINITE,函数将一直等待直到调试事件发生
    如果函数成功,则返回非零值;如果失败,则返回零
    在调试器调用WaitForDebugEvent返回后,得到事件通知,然后解析DEBUG_EVENT结构,并对事件进行响应,处理完成后调试器将会调用ContinueDebugEvent,并根据参数来通知调试目标执行相应操作。
    ContinueDebugEvent函数
    此函数允许调试器恢复先前由于调试事件而挂起的线程。
    [cpp]
    BOOL ContinueDebugEvent(DWORD dwProcessId,DWORD dwThreadId, DWORD dwContinueStatus )
    dwProcessId 为被调试进程的进程标识符
    dwThreadId  为欲恢复线程的线程标识符
    dwContinueStatus指定了该线程将以何种方式继续,包含两个定义值DBG_CONTINUE和DBG_EXCEPTION_NOT_HANDLED

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

具体实现为:
    [cpp]
    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结构为:
    [cpp]
    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;
    处理通知代码如下:
    [cpp]
    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事件
    很多程序员在调试程序时喜欢将执行的结果或中间步骤输出,用以检查程序执行的正确与否。在很多系统中这是很不方便的。但我们可以使用调试输出命令,将某些需要显示的结果输出到输出窗口中。如vc的TRACE宏。其实在TRACE宏内部是调用OutputDebugString来实现的 .调试器会把调试目标输出的字符串通过事件处理代码显示出来。在DEBUG_EVENT 结构中有一个DebugString成员。
    该结构定义为:
    [cpp]
    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事件的代码:
    [cpp]
    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
    读取指定进程的某区域内的数据。
    [cpp]
    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结构。
    [cpp]
    typedef struct _EXCEPTION_DEBUG_INFO {
    EXCEPTION_RECORD ExceptionRecord;
    DWORD            dwFirstChance;
    } EXCEPTION_DEBUG_INFO, *LPEXCEPTION_DEBUG_INFO;
    ExceptionRecord成员包含了异常信息的一个副本。如异常码,异常引发地址以及异常参数等。定义如下:
    [cpp]
    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,则告诉操作系统该异常并未被处理,操作系统将继续分发异常。
    [cpp]
    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 的别名是bpe.C++异常码0xE06D7363别名为eh.

【上篇】
【下篇】

抱歉!评论已关闭.