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





5.00/5 (10投票s)
本系列文章的第 2 部分,展示了如何在 VB 中编写调试器。
引言
Windows NT(及其后续操作系统版本)公开了一组 API 函数和结构,用于调试正在运行的进程。本文展示了如何从 Visual Basic(版本 5 或 6)访问这些函数。建议将本文 《可执行文件的内部:VB 程序员的 PE 文件格式入门》 与本文结合阅读,并且其附带的源代码也包含了本文的代码。
要调试的进程
有两种方法可以获得要调试的进程。要么
- 将调试器附加到已运行的进程
- 启动一个带有附加调试器的新进程。
或
启动一个带有附加调试器的新进程
要启动一个进程,您可以使用 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日:初始发布