windows环境下的调试器探究
字数 1589 2025-08-07 08:22:39
Windows环境下的调试器探究
0x00 前言
在Windows环境中,调试器主要通过三种方式触发异常:软件断点、内存断点和硬件断点。本文将详细分析这三种方式的原理,并通过实际代码实现调试器的核心功能。
0x01 软件断点
原理分析
软件断点通过将目标指令替换为0xCC(即int 3指令)实现:
- CPU检测到
INT 3指令 - 查IDT表找到对应的中断处理函数
- 调用
CommonDispatchException - 通过
KiDispatchException分发异常 DbgkForwardException收集并发送调试事件
关键点:
- 调试器会修改目标指令为
0xCC - 异常处理流程会经过内核到用户态的转换
- 如果没有内核调试器存在,3环调试器才能接收到异常
- 调试进程会被挂起(
DbgkpSuspendProcess)
实现代码关键部分
// 设置软件断点
VOID SetInt3BreakPoint(LPVOID addr)
{
CHAR int3 = 0xCC;
ReadProcessMemory(hDebuggeeProcess, addr, &OriginalCode, 1, NULL);
WriteProcessMemory(hDebuggeeProcess, addr, &int3, 1, NULL);
}
// 处理INT 3异常
BOOL Int3ExceptionProc(EXCEPTION_DEBUG_INFO *pExceptionInfo)
{
// 1. 恢复原始指令
if(!bIsSystemInt3) {
WriteProcessMemory(hDebuggeeProcess,
pExceptionInfo->ExceptionRecord.ExceptionAddress,
&OriginalCode, 1, NULL);
}
// 2. 修正EIP(INT 3异常EIP会停在断点+1的位置)
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hDebuggeeThread, &Context);
Context.Eip--;
SetThreadContext(hDebuggeeThread, &Context);
// 3. 显示断点信息并等待用户命令
printf("Int 3断点 : 0x%p \r\n", pExceptionInfo->ExceptionRecord.ExceptionAddress);
// ...
}
调试循环框架
BOOL ExceptionTest()
{
// 1. 创建调试进程
CreateProcess(DEBUGGEE, NULL, NULL, NULL, TRUE,
DEBUG_PROCESS || DEBUG_ONLY_THIS_PROCESS,
NULL, NULL, &startupInfo, &pInfo);
// 2. 调试循环
while(nIsContinue) {
WaitForDebugEvent(&debugEvent, INFINITE);
switch(debugEvent.dwDebugEventCode) {
case EXCEPTION_DEBUG_EVENT:
ExceptionHandler(&debugEvent);
break;
case CREATE_PROCESS_DEBUG_EVENT:
SetInt3BreakPoint((PCHAR)debugEvent.u.CreateProcessInfo.lpStartAddress);
break;
// 其他事件处理...
}
ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE);
}
}
0x02 内存断点
原理分析
内存断点分为两种类型:
- 内存访问:内存被读写时产生中断
- 内存写入:内存被写入时产生中断
实现原理:
- 使用
VirtualProtectEx修改内存页属性 - 内存访问断点:设置为
PAGE_NOACCESS(PTE的P位=0) - 内存写入断点:设置为
PAGE_EXECUTE_READ(PTE的P位=1,R/W位=0)
异常处理流程:
- CPU访问错误的内存地址,触发页异常
- 查IDT表找到中断处理函数(
nt!_KiTrap0E) CommonDispatchExceptionKiDispatchExceptionDbgkForwardException收集并发送调试事件
实现代码关键部分
// 设置内存断点
VOID SetMemBreakPoint(PCHAR pAddress)
{
// 访问断点
VirtualProtectEx(hDebuggeeProcess, pAddress, 1,
PAGE_NOACCESS, &dwOriginalProtect);
// 写入断点
// VirtualProtectEx(hDebuggeeProcess, pAddress, 1,
// PAGE_EXECUTE_READ, &dwOriginalProtect);
}
// 处理访问异常
BOOL AccessExceptionProc(EXCEPTION_DEBUG_INFO *pExceptionInfo)
{
// 1. 获取异常信息
DWORD dwAccessFlag = pExceptionInfo->ExceptionRecord.ExceptionInformation[0];
DWORD dwAccessAddr = pExceptionInfo->ExceptionRecord.ExceptionInformation[1];
// 2. 恢复内存属性
VirtualProtectEx(hDebuggeeProcess, (VOID*)dwAccessAddr, 1,
dwOriginalProtect, &dwProtect);
// 3. 显示异常信息
printf("内存断点 : dwAccessFlag - %x dwAccessAddr - %x \n",
dwAccessFlag, dwAccessAddr);
// 4. 获取上下文(EIP不需要修正)
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hDebuggeeThread, &Context);
printf("Eip: 0x%p \n", Context.Eip);
// ...
}
0x03 硬件断点
原理分析
硬件断点依赖于CPU的调试寄存器(Dr0-Dr7):
- Dr0-Dr3:存储断点地址(最多4个硬件断点)
- Dr6:调试状态寄存器
- Dr7:调试控制寄存器
Dr7关键位:
- L0/G0 ~ L3/G3:控制Dr0-Dr3是否有效(局部/全局)
- LENx(16-19位):断点长度(00=1字节,01=2字节,11=4字节)
- R/Wx(16-17位):断点类型(00=执行,01=写入,11=访问)
异常处理流程:
- CPU检测到当前线性地址与调试寄存器中的地址匹配
- 查IDT表找到中断处理函数(
nt!_KiTrap01) CommonDispatchExceptionKiDispatchExceptionDbgkForwardException收集并发送调试事件
实现代码关键部分
// 设置硬件断点
VOID SetHardBreakPoint(PVOID pAddress)
{
// 1. 获取线程上下文
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hDebuggeeThread, &Context);
// 2. 设置断点位置
Context.Dr0 = (DWORD)pAddress;
Context.Dr7 |= 1; // 启用Dr0
// 3. 设置断点类型和长度
Context.Dr7 &= 0xfff0ffff; // 执行断点(16-17位=00),1字节(18-19位=00)
// 4. 设置线程上下文
SetThreadContext(hDebuggeeThread, &Context);
}
// 处理单步异常(硬件断点)
BOOL SingleStepExceptionProc(EXCEPTION_DEBUG_INFO *pExceptionInfo)
{
// 1. 获取线程上下文
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hDebuggeeThread, &Context);
// 2. 判断异常原因
if(Context.Dr6 & 0xF) { // B0-B3不为空,硬件断点
printf("硬件断点:%x 0x%p \n", Context.Dr7&0x00030000, Context.Dr0);
// 清除断点
Context.Dr0 = 0;
Context.Dr7 &= 0xfffffffe;
} else { // 单步异常
printf("单步:0x%p \n", Context.Eip);
Context.Dr7 &= 0xfffffeff;
}
SetThreadContext(hDebuggeeThread, &Context);
// ...
}
综合调试器实现
完整调试循环框架:
int main(int argc, char* argv[])
{
// 1. 创建调试进程
STARTUPINFO startupInfo = {0};
PROCESS_INFORMATION pInfo = {0};
CreateProcess(DEBUGGEE, NULL, NULL, NULL, TRUE,
DEBUG_PROCESS || DEBUG_ONLY_THIS_PROCESS,
NULL, NULL, &startupInfo, &pInfo);
hDebuggeeProcess = pInfo.hProcess;
// 2. 调试循环
while(nIsContinue) {
WaitForDebugEvent(&debugEvent, INFINITE);
switch(debugEvent.dwDebugEventCode) {
case EXCEPTION_DEBUG_EVENT:
ExceptionHandler(&debugEvent);
break;
case CREATE_PROCESS_DEBUG_EVENT:
// 选择设置哪种断点
SetInt3BreakPoint((PCHAR)debugEvent.u.CreateProcessInfo.lpStartAddress);
// SetMemBreakPoint((PCHAR)debugEvent.u.CreateProcessInfo.lpStartAddress);
break;
// 其他事件处理...
}
ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE);
}
return 0;
}
关键点总结
-
软件断点:
- 修改指令为
0xCC(INT 3) - 需要修正EIP(EIP会停在断点+1的位置)
- 系统断点和用户断点需要区分处理
- 修改指令为
-
内存断点:
- 通过修改内存页属性触发异常
- 访问断点(PAGE_NOACCESS)和写入断点(PAGE_EXECUTE_READ)
- 异常信息中包含访问类型(读/写)和访问地址
-
硬件断点:
- 使用CPU调试寄存器(Dr0-Dr7)
- 最多4个硬件断点(Dr0-Dr3)
- 通过Dr6判断哪个寄存器触发了异常
- 单步异常和硬件断点异常共用异常类型
-
调试框架:
- 使用
DEBUG_PROCESS标志创建调试进程 WaitForDebugEvent和ContinueDebugEvent构成主循环- 处理多种调试事件(异常、进程/线程创建、DLL加载等)
- 使用
-
线程上下文:
- 使用
GetThreadContext和SetThreadContext访问/修改寄存器 - 硬件断点需要在线程上下文中设置
- 上下文标志
CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS获取完整信息
- 使用
通过这三种断点技术的组合,可以实现功能强大的调试器,满足不同的调试需求。