Windows用户态调试器原理

本文介绍了Windows操作系统中用户态调试器的工作原理,包括创建调试目标、处理调试事件及查看和修改调试目标的方法。详细解释了如何使用API进行调试,并展示了调试过程中关键事件的处理流程。

Windows用户态调试器原理

    Windows操作系统提供了一组API来支持调试器。这些API可以分为三类:

l  创建调试目标的API

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

l  查看和修改调试目标的API

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

创建调试目标

    在调试器工作之前,需要创建调试目标。用户态调试器有两种创建调试目标的方法:

1.         创建新进程;

2.         附加到一个运行的进程。

采用这两种方法中的任一种后,该进程就成为了调试目标。操作系统将调试器与调试目标关联起来。

1.         调试器创建调试目标是通过调用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);

2.         调试器附加到一个运行的进程是通过调用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_CONTINUEDBG_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事件

    很多程序员在调试程序时喜欢将执行的结果或中间步骤输出,用以检查程序执行的正确与否。在很多系统中这是很不方便的。但我们可以使用调试输出命令,将某些需要显示的结果输出到输出窗口中。如VCTRACE宏。其实在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:

                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,断点异常为0x80000003。为了方便记忆,这些异常码被定义为常量。其名字形如STATUS_XXX。如

    #define STATUS_BREAKPOINT ((NTSTATUS)0x80000003L)

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值