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


当前回答

我的理解是:

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

在命中断点并停止进程之后,调试器查看它的断点列表,并用原来的字节替换CC。调试器设置TF,即EFLAGS中的Trap标志(通过修改CONTEXT),并继续该过程。Trap标志导致CPU在下一条指令上自动生成一个单步异常(INT 1)。

当被调试的进程下一次停止时,调试器再次用CC替换断点指令的第一个字节,进程继续。

我不确定这是否是所有调试器都实现的方式,但我已经编写了一个Win32程序,它使用这种机制来调试自己。完全没用,但有教育意义。

其他回答

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

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

一旦附加,调试器将进入一个事件循环,就像任何UI一样,但事件不是来自窗口系统,而是操作系统将根据正在调试的进程中发生的事情生成事件——例如发生异常。看到WaitForDebugEvent。

调试器能够读写目标进程的虚拟内存,甚至通过操作系统提供的api调整其寄存器值。请参阅Windows的调试函数列表。

调试器能够使用符号文件中的信息将地址转换为源代码中的变量名和位置。符号文件信息是一组单独的api,并不是操作系统的核心部分。在Windows上,这是通过调试接口访问SDK实现的。

如果正在调试托管环境(。NET、Java等)的过程通常看起来类似,但细节不同,因为虚拟机环境提供的是调试API而不是底层操作系统。

我的理解是,当你编译一个应用程序或DLL文件时,无论它编译到什么,都包含表示函数和变量的符号。

When you have a debug build, these symbols are far more detailed than when it's a release build, thus allowing the debugger to give you more information. When you attach the debugger to a process, it looks at which functions are currently being accessed and resolves all the available debugging symbols from here (since it knows what the internals of the compiled file looks like, it can acertain what might be in the memory, with contents of ints, floats, strings, etc.). Like the first poster said, this information and how these symbols work greatly depends on the environment and the language.

如果你使用的是Windows操作系统,John Robbins写的《调试。net和Windows应用程序》是一个很好的参考资料:

http://www.amazon.com/dp/0735615365

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

这本书有一章是关于调试器如何工作的,其中包括几个简单(但可以工作的)调试器的代码。

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

在Linux中,调试进程从ptrace(2)系统调用开始。本文提供了一个很好的教程,介绍如何使用ptrace实现一些简单的调试构造。

我的理解是:

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

在命中断点并停止进程之后,调试器查看它的断点列表,并用原来的字节替换CC。调试器设置TF,即EFLAGS中的Trap标志(通过修改CONTEXT),并继续该过程。Trap标志导致CPU在下一条指令上自动生成一个单步异常(INT 1)。

当被调试的进程下一次停止时,调试器再次用CC替换断点指令的第一个字节,进程继续。

我不确定这是否是所有调试器都实现的方式,但我已经编写了一个Win32程序,它使用这种机制来调试自己。完全没用,但有教育意义。