Loading... ##### **附加/创建** 为了调试一个进程,调试器必须能够附加到它。这意味着调试器应该有一种方式与进程交互。使得调试器可以访问进程地址空间、停止和继续执行的能力、修改寄存器等。同样,调试器应该能够安全地与进程分离,并在调试会话结束时让它继续运行的进程。在windows平台上,这是通过**[DebugActiveProcess](https://docs.microsoft.com/zh-cn/windows/win32/api/debugapi/nf-debugapi-debugactiveprocess?redirectedfrom=MSDN)**函数并指定目标的进程标识符来实现的。或者,也可以通过调用[CreateProcess](https://docs.microsoft.com/zh-cn/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessa?redirectedfrom=MSDN)来完成使用DEBUG_PROCESS或DEBUG_ONLY_THIS_PROCESS创建标志。后一种方法创建一个新进程并附加到它,而不是附加到系统上已经运行的进程。附加后,调试器可以使用[DebugSetProcessKillOnExit](https://docs.microsoft.com/zh-cn/windows/win32/api/winbase/nf-winbase-debugsetprocesskillonexit?redirectedfrom=MSDN)指定是否在分离时终止进程的策略。最后,分离过程是对DebugActiveProcessStop的直接调用。 ##### **调试循环** 任何调试器的核心组件都是调试循环。这是负责等待调试事件,处理所述事件,然后等待下一个事件事件,这样循环。在windows平台上,这非常简单。实际上,它非常简单,微软就此步骤需要完成的工作编写了一个简短的[调试器Demo](https://docs.microsoft.com/zh-cn/windows/win32/debug/writing-the-debugger-s-main-loop?redirectedfrom=MSDN)页面。大致的步骤是(在一个循环中)调试器调用[WaitForDebugEvent](https://docs.microsoft.com/zh-cn/windows/win32/api/debugapi/nf-debugapi-waitfordebugevent?redirectedfrom=MSDN),它等待来自进程的调试事件。该函数返回后,将使用与特定调试事件相关的信息填充`DEBUG_EVENT`结构。至此,调试器的工作就是处理事件。处理事件之后,处理程序必须向[ContinueDebugEvent](https://https://docs.microsoft.com/en-us/windows/win32/api/debugapi/nf-debugapi-continuedebugevent)返回处理状态,`dwContinueStatus`是一个`DWOR`类型变量,用于告诉引发事件的线程在处理事件后如何继续执行。对于大多数事件,即`CREATE_PROCESS_DEBUG_EVENT`,`LOAD_DLL_DEBUG_EVENT`等等,你想要继续执行,因为这些事件并未反映出程序行为的任何错误,而是通知调试器更改过程状态的事件。通过选择`DBG_CONTINUE`作为继续状态来完成此操作。对于异常情况,例如导致未定义程序行为的异常(例如访问冲突,非法指令执行,除零等),这是程序无法自己解决的问题。此时的调试器工作是收集和显示与崩溃有关的信息,并且在大多数情况下,终止进程。对于这些异常,终止可以在处理程序内部进行,或者调试器可以继续使用`DBG_EXECPTION_NOT_HANDLED`进行调试事件返回。保持原状,表示调试器放弃处理此条异常。几乎所有情况下,程序随后会立即终止。但是,有时会出现一些极端情况,尤其是在恶意软件中,在该过程中,进程将安装其自己的运行时异常处理程序作为一种混淆技术,并生成自己的运行时异常以在该处理程序中进行处理以实现某些功能。在这种情况下继续执行异常不会导致崩溃,因为在调试器将沿着异常处理程序链转发后,该进程能够处理其自身的异常。 ##### **断点** 断点可以简单地定义为正在执行的代码中导致故意中断的位置。在调试器的上下文中,这很有用,因为它允许调试器在什么也没有发生的时间(即过程实际上"中断")时检查过程。当目标处于中断状态时,调试器中会看到的典型功能是查看和修改寄存器/内存,打印断点周围区域的反汇编列表,进入或移至汇编指令或源代码行的能力,和其它相关的诊断功能。断点本身可以有几种不同的形式。 **硬件中断断点** 硬件中断断点可能是在调试器中了解和实现的最常见和最简单的断点。这些特定于x86和x64体系结构,并通过使用硬件支持的指令来实现,该指令专门用于生成调试器专用的软件中断。该指令的操作码为`0xCC`,它与INT3指令匹配。大多数调试器实现此目的的方式是将期望地址(即断点地址)处的操作码替换为`0xCC`操作码。调用代码后,中断(`EXCEPTION_BREAKPOINT`),调试器将处理并为用户提供执行上述调试功能的选项。当用户完成检查该地址处的程序状态并希望继续时,调试器将替换原始指令,确保执行地址(EIP或RIP寄存器,取决于体系架构)指向该原始地址,并且继续执行。 但是,出现了一个有趣的问题。当原始指令被替换回时,断点将丢失。如果断点仅被命中一次,这可能没问题,但这种情况很少发生。之后需要一种方法可以立即重新启动该断点。这是通过设置`EFlags`(或x64的RFlags)寄存器将处理器设置为单步模式来实现的。幸运的是,通过启动第8位(即与0x100进行"或"运算)可以很容易地实现这一点。当`EXCEPTION_BREAKOINT`异常被处理并恢复执行,将有另一个异常,`EXCEPTION_SINGLE_STEP`,引发下一条执行的指令。此时,调试器可以重新启动上一条指令的断点并恢复执行。 **硬件调试寄存器** 第二种断点实现技术也特定于`x86`和`x64`体系结构,并利用了指令集提供的特定调试器。这些是调试器`DR0`,`DR1`,`DR2`,`DR3`和`DR7`。前四个寄存器用于保存硬件断点将断开的地址。使用这些意味着对于整个程序,最多可以有四个硬件断点。`DR7`寄存器用于控制这些寄存器的使用,位0、2、4、6对应于`DR0`,...,`DR3`的开/关。位16-17、20-21、24-25和28-29充当`DR0`,...,`DR3`的位掩码,用于触发这些断点的条件,执行时为`00`,读取时为`01`,写入时为`11`。 在windows平台上设置这些断点有些棘手。它们必须在进程主线程上设置。这涉及获取主线程,使用至少THREAD_GET_CONTEXT和THREAD_SET_CONTEXT特权打开该线程的句柄,并使用带有新添加的调试寄存器的GetThreadContext/SetThreadContext获取/设置线程上下文,以反映更改。请注意,没有修改内存中的可执行代码来设置这些断点。这与之前必须替换操作码的情况不同。这些事通过更改硬件寄存器的内容来设置和取消设置的断点。设置这些参数后将发生的情况是,该过程将引发在地址处命中指令后会发生EXCEPTION_SINGLE_STEP异常,然后调试器将以与上一节中的方式几乎同时的方法对其进行处理。由于数量的限制,这些内容不会再本文的示例断点代码中显示,但是出于完整性的考虑,最终可能会以后写出来。 **软件断点** 最后一类断点完全再软件中执行,并且与操作系统的功能密切相关。它们的另一个名称是内存断点。它们结合了中断断点的一些最佳功能,即具有任意数量的能力,以及硬件断点的最佳功能,这就是执行代码中的任何内容都不需要覆盖。但是,存在一个主要缺点:由于其实现,它们使代码的执行速度大大降低。 这些不是在地址级别实现的,而是在页面级别实现的。这些工作原理是使用`VirtualProtectEx`将要设置断点的地址的页面权限更改为保护页面的页面权限。当页面上的任何指令将被访问时,将抛出`EXCEPTION_GUARD_PAGE`异常。调试器将处理此异常,并检查有问题的地址是否是断点地址的地址。如果是这样,调试器可以项执行其它任何断点一样执行通常的处理/用户提示。如果不是,则调试器必须执行一些额外的步骤(如抛出异常)。 根据文档,保护页保护在提升后从页面上删除。这意味着一旦处理了异常并继续执行,此后的任何访问都将不会生成`EXCEPTION_GUARD_PAGE`异常。因此,如果所访问的指令不是所需的断点地址,则将断点将丢失。为了解决这个问题,将使用与硬件中断断点部分介绍的技术类似的技术。处理器将进入单步模式并继续执行。在下一条指令上,将有一个`EXCEPTION_SINGLE_STEP`引发异常。这将由调试器处理,并且将在页面上重新启用保护页面属性。 ##### 调试符号 在最简单的定义下,调试符号是一条信息,它显示了已编译程序的特定部分如何映射回源级别。例如,调试符号可能会告诉你有关内存地址中变量名称的信息,或一系列汇编指令映射到的代码行以及在那个文件中的信息。它们通常在调试构建期间生成,用于为正在调试(或逆向工程)一段代码的开发人员提供一些清晰度。一种语言没有通用的调试符号格式,并且它们在编译器之间可能会有所不同。在现代windows平台上,调试符号已程序数据库(`PDB`)文件的形式出现,以`.pdb`扩展名结尾。 初始化符号处理程序非常简单:仅涉及调用`SymInitialize`。该函数具有一个进程句柄,该句柄在附加时由调试器打开。用户搜索路径还有一个参数可以找到`PDB`文件,第三个参数可以指定调试器的目标进程,或者使用了延迟加载`DLL`,可能会导致某些符号无法加载。此外,如果第三个参数设置为`true`并且在接收到所有`LOAD_DLL_DEBUG_EVENT`事件之前初始化了符号处理了程序,则可能不会加载某些符号。 调试器的一个有用功能可能时在内部枚举模块的所有符号。这样可以在以后存储和快速查找。或者,它可以为用户提供图形显示,并可以从其名称轻松导航到符号地址。枚举符号是一个两步过程:首先调用`SymLoadModuleEx`来加载模块的符号信息,然后可以使用模块的基地址来调用`SymEnumSymbols`。`SymEnumSymbols`将`PSYM_ENUMERATESYMBOLS_CALLBACK`类型的回调作为参数。该回调将针对在模块符号表中找到每个符号进行调用,并将具有`SYMBOL_INFO`结构,该结构显示有关符号的信息,例如其名称,地址,是否为寄存器,如果其为常数则是什么值等。 原文链接: https://yeanhoo.gitee.io/yeanhoo/2021/04/02/%E8%B0%83%E8%AF%95%E5%99%A8%E5%9F%BA%E7%A1%800x2(%E8%B0%83%E8%AF%95%E5%99%A8%E6%A1%86%E6%9E%B6%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86)/ 最后修改:2021 年 10 月 19 日 03 : 00 PM © 允许规范转载