处理器如何知道断点?

Jam*_*234 9 c debugging assembly operating-system exception

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

#include<stdio.h>

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

使用以下方法查看生成的汇编代码时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
Run Code Online (Sandbox Code Playgroud)

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

我有点困惑,是Code :: Blocks照顾这个或用户正在使用的程序吗?

提前致谢!

Ton*_*ous 11

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

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

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

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

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

当处理器遇到此指令时,它会生成陷阱.此陷阱作为中断传递给操作系统,该操作系统注意到陷阱用于调试程序.操作系统知道正在执行哪个程序(因此也正在执行它) - 因此它可以进行一些权限检查,以确保此时实际上允许用户调试应用程序.如果一切看起来都很好,操作系统会通知调试器遇到断点,并告诉您它已停止.

这不是一个普遍的解释.要实现上述要求,需要大量的OS支持.在Linux和BSD上,大多数此功能都是通过ptrace(2)syscall 公开的(它允许读取和替换指令,以及单步执行).虽然符合POSIX标准,但OS X并未实现ptrace(2),而是为此提供各种Mach端口.Windows完全有其他功能.

在嵌入式系统上,可能会提供特殊硬件端口(如JTAG)以允许在硬件级别进行内省,从而允许开发外部调试器,该调试器使用JTAG直接"说话"到硬件.

  • 替换内存中的指令是一种解决方案,在cpu或芯片中有一些东西在监视地址或指令或某种机制并停止核心而没有替换指令是另一种解决方案.替换指令解决方案允许接近无限数量的断点,内部片上调试器解决方案通常限于任何一次相对较少数量的断点. (4认同)
  • 寻找地址的硬件与使用相对于管道的断点指令替换存储器具有相同的问题.老式的调试是......过时的...比过去更多的是你要干扰你不需要干扰调试,停止/改变由于断点或单步执行的代码流的事情,等等你正在改变这种行为,有时这无关紧要,有时也会这样,取决于错误...... (2认同)
  • @thingy那句话显然是假的; 如果设置了4个以上的断点,调试器会做什么?什么是"内部流水线问题",使软件断点"不再有用"?可以保证,无论流水线操作和乱序执行,指令总是按顺序退役,因此看起来好像是串行执行的.此外,除了调试多线程代码固有的问题之外,还有哪些"多线程问题"?可能会使用硬件断点寄存器*,但软件断点仍然可行且常见. (2认同)
  • @TonyTannous可能值得注意的是`int 3`可能是`0xcd03`或`0xcc`,以及为什么调试器会使用`0xcc`代替.特别是,断点指令的大小必须<=处理器支持的最小指令,以及为什么必须为真.可能还值得注意的是,"int 3"特定于x86/x86-64 ISA. (2认同)