【问题标题】:How does the processor know the breakpoints?处理器如何知道断点?
【发布时间】:2017-10-17 12:30:43
【问题描述】:

让我们考虑这个非常简单的程序:

#include<stdio.h>

int main () 
{
    int num1=4, num2=5;
    printf("Welcome\n");
    printf("num1 + num2 = %d\n", num1+num2);
    return 0;
}

使用gcc -S prog.c查看生成的汇编代码时:

    .file   "p.c"
    .def    ___main;    .scl    2;  .type   32; .endef
    .section .rdata,"dr"
LC0:
    .ascii "Welcome\0"
LC1:
    .ascii "num1 + num2 = %d\12\0"
    .text
    .globl  _main
    .def    _main;  .scl    2;  .type   32; .endef
_main:
LFB10:
    .cfi_startproc
    pushl   %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    andl    $-16, %esp
    subl    $32, %esp
    call    ___main
    movl    $4, 28(%esp)
    movl    $5, 24(%esp)
    movl    $LC0, (%esp)
    call    _puts
    movl    28(%esp), %edx
    movl    24(%esp), %eax
    addl    %edx, %eax
    movl    %eax, 4(%esp)
    movl    $LC1, (%esp)
    call    _printf
    call    _getchar
    movl    $0, %eax
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
LFE10:
    .ident  "GCC: (GNU) 5.3.0"
    .def    _puts;  .scl    2;  .type   32; .endef
    .def    _printf;    .scl    2;  .type   32; .endef
    .def    _getchar;   .scl    2;  .type   32; .endef

我知道 CPU 看到的是编译器为其生成的汇编代码,我不明白程序如何在用户设置的 breakpoint 处停止?为什么CPU不继续运行程序?这是怎么回事? 我的意思是,为什么它在获取指令后会停止?

我对此有点困惑,是 Code::Blocks 关心这个还是用户正在使用的任何程序?

提前致谢!

【问题讨论】:

  • 调试器会处理这个问题。它要么使用软件断点指令 (int3) 覆盖您的程序,要么设置硬件辅助断点。
  • CPU 怎么知道addl 是加法?如何处理ret指令?
  • @Olaf 他在生成的代码中看不到导致程序停止的命令。这与他对addl或ret的理解无关,我想。
  • @TonyTannous:如果您知道 RAM 中的断点是如何使用的,那么您就知道了。
  • 答案因处理器而异。

标签: c debugging exception assembly operating-system


【解决方案1】:

大多数现代指令集都包含一个breakpoint 异常,用于允许调试器通过将相关程序指令临时替换为特殊的软件中断指令来在程序代码中插入断点。在 x86/x86-64 ISA 上,这条指令是“中断向量 3”(又名 int3),通常作为单字节指令 0xcc 发出。

关于断点指令需要注意的重要一点是,它们通常必须至少与 ISA 上可能的最小指令一样小。这有几个原因。一些 ISA 要求指令的最小对齐;较短的指令通常会有不太严格的对齐要求。此外,用更长的指令替换某些指令意味着您可能会覆盖后面的指令。这在单线程应用程序中可能没什么大不了的,但在多线程应用程序中,它是一个阻碍。例如,考虑一下,如果将可选分支末尾的短指令替换为更长的指令,而另一个正在运行的线程跳过该分支会发生什么情况。

在其他情况下,可能不存在这样的特殊指令。在缺乏特定断点指令的硬件平台上,有时会提供特殊的硬件寄存器,以使处理器在尝试访问内存中的特定位置时陷入陷阱。这些寄存器的数量通常相当有限,因此在使用大量断点进行调试时,专用断点指令非常有用。

当您在调试器中启动程序并添加启用软件的断点时,通常会发生以下情况:

调试器将程序加载到内存中并为您提供一些输入提示。你告诉调试器添加一个断点。它可能会使用一些信息来确定您的断点在内存中的哪个位置实际上对应于程序的内存中表示。然后调试器解码该地址处的指令(因为它通常想要替换整个指令)并替换 它(在内存中)与断点指令。然后你告诉调试器执行/继续执行程序。

当处理器遇到这条指令时,它会产生一个陷阱。该陷阱作为中断传递给操作系统,操作系统会注意到该陷阱旨在调试程序。操作系统知道正在执行哪个程序(因此也知道谁在执行它)——因此它可能会进行一些权限检查,以确保此时实际允许调试应用程序的用户这样做。如果一切正常,操作系统会通知调试器遇到了断点,并告诉您它已停止。

这不是一个普遍的解释。要使上述情况属实,需要大量的操作系统支持。在 Linux 和 BSD 上,大部分功能通过 ptrace(2) 系统调用(它允许读取和替换指令,以及单步执行指令)公开。虽然符合 POSIX,但 OS X 并没有实现 ptrace(2),而是为此提供了各种 Mach 端口。 Windows 完全不同。

在嵌入式系统上,可能会提供特殊的硬件端口(如JTAG)以允许在硬件级别进行自省,从而允许开发外部调试器,该调试器使用 JTAG 直接与硬件“对话”。

【讨论】:

  • ??现代调试器使用内部硬件断点寄存器。由于内部流水线问题和多线程问题,软件断点不再有用。
  • 硬件查找地址与用断点指令替换内存的管道问题相同。老式的调试是……老式的……与过去相比,您将更多地干扰您不需要干扰调试的事情,由于断点或单步执行停止/更改代码流,等等你正在改变行为,有时这无关紧要,有时这取决于错误......
  • 替换内存中的指令是一种解决方案,在 cpu 或芯片中放置一些东西来监视地址或指令或某种机制并在不替换指令的情况下停止内核是另一种解决方案。替换指令解决方案允许几乎无限数量的断点,内部片上调试器解决方案通常仅限于在任何时候使用一些相对较少的断点。
  • @thingy 这句话显然是错误的;如果设置了超过 4 个断点,调试器会做什么?使软件断点“不再有用”的“内部流水线问题”是什么?保证不管流水线和乱序执行,指令总是按顺序退出,因此看起来好像它们是连续执行的。另外,除了调试多线程代码所固有的问题之外,还有什么“多线程问题”? 可能使用硬件断点寄存器,但软件断点仍然可行且司空见惯。
  • @TonyTannous 值得注意的是int 3 可能是0xcd03 或0xcc,以及为什么调试器会使用0xcc。特别是,断点指令的大小必须int 3 特定于 x86/x86-64 ISA。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多