gpt4 book ai didi

visual-studio - 如何在Visual Studio中添加运行时断点?

转载 作者:行者123 更新时间:2023-12-04 16:58:31 24 4
gpt4 key购买 nike

当我在运行时向某些C#代码添加断点时,它会受到打击。这实际上是如何发生的?

我想说的是,在 Debug模式下运行时,Visual Studio具有代码块的引用,并且在运行时添加断点时,一旦在编译后的代码中调用了该引用,便会激活该断点。

这是正确的假设吗?如果是这样,您能否提供有关其工作原理的更多详细信息?

最佳答案

这实际上是一个相当大且复杂的主题,并且也是特定于体系结构的,因此,我仅以此答案为目标,以概述Intel(及兼容)x86微体系结构的常用方法。

好消息是,它与语言无关,因此无论调试VB.NET,C#还是C++代码,调试器都将以相同的方式工作。之所以如此,是因为所有代码最终都将最终进行编译(无论是静态的(例如,像C++那样的提前进行,还是使用诸如.NET的JIT编译器))还是动态的(例如,通过运行时解释器进行)编译。 )指向可以由处理器 native 执行的目标代码。调试器最终将使用此本地代码。

此外,这不仅限于Visual Studio。它的调试器肯定会按照我将描述的方式工作,但是其他Windows调试器也是如此,例如Debugging Tools for Windows调试器(WinDbg,KD,CDB,NTSD等),GNU's GDBIDA's debugger,开源x64dbg等。上。

让我们从一个简单的定义开始-什么是断点?它只是一种允许暂停执行的机制,因此您可以进行进一步的分析,无论是检查调用堆栈,打印变量的值,修改内存或寄存器的内容,甚至修改代码本身。

在x86架构上,有几种实现断点的基本方法。它们可以分为软件断点和硬件断点两大类。

尽管软件断点使用处理器本身的功能,但它主要是在软件中实现的,因此命名为软件。具体来说,中断#3(the assembly language instruction INT 3 )提供了一个断点中断。可以将其放置在可执行代码中的任何位置,并且当CPU在执行过程中点击此指令时,它将被捕获。然后,调试器可以捕获此陷阱并执行其想做的任何事情。如果程序未在调试器下运行,则操作系统将处理陷阱;否则,操作系统将处理陷阱。操作系统的默认处理程序将仅终止程序。
INT 3指令有两种可能的编码。也许最合乎逻辑的编码是0xCD 0x03,其中0xCD表示INT,而0x03指定“参数”或要触发的中断号。但是,由于断点非常重要,因此英特尔的设计人员还为INT 3(单字节操作码0xCC)添加了一种特殊情况的表示形式。

