调试器原理_调试器的工作原理

调试器原理

调试器是大多数(如果不是每种)开发人员在软件工程生涯中至少使用一次的软件之一,但是你们当中有多少人知道它们的实际工作原理? 在悉尼举行的linux.conf.au 2018上的演讲中,我将谈论从头开始编写调试器...在Rust中

在本文中,术语调试器/跟踪器是可互换的。 “跟踪”是指跟踪程序正在跟踪的进程。

ptrace系统调用

大多数调试器严重依赖于称为ptrace(2)的系统调用,该系统调用具有以下原型:



   
   
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);

这是一个系统调用,可以处理流程的几乎所有方面。 但是,在调试器可以附加到进程之前,“ tracee”必须使用请求PTRACE_TRACEME调用ptrace 。 这告诉Linux,父进程通过ptrace附加到此进程是合法的。 但是...我们如何强迫一个进程调用ptrace ? 十分简单! fork/execve提供了一种在fork之后但在tracee真正开始使用execve之前调用ptrace的简便方法。 方便地, fork也将返回tracee的pid ,这是以后使用ptrace所必需的。

现在,调试器可以跟踪该跟踪,进行了重要的更改:

  • 每次将信号传递到跟踪时,它都会停止,并且将等待事件传递到跟踪器,该事件可以由wait的系统调用系列捕获。
  • 每个execve系统调用都将导致SIGTRAP被传递给Tracee。 (与之前的项目相结合,这意味着之前的tracee停止execve可以充分利用的地方。)

这意味着,一旦我们发出PTRACE_TRACEME请求并调用execve系统调用以在跟踪中实际启动程序,由于execve传递了SIGTRAP ,并且跟踪器中的等待事件捕获了该跟踪,所以该跟踪将立即停止。 我们如何继续? 正如人们所期望的那样, ptrace有许多请求可用于告诉该跟踪继续进行下去:

  • PTRACE_CONT :这是最简单的。 跟踪将一直运行,直到接收到信号为止,这时将等待事件传递到跟踪器。 这最常用于实现实际调试器的“ continue-until-breakpoint”和“ continue-forever”选项。 断点将在下面介绍。
  • PTRACE_SYSCALL :非常相似PTRACE_CONT ,而是进入了一个系统调用之前停止,之前也一个系统调用返回到用户空间。 它可以与其他请求(我们将在本文后面介绍)结合使用,以监视和修改系统调用的参数或返回值。 strace (系统调用跟踪程序)大量使用此请求来确定进程进行了哪些系统调用。
  • PTRACE_SINGLESTEP :这是不言自明的。 如果您之前使用过调试器,则此请求将执行下一条指令,但之后立即停止。

我们可以通过各种请求来停止该过程,但是如何获取示踪的状态呢? 进程的状态主要由其寄存器捕获,因此ptrace当然有一个获取(或修改!)寄存器的请求:

  • PTRACE_GETREGS :该请求将给出停止跟踪时的寄存器状态。
  • PTRACE_SETREGS :如果跟踪器具有上一次调用PTRACE_GETREGS的寄存器值,则可以通过该请求修改该结构中的值,并将寄存器设置为新值。
  • PTRACE_PEEKUSERPTRACE_POKEUSER :这些允许从跟踪的USER区域读取,该区域保存寄存器和其他有用信息。 这可用于修改单个寄存器,而无需更重的PTRACE_{GET,SET}REGS

在调试器中修改寄存器并不总是足够的。 调试器有时需要读取存储器的某些部分,甚至对其进行修改。 GNU项目调试器(GDB)可以使用print获取内存位置或变量的值。 ptrace具有实现此功能的功能:

  • PTRACE_PEEKTEXTPTRACE_POKETEXT :这些允许在跟踪的地址空间中读取和写入单词。 当然,必须停止示踪才能起作用。

现实世界中的调试器还具有断点和观察点之类的功能。 在下一节中,我将深入介绍调试支持的体系结构细节。 为了清楚和简洁起见,本文仅考虑x86。

建筑支持

ptrace很酷,但是它如何工作? 在上一节中,我们已经看到了ptrace有相当多的做信号: SIGTRAP可以单步执行前交付, execve和之前或之后的系统调用。 信号可以通过多种方式生成,但是我们将看看两个特定的示例,调试器可以使用这些示例在给定位置停止程序(有效地创建断点!):

  • 未定义的指令:当进程尝试执行未定义的指令时,CPU会引发异常。 通过CPU中断处理此异常,然后调用与内核中的中断对应的处理程序。 这将导致SIGILL发送到该进程。 反过来,这导致进程停止,并通过等待事件通知跟踪程序。 然后,它可以决定要做什么。 在x86上,保证ud2指令ud2是未定义的。

  • 调试中断:前一种方法的问题是ud2指令占用了两个字节的机器代码。 存在一条占用一个字节并引发中断的特殊指令。 它是int $3 ,机器代码是0xCC 。 引发此中断时,内核将SIGTRAP发送到进程,并且像以前一样,通知跟踪器。

很好,但是我们如何强迫示踪执行这些指令? 容易: ptrace具有PTRACE_POKETEXT ,可以覆盖存储位置的单词。 调试器将使用PTRACE_PEEKTEXT读取该位置处的原始单词,并将其替换为0xCC ,从而记住原始字节以及该字节处于其内部状态的断点这一事实。 下次在该位置执行跟踪时,将借助SIGTRAP自动将其停止。 然后,调试器的最终用户可以决定如何继续(例如,检查寄存器)。

好的,我们已经介绍了断点,但是观察点呢? 当读取或写入某个内存位置时,调试器如何停止程序? 当然,您不会仅仅用int $3覆盖可以读取或写入某些内存位置的每条指令。 满足调试寄存器,一组旨在更有效地实现此目标的寄存器:

  • DR0DR3 :每个寄存器都包含一个地址(存储位置),调试器出于某种原因希望该跟踪停止。 原因在DR7指定为位掩码。
  • DR4DR5 :分别是DR6DR7别名。
  • DR6 :调试状态。 包含有关哪个DR0DR3引发调试异常的信息。 Linux使用它来确定与SIGTRAP一起传递给跟踪的信息。
  • DR7 :调试控件。 使用这些寄存器中的位,调试器可以控制如何解释DR0 DR3中指定的地址。 位掩码控制观察点的大小(监视的是1、2、4还是8个字节),以及是否在执行,读取,写入或读取和写入时引发异常。

因为调试寄存器构成了进程USER区域的一部分,所以调试器可以使用PTRACE_POKEUSER将值写入调试寄存器。 调试寄存器仅与特定进程相关,因此在进程重新获得对CPU的控制之前,它们会抢占先恢复为该值。

冰山一角

我们看了一眼调试器的冰山一角:我们介绍了ptrace ,介绍了它的一些功能,然后看了如何实现ptraceptrace某些部分可以用软件实现,而其他部分则必须用硬件实现,否则它们将非常昂贵,甚至不可能。

当然,我们没有涉及很多。 出现诸如“调试器如何知道变量在内存中的位置?”之类的问题。 由于时间和空间的限制,请保持开放状态,但我希望您从本文中学到了一些知识; 如果它激起了您的兴趣,可以在网上找到大量资源以了解更多信息。


有关更多信息,请参加Levente Kurusa的演讲, 让我们编写调试器! 网址linux.conf.au ,将于1月22日至26日在悉尼举行。

翻译自: https://opensource.com/article/18/1/how-debuggers-really-work

调试器原理

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值