编写自己的调试器以处理断点






4.55/5 (10投票s)
基本调试器,断点,OutputDebugString
引言
构建你自己的调试器是了解商业调试器工作原理的好方法。在本文中,读者将接触到操作系统和 CPU 指令码(仅限 x86-32 位)的某些方面。本文将展示断点的工作原理以及 OutputDebugString
的工作原理(因为我们只处理这两个事件),这在调试时很常用。鼓励读者研究大多数调试器通常支持的条件断点和逐行执行。
背景
在我们开始之前,读者需要具备操作系统的基本知识。与 OS 相关的讨论超出了本文的范围。请随意参考其他文章(或给我写信)来阅读本文。读者需要接触过商业调试器(本文为 VS2010),并使用断点调试过应用程序。
断点
断点允许用户在被调试程序的执行流程中设置一个中断点。用户可以这样做来评估执行到该点的某些条件。
调试器在被调试的可执行文件的进程空间中,在目标地址处插入一条指令:int 3
(指令码:0xcc)。遇到此指令后
- EIP 会移至中断服务例程(在此情况下为
int 3
)。 - 服务例程将保存 CPU 寄存器(所有中断服务例程都必须这样做),通知已附加的调试器,调用
DebugActiveProcess(被调试可执行文件的进程 ID)
的程序会在 MSDN 中查找此 API。 - 调试器将运行一个调试循环(在代码中称为 Debugger.cpp 文件中的
EnterDebugLoop()
)。服务例程的信号将触发WaitForDebugEvent(&de, INFINITE)
,调试循环(在代码中称为EnterDebugLoop
)将遍历WaitForDebugEvent
遇到的每个调试信号。处理完调试例程后,调试器将通过用原始指令替换0xcc (int 3)
来恢复指令,并通过调用ContinueDebugEvent(de.dwProcessId, de.dwThreadId, DBG_CONTINUE)
从服务例程返回。(在设置断点之前,调试器必须使用ReadProcessMemory
来获取该内存位置的原始字节)。 - 当从中断服务例程返回(使用
IRET
)时,EIP 将指向下一条要执行的指令,但我们希望它指向前一个字节(已恢复的那个字节),这是在处理断点时完成的。尽管正在处理断点服务例程(其 EIP 指向某个服务例程中的位置),GetThreadContext
将返回 EIP 移至int 3
服务例程之前的寄存器值。将 EIP 减去 1,使用SetThreadContext
来设置 EIP。
OutputDebugString
此 API 用于在调试控制台上显示一个字符串
,用户可以使用它来显示某些与状态相关的信息或进行跟踪。
- 当调用此 API 时,会触发
OUTPUT_DEBUG_STRING_EVENT
事件。 - 已附加的调试器将在调试循环中处理此事件(在代码中称为
EnterDebugLoop
)。 - 事件处理 API 将提供与调试目标进程空间相关的
字符串
信息。 - 使用
ReadProcessMemory
从另一个进程获取字符串
(内存转储)。
Using the Code
阅读本文时,请始终参考附加的代码。断点(指令码:0xcc)由
BYTE p[]={0xcc}; //0xcc=int 3
::WriteProcessMemory(pi.hProcess,(void*)address_to_set_breakpoint, p, sizeof(p), &d);
第二个参数(需要放置断点指令的地址)在.PDB 文件(调试符号文件)中查找。
通过.PDB 文件,VS2010 可以准确地将断点放置在与导致在该内存位置生成指令的代码行对应的内存位置。
上述方法已注释掉,原因是由于我没有使用任何调试符号,因此无法准确放置断点,而是使用 ::DebugBreak();
来在被调试进程中产生断点,请参考代码。
鼓励读者尝试使用 WriteProcessMemory
API,但对于本文,我无法使用它,因为 WriteProcessMemory
的地址(第二个参数)的值只有在编译代码后才知道(并希望操作系统能为 EIP 分配相同的值)。
VS2010 创建的断点
对于需要使用 VS2010 调试(任何)应用程序的读者,如果断点放置在代码中(其可执行文件是使用调试设置创建的),使用 VS2010 IDE(按 F9)时,内存调试视图将不会显示 0xcc。读者需要转储创建断点处的内存,当然,地址位置需要通过反汇编来查找(因为您正在调试应用程序,可以按 ALT-8)。
在附加的代码中,我使用了 EIP 的值,我们通过以下代码获取 EIP 的值(代码注释对此有详细说明)。
UINT EIP=0; //declare some variable
_asm {
call f //this will push the current eip value on to stack
jmp finish
f: pop eax //get the last value from the stack, in our case value of eip
mov EIP,eax //store the value of eip in some memory
push eax //restore the stack
ret //return
finish:
}
// print the memory dump
BYTE *b=(BYTE*)EIP;
for(int i=0; i<200; i++) printf("%x : %x \n",EIP+i,b[i]);
主循环(调试器使用)引用 Debugger.cpp 文件中的 void EnterDebugLoop()
。WaitForDebugEvent
API 用于处理使用 DebugActiveProcess
(调试目标进程 ID)附加到调用进程的任何进程的调试事件。
WaitForDebugEvent(&de, INFINITE); //will wait till a debug event is triggered
switch (de.dwDebugEventCode)
{
case EXCEPTION_DEBUG_EVENT:
switch(de.u.Exception.ExceptionRecord.ExceptionCode)
{
case EXCEPTION_BREAKPOINT:
MessageBoxA(0,"Found break point","",0);
break;
}
break;
case OUTPUT_DEBUG_STRING_EVENT:
{
char a[100];
ReadProcessMemory(pi.hProcess,de.u.DebugString.lpDebugStringData,
a,de.u.DebugString.nDebugStringLength,NULL); //mentioned earlier to read memory
//from another process.
printf("output from debug string is: %s",a);
}
break;
}
ContinueDebugEvent(de.dwProcessId, de.dwThreadId, DBG_CONTINUE); // After the debug event
// is handled, Debugger must call
ContinueDebugEvent
将允许调试器继续(该线程)报告调试事件。
关注点
现在我们知道编写自己的调试器/性能分析工具并不难。在掌握了编写简单调试器的基础知识后,鼓励读者编写更复杂的调试器。