Fai*_*hir 4 debugging x86 breakpoints visual-studio
当我在运行时向某些C#代码添加断点时,它会被命中.这究竟是如何发生的?
我想说,当在调试模式下运行时,Visual Studio会引用代码块,并且在运行时添加断点时,一旦在编译的代码中调用了引用,它就会被激活.
这是正确的假设吗?如果是这样,请您提供更多有关其工作原理的详细信息?
这实际上是一个相当大而复杂的主题,它也是特定于体系结构的,所以我只针对这个答案,提供英特尔(和兼容的)x86微体系结构的常见方法的总结.
好消息是,它与语言无关,因此无论是调试VB.NET,C#还是C++代码,调试器的工作方式都是一样的.这是正确的原因是所有代码最终都要编译(无论是静态[ 即,像C++一样提前还是像.NET一样使用JIT编译器])或动态[ 例如,通过运行时解释器] )对象可以由处理器本机执行的代码.调试器最终可以使用这个本机代码.
此外,这不仅限于Visual Studio.它的调试器肯定会以我将要描述的方式工作,但任何其他Windows调试器也是如此,例如Windows 调试工具(WinDbg,KD,CDB,NTSD等),GNU的GDB,IDA的调试器,open- source x64dbg,依此类推.
让我们从一个简单的定义开始 - 什么是断点?它只是一种允许执行暂停的机制,以便您可以进行进一步的分析,无论是检查调用堆栈,打印变量值,修改内存或寄存器的内容,甚至修改代码本身.
在x86架构上,有几种基本方法可以实现断点.它们可以分为软件断点和硬件断点这两大类.
虽然软件断点使用处理器本身的功能,但它主要在软件中实现,因此名称.具体而言,中断#3(汇编语言指令INT 3)提供断点中断.这可以放在可执行代码中的任何位置,当CPU在执行期间命中该指令时,它将陷阱.然后,调试器可以捕获此陷阱并执行它想要执行的任何操作.如果程序没有在调试器下运行,那么操作系统将处理陷阱; 操作系统的默认处理程序将简单地终止程序.
INT 3指令有两种可能的编码方式.也许最合乎逻辑的编码是0xCD 0x03,其中0xCD表示INT并0x03指定"参数",或者要触发的中断的编号.但是,由于断点非常重要,英特尔的设计人员还为INT 3单字节操作码添加了特殊情况表示0xCC.
关于这是一个单字节指令的好处是它可以很容易地插入到程序中的任何地方.从概念上讲,这很简单,但实际工作方式有点棘手.基本上,有两种选择:
如果它是一个固定的断点,那么调试器可以INT在编译时将该指令插入代码中.然后,每当你点击那一点,它就会执行该指令并中断.
在C/C++,固定断点可以经由到一个呼叫被插入的DebugBreakAPI函数,用该__debugbreak固有,或使用内联组件插入一个INT 3指令.在.NET代码中,您将使用System.Diagnostics.Debugger.Break发出固定断点.
在运行时,一个固定的断点可容易地通过更换一个字节删除INT指令(0xCC用一个字节)NOP指令(0x90).NOP是无操作的助记符:它只会导致处理器浪费一个循环而不做任何事情.
但如果它是一个动态断点,那么事情变得更加复杂.调试器必须修改内存中的二进制文件并插入INT指令.但它会在哪里插入?即使在调试版本中,编译器也无法合理地NOP在每条指令之间插入一个,并且它不会事先知道您可能想要插入断点的位置,因此即使是单字节INT指令也不会有空格.代码中的任意位置.
所以它的作用是将INT指令(0xCC)插入请求的位置,写入当前的任何指令.如果这是一个单字节指令(例如一个INC),那么它只是被一个替换INT.如果这是一个多字节指令(大多数是),那么只有该指令的第一个字节被替换0xCC.然后原始指令变为无效,因为它已被部分覆盖.但这没关系,因为一旦处理器点击INT指令,它就会陷阱并在这一点上停止执行.部分的,损坏的原始指令不会被命中.一旦调试器捕获由INT指令触发的陷阱并"中断",它就撤消内存中的修改,用0xCC原始指令的正确字节表示替换插入的字节.这样,当您从该点恢复执行时,代码是正确的,并且您不会反复敲击相同的断点.注意,所有这些修改都发生在存储在存储器中的二进制可执行文件的当前图像中; 它直接在内存中修补,无需修改磁盘上的文件.(这是通过使用完成ReadProcessMemory和WriteProcessMemoryAPI函数,专门为调试器设计).
这是机器代码,显示原始字节以及汇编语言助记符:
31 C0             xor  eax, eax     ; clear EAX register to 0
BA 02 00 00 00    mov  edx, 2       ; set EDX register to 2
01 D0             add  eax, edx     ; add EDX to EAX
C3                ret               ; return, with result in EAX
如果我们在添加值(ADD反汇编中的ADD指令0x01)的源代码行上设置断点,则指令()的第一个字节将被替换为0xCC,将剩余的字节留作无意义的垃圾:
31 C0             xor  eax, eax     ; clear EAX register to 0
BA 02 00 00 00    mov  edx, 2       ; set EDX register to 2
CC                int  3            ; BREAKPOINT!
D0                ???               ; meaningless garbage, never executed
C3                ret               ; also meaningless garbage from CPU's perspective
希望你能够遵循所有这些,因为这实际上是最简单的情况.软件断点是您大多数时间使用的.调试器的许多最常用功能都是使用软件断点实现的,包括单步执行调用,执行到特定点的所有代码,以及运行到函数末尾.在幕后,所有这些都使用临时软件断点,在第一次被击中时自动删除.
但是,在处理器的直接帮助下,有一种更复杂,更强大的方法来设置断点.这些被称为硬件断点.x86指令集提供6个特殊调试寄存器.(它们被称为DB0通过DB7,提示共8,但DR4和DR5是相同的DR6和DR7,所以有实际上只有6)第4个调试寄存器(DR0通过DR3)或者存储的存储器地址或I/O地址,其值可以使用特殊形式的MOV指令设置.DR6(相当于DR4)是一个包含标志的状态寄存器,DR7(相当于DR5)是一个控制寄存器.当相应地设置控制寄存器时,处理器尝试访问这四个位置之一将导致硬件断点(具体地,INT 1将引发中断),然后可以由调试器捕获.同样,细节很复杂,可以在网上或英特尔的技术手册中找到各种地方,但不一定是为了获得高层次的理解.
这些特殊调试寄存器的优点是它们提供了一种实现数据断点的方法,而无需修改代码!但是,有两个严重的限制.首先,只有四个可能的位置,所以没有很多聪明,你只能有四个断点.其次,调试寄存器是特权资源,访问和操作它们的指令只能在环0(本质上是内核模式)下执行.尝试在任何其他权限级别(例如在环3中,实际上是用户模式)读取或写入这些寄存器将导致一般性保护错误.因此,Visual Studio调试器必须跳过一些箍来使用它们.我认为,第一线程挂起,然后调用的SetThreadContextAPI函数(这将导致一个开关在内部内核模式)来操纵寄存器的内容.最后,它恢复了线程.这些调试寄存器非常强大,可用于为包含数据的内存位置设置读/写断点,以及为包含代码的内存位置设置执行断点.
但是,如果您需要超过4个,或者遇到其他限制,那么这些硬件提供的调试寄存器将无法工作.Visual Studio调试器必须具有一些其他更通用的方法来实现数据断点.事实上,这就是为什么在调试器下运行时,拥有大量断点可以真正减慢程序的执行速度.
这里有各种技巧,而且我对不同的闭源调试器确切使用哪些技巧知之甚少.你几乎可以肯定通过逆向工程或甚至更近的观察来发现,也许有人比我更了解这一点.但我将简要总结一下我所知道的几个技巧:
内存访问断点的一个技巧是使用保护页面.这涉及更改包含感兴趣数据的虚拟内存页面的保护级别PAGE_GUARD,这意味着后续尝试访问该页面(读取或写入)将引发防护页面违例异常.然后,调试器可以捕获此异常,验证它是否在访问感兴趣的内存地址时发生,并将其作为断点进行处理.然后,当您恢复执行时,调试器会安排页面访问成功,PAGE_GUARD再次重置标志,然后继续.这就是OllyDBG如何实现对内存访问断点的支持.我不知道Visual Studio的调试器是否使用了这个技巧.
另一个技巧是使用单步支持.基本上,调试器TF在x86 EFLAGS寄存器中设置Trap Flag().这会导致CPU在执行每条指令之前陷阱(通过引发INT 1异常,就像我们在使用调试寄存器时看到的那样).然后调试器捕获此陷阱,并决定是否应继续执行.
最后,还有条件断点.这是您可以在一行代码上设置断点的地方,但是如果某个指定条件的计算结果为true,则要求调试器仅在那里中断.这些功能非常强大,但根据我的经验,开发人员很少使用它们.据我所知,这些是在正常的无条件断点下实现的.当命中断点时,调试器会自动评估条件.如果是真的,它会为用户"中断".如果为false,则继续执行,就好像断点从未被击中一样.条件断点没有硬件支持(超出上面讨论的数据断点支持),我不知道对条件断点的任何低级支持(例如,操作系统提供的东西).当然,这就是为什么在断点上附加复杂条件会显着降低程序执行速度的原因!
如果您对更多细节感兴趣(好像这个答案还不够长!),您可以查看Tarik Soulami的Inside Windows Debugging.看起来它包含相关信息,虽然我还没有读过它所以我不能毫不掩饰地推荐它.(这是我的亚马逊愿望清单!)