65.9K
CodeProject 正在变化。 阅读更多。
Home

编写调试器 - 第 2 部分:调试循环

starIconstarIconstarIconstarIconstarIcon

5.00/5 (10投票s)

2003年10月22日

CPOL

4分钟阅读

viewsIcon

89178

本系列文章的第 2 部分,展示了如何在 VB 中编写调试器。

引言

Windows NT(及其后续操作系统版本)公开了一组 API 函数和结构,用于调试正在运行的进程。本文展示了如何从 Visual Basic(版本 5 或 6)访问这些函数。建议将本文 《可执行文件的内部:VB 程序员的 PE 文件格式入门》 与本文结合阅读,并且其附带的源代码也包含了本文的代码。

要调试的进程

有两种方法可以获得要调试的进程。要么

  1. 将调试器附加到已运行的进程
  2. 启动一个带有附加调试器的新进程。

启动一个带有附加调试器的新进程

要启动一个进程,您可以使用 CreateProcess API 调用。

Private Declare Function CreateProcess Lib _
    "kernel32" Alias "CreateProcessA" (ByVal lpApplicationName As String, _
                                       ByVal lpCommandLine As String, _
                                       ByVal lpProcessAttributes As Long,_
                                        ByVal lpThreadAttributes As Long,_
                                        ByVal bInheritHandles As Long, _
                                       ByVal dwCreationFlags As ProcessCreationFlags, _
                                       ByVal lpEnvironment As Long, _
                                       ByVal lpCurrentDirectory As String, _
                                       lpStartupInfo As STARTUPINFO, _
                                       lpProcessInformation As PROCESS_INFORMATION) _
                                       As Long

除了 Shell 命令的功能外,它还允许您指定影响进程创建方式的其他标志。

Public Enum ProcessCreationFlags
    DEBUG_PROCESS = &H1
    DEBUG_ONLY_THIS_PROCESS = &H2
    CREATE_SUSPENDED = &H4
    DETACHED_PROCESS = &H8
    CREATE_NEW_CONSOLE = &H10
    NORMAL_PRIORITY_CLASS = &H20
    IDLE_PRIORITY_CLASS = &H40
    HIGH_PRIORITY_CLASS = &H80
    REALTIME_PRIORITY_CLASS = &H100
    CREATE_NEW_PROCESS_GROUP = &H200
    CREATE_UNICODE_ENVIRONMENT = &H400
    CREATE_SEPARATE_WOW_VDM = &H800
    CREATE_SHARED_WOW_VDM = &H1000
    CREATE_FORCEDOS = &H2000
    CREATE_DEFAULT_ERROR_MODE = &H4000000
    CREATE_NO_WINDOW = &H8000000
End Enum

为了使用附加调试器启动进程,您需要指定 DEBUG_PROCESS + DEBUG_ONLY_THIS_PROCESS 标志。

将调试器附加到现有进程

要将调试器附加到已运行的进程,您需要获取其句柄,然后使用 DebugActiveProcess API 调用来附加调试器。

Private Declare Function DebugActiveProcess Lib "kernel32" _
                                   (ByVal dwProcessId As Long) As Long

调试循环

一旦将调试器附加到进程,您就需要进入调试循环。这包括等待调试事件、处理接收到的事件,然后允许被调试进程继续运行。

等待调试事件发生

要等待调试事件,您需要调用 WaitForDebugEvent API 调用。

Private Declare Function WaitForDebugEvent Lib "kernel32" _
                                       (lpDebugEvent As DEBUG_EVENT_BUFFER, _
                                       ByVal dwMilliseconds As Long) As Long

当调试事件发生时,这将返回 TRUE 并填充 DEBUG_EVENT_... 结构,该结构取决于发生的事件,但始终以 DEBUG_EVENT_HEADER 开始。

Private Type DEBUG_EVENT_HEADER
    dwDebugEventCode As DebugEventTypes
    dwProcessId As Long
    dwThreadId As Long
End Type

处理调试事件

您如何处理调试事件,自然会取决于发生了什么事件。事件类型包括:

