调试器的工作原理

在本文中,将探讨商业可用调试器的某些方面,特别是针对操作系统(OS)和CPU操作码(x86-32位)。文章将展示断点和OutputDebugString的工作方式,这两种事件在调试过程中非常常见。建议读者进一步研究条件断点和逐步执行(逐行执行),这些功能大多数调试器都支持。“运行到光标”与断点类似。

在开始之前,读者需要具备基本的操作系统知识。与操作系统相关的讨论超出了本文的范围。如果需要,可以参考其他文章或联系作者。读者需要熟悉商业可用的调试器(本文以VS2010为例),并且在使用断点调试应用程序之前有一定的经验。

断点

断点允许用户在被调试程序的执行流程中设置一个中断。用户可以这样做,以便在执行的这一点评估某些条件。调试器会在特定的地址(用户希望设置断点的地方)的可执行文件进程空间中添加一个指令:

int 3 (操作码: 0xcc)

遇到这个指令后:

  • EIP移动到中断服务例程(在这种情况下是int 3)。
  • 服务例程将保存CPU寄存器(所有中断服务例程都必须这样做),并向附加的调试器发送信号,调用DebugActiveProcess(正在调试的exe的进程ID)查找MSDN中的这个API。
  • 调试器将运行一个调试循环(在代码中称为EnterDebugLoop(),在文件Debugger.cpp中)。服务例程的信号将触发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从另一个进程获取字符串(内存转储)。

使用代码

在阅读本文时,必须始终参考附加的代码。通过以下方式引入断点(操作码:0xcc):

BYTE p[] = {0xcc}; //0xcc=int 3 ::WriteProcessMemory(pi.hProcess, (void*)address_to_set_breakpoint, p, sizeof(p), &d);

第二个参数是需要放置断点指令的地址,通过.PDB文件(调试符号文件)查找。通过.PDB文件,VS2010可以准确地将断点放置在与生成指令的代码行对应的内存位置。上述方法被注释掉了,原因是不能准确地放置断点,因为没有使用任何调试符号,而是使用::DebugBreak()在被调试的进程中引起断点,参考代码。

VS2010创建的断点

对于使用VS2010调试(任何)应用程序的读者 - 如果断点放置在代码中(其可执行文件是使用调试设置创建的),使用VS2010 IDE(通过按F9),内存调试视图不会显示0xcc。读者必须转储断点创建点的内存,当然,地址位置必须通过反汇编查找(由于目前正在调试应用程序,可以按ALT-8)。

调试循环

在附加代码中,使用了EIP的值,从以下代码中获取EIP的值(代码注释使这自解释):

UINT EIP = 0; _asm { call f jmp finish f: pop eax mov EIP,eax push eax ret finish: } // print the memory dump BYTE *b = (BYTE*)EIP; for (int i = 0; i < 200; i++) printf("%x : %x \n", EIP+i, b[i]); 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); 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
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485