关于这是一个单字节指令的好处是,它可以很容易地插入到程序中的几乎任何位置。从概念上讲,这很简单,但是它的实际工作方式有些棘手。基本上有两种选择:

  • 如果它是固定的断点,则调试器可以在编译时将此INT指令插入代码中。然后,每当您达到该点时,它将执行该指令并中断。

    在C/C++中,可以通过使用the DebugBreak API function调用the __debugbreak intrinsic或使用内联汇编插入INT 3指令来插入固定的断点。在.NET代码中,您将使用 System.Diagnostics.Debugger.Break 发出固定的断点。

    在运行时,可以通过将一个字节的INT指令(0xCC)替换为一个字节的 NOP instruction(0x90)轻松删除固定的断点。 NOP是不操作的助记符:它只会导致处理器浪费一个周期而无所事事。
  • 但是,如果这是一个动态断点,那么事情会变得更加复杂。调试器必须修改二进制内存并插入INT指令。但是要在哪里插入呢?即使在调试版本中,编译器也无法在每条指令之间合理地插入NOP,并且它事先也不知道您可能要在何处插入断点,因此即使是一个字节的INT也不会有空间插入指令在代码中的任意位置。

    因此,它要做的是在请求的位置插入INT指令(0xCC),覆盖当前存在的任何指令。如果这是一个单字节的指令(例如INC),则只需将其替换为INT即可。如果这是一个多字节指令(其中大多数是),那么只有该指令的第一个字节才被0xCC替换。然后,原始指令将无效,因为它已被部分覆盖。但这没关系,因为一旦处理器点击了INT指令,它就会在该点捕获并停止执行。部分,已损坏的原始指令将不会被点击。一旦调试器捕获到由INT指令触发的陷阱并“闯入”,它将撤消内存中的修改,将插入的0xCC字节替换为原始指令的正确字节表示。这样,当您从该点恢复执行时,代码是正确的,并且不会一遍又一遍地碰到相同的断点。请注意,所有这些修改都发生在存储在内存中的二进制可执行文件的当前镜像中。它直接在内存中打补丁,而无需修改磁盘上的文件。 (这是使用 ReadProcessMemory WriteProcessMemory API函数专门为调​​试器设计的。)

    它在机器代码中,显示原始字节和汇编语言助记符:
    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个,但是 DR4DR5DR6DR7相同,因此实际上只有6个。)前4个调试寄存器( DR0通过 DR3)存储一个存储器地址或I/O位置,可以使用特殊形式的 MOV指令设置其值。 DR6(相当于 DR4)是一个包含标志的状态寄存器, DR7(相当于 DR5)是一个控制寄存器。相应地设置控制寄存器后,处理器尝试访问这四个位置之一将导致硬件断点(特别是会引发 INT 1中断),然后可以由调试器捕获该断点。同样,细节很复杂,可以在网上或 Intel's technical manuals的各个地方找到,但并不需要获得高水平的了解。

    这些特殊的调试寄存器的好处在于,它们提供了一种无需修改代码即可实现数据断点的方法!但是,有两个严重的局限性。首先,只有四个可能的位置,因此没有很多技巧,您只能使用四个断点。其次,调试寄存器是特权资源,访问和操作它们的指令只能在环0(本质上是内核模式)上执行。尝试在任何其他 privilege level上读取或写入这些寄存器(例如在环3中,这实际上是用户模式)将导致一般的保护错误。因此,Visual Studio调试器必须跳过一些箍才能使用它们。我相信它首先会挂起线程,然后调用 the SetThreadContext API function(这会在内部导致切换到内核模式)来操纵寄存器的内容。最后,它恢复线程。这些调试寄存器非常强大,可以为包含数据的内存位置设置读/写断点,以及为包含代码的内存位置设置执行断点。

    但是,如果您需要超过4个或遇到其他限制,则这些硬件提供的调试寄存器将无法工作。 Visual Studio调试器必须具有其他一些更通用的方法来实现数据断点。实际上,这就是为什么在调试器下运行时,拥有大量断点确实会减慢程序的执行速度的原因。

    这里有各种各样的技巧,而对于不同的闭源调试器到底使用了哪些技巧,我知之甚少。您几乎可以肯定地通过反向工程或什至更仔细的观察发现了这一点,也许有人比我更了解这一点。但是,我将简要总结一些我知道的技巧:
  • 内存访问断点的一个技巧是使用guard pages。这涉及将包含感兴趣数据的虚拟内存页面的保护级别更改为PAGE_GUARD,这意味着后续访问该页面(读取或写入)的尝试将引发保护页面违反异常。然后,调试器可以捕获此异常,验证其是否在访问目标内存地址时发生,并将其作为断点进行处理。然后,当您恢复执行时,调试器安排页面访问成功,再次重置PAGE_GUARD标志,然后继续。这就是OllyDBG实现其对内存访问断点的支持的方式。我不知道Visual Studio的调试器是否使用此技巧。
  • 另一个技巧是使用单步支持。基本上,调试器在x86 TF寄存器中设置陷阱标志(EFLAGS)。这会导致CPU在执行每条指令之前进行陷阱(这是通过引发INT 1异常来完成的,就像我们在上面使用调试寄存器时所看到的那样)。然后,调试器捕获此陷阱,并决定是否应继续执行。


  • 最后,有条件断点。您可以在此处在一行代码上设置一个断点,但仅当某个指定条件的评估结果为true时,才要求调试器在此断点。这些功能非常强大,但是以我的经验,开发人员很少使用它们。据我所知,这些是在幕后作为正常的无条件断点实现的。当达到断点时,调试器将自动评估条件。如果为真,则为用户“闯入”。如果为假,它将继续执行,就好像从未命中过断点一样。对条件断点没有硬件支持(除了上面讨论的数据断点支持之外),我不知道对条件断点有任何较低级别的支持(例如,操作系统提供的支持)。当然,这就是为什么将复杂的条件附加到断点会大大降低程序的执行速度的原因!

    如果您对更多详细信息感兴趣(好像这个答案还不够长!),可以检查 Tarik Soulami's Inside Windows Debugging。它似乎包含相关信息,尽管我还没有阅读过,所以我不会毫不犹豫地推荐它。 (在我的亚马逊愿望 list 上!)

    关于visual-studio - 如何在Visual Studio中添加运行时断点?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/41135561/

    24 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com