Public Enum DebugEventTypes
    EXCEPTION_DEBUG_EVENT = 1&
    CREATE_THREAD_DEBUG_EVENT = 2&
    CREATE_PROCESS_DEBUG_EVENT = 3&
    EXIT_THREAD_DEBUG_EVENT = 4&
    EXIT_PROCESS_DEBUG_EVENT = 5&
    LOAD_DLL_DEBUG_EVENT = 6&
    UNLOAD_DLL_DEBUG_EVENT = 7&
    OUTPUT_DEBUG_STRING_EVENT = 8&
    RIP_EVENT = 9&
End Enum

EXCEPTION_DEBUG_EVENT

当被调试的应用程序发生异常时,会抛出此调试事件。例如,如果该应用程序中的代码尝试除以零,您将收到一个 EXCEPTION_DEBUG_EVENT。为此事件返回的缓冲区是:

Public Enum ExceptionCodes
    EXCEPTION_GUARD_PAGE_VIOLATION = &H80000001
    EXCEPTION_DATATYPE_MISALIGNMENT = &H80000002
    EXCEPTION_BREAKPOINT = &H80000003
    EXCEPTION_SINGLE_STEP = &H80000004
    EXCEPTION_ACCESS_VIOLATION = &HC0000005
    EXCEPTION_IN_PAGE_ERROR = &HC0000006
    EXCEPTION_INVALID_HANDLE = &HC0000008
    EXCEPTION_NO_MEMORY = &HC0000017
    EXCEPTION_ILLEGAL_INSTRUCTION = &HC000001D
    EXCEPTION_NONCONTINUABLE_EXCEPTION = &HC0000025
    EXCEPTION_INVALID_DISPOSITION = &HC0000026
    EXCEPTION_ARRAY_BOUNDS_EXCEEDED = &HC000008C
    EXCEPTION_FLOAT_DENORMAL_OPERAND = &HC000008D
    EXCEPTION_FLOAT_DIVIDE_BY_ZERO = &HC000008E
    EXCEPTION_FLOAT_INEXACT_RESULT = &HC000008F
    EXCEPTION_FLOAT_INVALID_OPERATION = &HC0000090
    EXCEPTION_FLOAT_OVERFLOW = &HC0000091
    EXCEPTION_FLOAT_STACK_CHECK = &HC0000092
    EXCEPTION_FLOAT_UNDERFLOW = &HC0000093
    EXCEPTION_INTEGER_DIVIDE_BY_ZERO = &HC0000094
    EXCEPTION_INTEGER_OVERFLOW = &HC0000095
    EXCEPTION_PRIVILEGED_INSTRUCTION = &HC0000096
    EXCEPTION_STACK_OVERFLOW = &HC00000FD
    EXCEPTION_CONTROL_C_EXIT = &HC000013A
End Enum

Public Enum ExceptionFlags
    EXCEPTION_CONTINUABLE = 0
    EXCEPTION_NONCONTINUABLE = 1   '\\ Noncontinuable exception
End Enum

Private Type DEBUG_EXCEPTION_DEBUG_INFO
    Header As DEBUG_EVENT_HEADER
    ExceptionCode                                        As ExceptionCodes
    ExceptionFlags                                       As ExceptionFlags
    pExceptionRecord                                     As Long
    ExceptionAddress                                     As Long
    NumberParameters                                     As Long
    ExceptionInformation(EXCEPTION_MAXIMUM_PARAMETERS)   As Long
    dwFirstChance As Long
End Type

异常标志告诉您是否可能从异常中恢复。

CREATE_THREAD_DEBUG_EVENT

当被调试应用程序创建新线程时,会发生此事件。传入的缓冲区是:

Private Type DEBUG_CREATE_THREAD_DEBUG_INFO
    Header As DEBUG_EVENT_HEADER
    hThread As Long
    lpThreadLocalBase As Long
    lpStartAddress As Long
End Type

这为您提供了线程句柄(用于线程控制 API 调用)以及被调试进程中线程的基地址和起始地址,这对于分析该应用程序的内存非常有用。

CREATE_PROCESS_DEBUG_EVENT

当进程创建时,会发生此事件。传入的缓冲区是:

