调试器如何工作?


170

我一直想知道调试器如何工作?特别是可以“附加”到已运行的可执行文件的程序。我知道编译器会将代码翻译成机器语言,但是调试器如何“知道”它所附加的内容?



@Oktalist本文很有趣,但仅讨论了在Linux上进行调试的API级别抽象。我想OP想要了解更多有关内幕的信息。
smwikipedia

Answers:


96

调试器如何工作的详细信息取决于您要调试的内容以及操作系统是什么。对于Windows上的本机调试,您可以在MSDN上找到一些详细信息:Win32 Debugging API

用户通过名称或进程ID告诉调试器要附加到哪个进程。如果是名称,则调试器将查找进程ID,并通过系统调用启动调试会话。在Windows下,这将是DebugActiveProcess

附加后,调试器将进入事件循环,就像任何UI一样,但是操作系统将根据正在调试的过程中发生的事件(例如发生的异常)来生成事件,而不是窗口系统发出的事件。请参见WaitForDebugEvent

调试器能够读取和写入目标进程的虚拟内存,甚至可以通过操作系统提供的API调整其寄存器值。请参阅Windows 调试功能列表。

调试器能够使用符号文件中的信息将地址从源代码转换为变量名称和位置。符号文件信息是一组单独的API,因此并不是操作系统的核心部分。在Windows上,这是通过Debug Interface Access SDK进行的

如果要调试托管环境(.NET,Java等),则该过程通常看起来很相似,但是细节不同,因为虚拟机环境提供了调试API而不是底层操作系统。


5
这个问题听起来很愚蠢,但是如果到达程序内部的特定地址,操作系统如何跟踪。例如,在地址0x7710cafe上设置了一个breackpoint。随着指令指针的更改,OS(或CPU)可能必须将指令指针与所有断点地址进行比较,还是我弄错了?这是如何运作的 ..?
displayname

3
@StefanFalk我写了一个答案,解决了一些较低级别的细节(在x86上)。
Jonathon Reinhart 2014年

您能否解释一下变量名如何精确映射到地址?每次运行时,应用程序是否使用相同的内存地址作为变量?我一直认为它只是从可用内存中映射而来,但从未真正考虑过这些字节是否会直接映射到应用程序内存空间中的同一位置。看来这将是一个主要的安全问题。
詹姆斯·约书亚街

@JamesJoshuaStreet我可以想象这是调试器特有的细节。
moonman239

这个答案揭示了一些东西。但是我认为op对一些底层细节而不是某些API抽象更感兴趣。
smwikipedia

63

据我了解:

对于x86上的软件断点,调试器将指令的第一个字节替换为CCint3)。这是WriteProcessMemory在Windows上完成的。当CPU到达该指令并执行时int3,这将导致CPU生成调试异常。OS接收到此中断,意识到正在调试进程,并通知调试器进程已命中断点。

击中断点并停止过程后,调试器将在其断点列表中查找,并将替换为CC原来存在的字节。调试器套TF,所述陷阱标志EFLAGS(通过修改CONTEXT),并且继续处理。陷阱标志使CPU INT 1在下一条指令上自动生成一个单步异常()。

当正在调试的进程下次停止时,调试器再次将断点指令的第一个字节替换为CC,然后进程继续。

我不确定这是否是所有调试器完全实现的方法,但是我编写了一个Win32程序,该程序使用此机制进行自我调试。完全没用,但是很有教育意义。


25

在Linux中,调试过程始于ptrace(2)系统调用。 本文有一个很棒的教程,介绍如何使用它ptrace来实现一些简单的调试结构。


1
是否(2)告诉我们更多(或小于)“ptrace的是一个系统调用”的东西吗?
拉泽尔

5
@eSKay,不是真的。的(2)是手动节号。有关手册部分的说明,请参见en.wikipedia.org/wiki/Man_page#Manual_sections
亚当·罗森菲尔德

2
@AdamRosenfield除了第2节专门为“系统调用”的事实。因此,间接地,是的,它告诉我们这ptrace是一个系统调用。
Jonathon Reinhart 2014年

1
实际上,这(2)告诉我们我们可以键入man 2 ptrace并获取正确的联机帮助页-在这里并不重要,因为没有其他ptrace歧义,但可以man printfman 3 printfLinux 进行比较。
苗条的

9

如果您使用的是Windows操作系统,John Robbins撰写的“调试Microsoft .NET和Microsoft Windows的应用程序”的最佳资源是:

(甚至旧版本:“调试应用程序”

本书有一章介绍了调试器的工作方式,其中包括一些简单(但有效)的调试器的代码。

由于我不熟悉Unix / Linux调试的详细信息,因此这些内容可能根本不适用于其他OS。但是我想作为一个非常复杂的主题的介绍,这些概念(如果不是细节和API)应该“移植”到大多数操作系统。


3

了解调试的另一个有价值的资料是Intel CPU手册(Intel®64和IA-32体系结构软件开发人员手册)。在第3A卷第16章中,它介绍了调试的硬件支持,例如特殊异常和硬件调试寄存器。以下是该章的内容:

T(陷阱)标志,TSS-尝试切换到在其TSS中设置了T标志的任务时,生成调试异常(#DB)。

我不确定Window或Linux是否使用此标志,但是阅读该章非常有趣。

希望这对某人有帮助。


2

我认为这里有两个主要问题要回答:

1.调试器如何知道发生了异常?

当正在调试的进程中发生异常时,在目标进程中定义的任何用户异常处理程序有机会响应该异常之前,操作系统会通知调试器。如果调试器选择不处理此(优先机会)异常通知,则异常分发序列将继续进行,然后如果目标线程愿意,则为目标线程提供处理异常的机会。如果目标进程未处理SEH异常,则将向调试器发送另一个调试事件,称为第二次机会通知,以通知它目标进程中发生未处理的异常。资源

在此处输入图片说明


2.调试器如何知道如何在断点处停止?

简化的答案是:当您在程序中放置一个断点时,调试器将用int3指令替换该点的代码,该指令是软件中断。结果是程序被挂起并调用调试器。


1

我的理解是,当您编译应用程序或DLL文件时,其编译后的内容都会包含代表函数和变量的符号。

使用调试版本时,这些符号比发布版本时要详细得多,从而使调试器可以为您提供更多信息。将调试器附加到进程时,它会查看当前正在访问哪些函数,并从此处解析所有可用的调试符号(因为它知道编译文件的内部结构是什么样,因此可以确定内存中可能有什么内容) ,以及int,float,string等的内容)。就像第一个海报所说的那样,这些信息以及这些符号的工作方式很大程度上取决于环境和语言。


2
这只是关于符号。调试远远不止符号。
Jonathon Reinhart 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.