Private Type DEBUG_CREATE_PROCESS_DEBUG_INFO
    Header As DEBUG_EVENT_HEADER
    hfile As Long
    hProcess As Long
    hThread As Long
    lpBaseOfImage As Long
    dwDebugInfoFileOffset As Long
    nDebugInfoSize As Long
    lpThreadLocalBase As Long
    lpStartAddress As Long
    lpImageName As Long
    fUnicode As Integer
End Type

您可以根据此缓冲区中的文件句柄,按照 本文 来查找进程的不同部分(导入部分、导出、调试信息等)。

EXIT_THREAD_DEBUG_EVENT

当线程退出时,会发生此事件。传入的缓冲区是:

Private Type DEBUG_EXIT_THREAD_DEBUG_INFO
    Header As DEBUG_EVENT_HEADER
    dwExitCode As Long
End Type

退出代码是线程设置的任何值,但如果错误导致线程退出,通常会将其设置为非零。

EXIT_PROCESS_DEBUG_EVENT

当进程退出时,会发生此事件。传入的缓冲区是:

Private Type DEBUG_EXIT_PROCESS_DEBUG_INFO
    Header As DEBUG_EVENT_HEADER
    dwExitCode As Long
End Type

退出代码是进程设置的任何值,但如果错误导致线程退出,通常会将其设置为非零。收到此事件后,您应该停止调试循环。

LOAD_DLL_DEBUG_EVENT

当被调试的应用程序加载动态链接库时,会发生此事件。传入的缓冲区是:

Private Type DEBUG_LOAD_DLL_DEBUG_INFO
    Header As DEBUG_EVENT_HEADER
    hfile As Long
    lpBaseOfDll As Long
    dwDebugInfoFileOffset As Long
    nDebugInfoSize As Long
    lpImageName As Long
    fUnicode As Integer
End Type

您可以根据此缓冲区中的文件句柄,按照 本文 来查找 DLL 的不同部分(导入部分、导出、调试信息等)。

UNLOAD_DLL_DEBUG_EVENT

当被调试进程卸载已加载的 DLL 时,会发生此事件。传入的缓冲区是:

Private Type DEBUG_UNLOAD_DLL_DEBUG_INFO
    Header As DEBUG_EVENT_HEADER
    lpBaseOfDll As Long
End Type

您可以使用缓冲区中的 lpBaseOfDll 值来识别哪个 DLL 被卸载了。

OUTPUT_DEBUG_STRING_EVENT

当被调试进程调用 OutputDebugString API 调用向附加的调试器发送调试信息时,会发生此事件。传入的缓冲区是:

Private Type DEBUG_OUTPUT_DEBUG_STRING_INFO
    Header As DEBUG_EVENT_HEADER
    lpDebugStringData As Long
    fUnicode As Integer
    nDebugStringLength As Integer
End Type

您可以使用 ReadProcessMemory API 调用从被调试进程中读取字符串。

RIP_EVENT

如果您的被调试进程意外终止,会发生此事件。传入的缓冲区是:

Private Type DEBUG_RIP_INFO
    Header As DEBUG_EVENT_HEADER
    dwError As Long
    dwType As Long
End Type

恢复被调试进程

一旦您从调试事件中提取了所需信息,您就需要恢复被调试进程,使其能够继续运行。为此,您需要调用 ContinueDebugEvent API 调用。

Public Enum DebugStates
    DBG_CONTINUE = &H10002
    DBG_TERMINATE_THREAD = &H40010003
    DBG_TERMINATE_PROCESS = &H40010004
    DBG_CONTROL_C = &H40010005
    DBG_CONTROL_BREAK = &H40010008
    DBG_EXCEPTION_NOT_HANDLED = &H80010001
End Enum

Private Declare Function ContinueDebugEvent Lib "kernel32" _
                                       (ByVal dwProcessId As Long, _
                                       ByVal dwThreadId As Long, _
                                       ByVal dwContinueStatus As DebugStates) As Long

进一步开发

要扩展此框架并创建一个完整的调试器,需要能够遍历被调试进程的内存和堆栈,以及设置断点。我希望能在下一篇文章中介绍这些内容。

历史

  • 2003年10月21日:初始发布
© . All rights reserved.