---
author: Levente Kurusa
category: 软件开发
comments_data: []
count:
  commentnum: 0
  favtimes: 0
  likes: 0
  sharetimes: 0
  viewnum: 5471
date: '2018-07-19 08:57:00'
editorchoice: false
excerpt: 你也许用过调速器检查过你的代码，但你知道它们是如何做到的吗？
fromurl: https://opensource.com/article/18/1/how-debuggers-really-work
id: 9849
islctt: true
largepic: /images/p/201807/18/230247sopnnhcu2uepgnae.jpg
permalink: /article-9849-1.html
pic: /images/p/201807/18/230247sopnnhcu2uepgnae.jpg.thumb.jpg
related: []
reviewer: ''
selector: ''
summary: 你也许用过调速器检查过你的代码，但你知道它们是如何做到的吗？
tags:
- 调试
- ptrace
thumb: false
title: 调试器到底怎样工作
titlepic: true
translator: stephenxs
updated: '2018-07-19 08:57:00'
---


> 
> 你也许用过调速器检查过你的代码，但你知道它们是如何做到的吗？
> 
> 
> 


![](/images/p/201807/18/230247sopnnhcu2uepgnae.jpg)


调试器是大多数（即使不是每个）开发人员在软件工程职业生涯中至少使用过一次的那些软件之一，但是你们中有多少人知道它们到底是如何工作的？我在悉尼 [linux.conf.au 2018](https://linux.conf.au/index.html) 的演讲中，将讨论从头开始编写调试器……使用 [Rust](https://www.rust-lang.org)！


在本文中，术语<ruby> 调试器 <rt>  debugger </rt></ruby>和<ruby> 跟踪器 <rt>  tracer </rt></ruby>可以互换。 “<ruby> 被跟踪者 <rt>  Tracee </rt></ruby>”是指正在被跟踪器跟踪的进程。


### ptrace 系统调用


大多数调试器严重依赖称为 `ptrace(2)` 的系统调用，其原型如下：



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

```

这是一个可以操纵进程几乎所有方面的系统调用；但是，在调试器可以连接到一个进程之前，“被跟踪者”必须以请求 `PTRACE_TRACEME` 调用 `ptrace`。这告诉 Linux，父进程通过 `ptrace` 连接到这个进程是合法的。但是……我们如何强制一个进程调用 `ptrace`？很简单！`fork/execve` 提供了在 `fork` 之后但在被跟踪者真正开始使用 `execve` 之前调用 `ptrace` 的简单方法。很方便地，`fork` 还会返回被跟踪者的 `pid`，这是后面使用 `ptrace` 所必需的。


现在被跟踪者可以被调试器追踪，重要的变化发生了：


* 每当一个信号被传送到被跟踪者时，它就会停止，并且一个可以被 `wait` 系列的系统调用捕获的等待事件被传送给跟踪器。
* 每个 `execve` 系统调用都会导致 `SIGTRAP` 被传递给被跟踪者。（与之前的项目相结合，这意味着被跟踪者在一个 `execve` 完全发生之前停止。）


这意味着，一旦我们发出 `PTRACE_TRACEME` 请求并调用 `execve` 系统调用来实际在被跟踪者（进程上下文）中启动程序时，被跟踪者将立即停止，因为 `execve` 会传递一个 `SIGTRAP`，并且会被跟踪器中的等待事件捕获。我们如何继续？正如人们所期望的那样，`ptrace` 有大量的请求可以用来告诉被跟踪者可以继续：


* `PTRACE_CONT`：这是最简单的。 被跟踪者运行，直到它接收到一个信号，此时等待事件被传递给跟踪器。这是最常见的实现真实世界调试器的“继续直至断点”和“永远继续”选项的方式。断点将在下面介绍。
* `PTRACE_SYSCALL`：与 `PTRACE_CONT` 非常相似，但在进入系统调用之前以及在系统调用返回到用户空间之前停止。它可以与其他请求（我们将在本文后面介绍）结合使用来监视和修改系统调用的参数或返回值。系统调用追踪程序 `strace` 很大程度上使用这个请求来获知进程发起了哪些系统调用。
* `PTRACE_SINGLESTEP`：这个很好理解。如果您之前使用过调试器（你会知道），此请求会执行下一条指令，然后立即停止。


我们可以通过各种各样的请求停止进程，但我们如何获得被调试者的状态？进程的状态大多是通过其寄存器捕获的，所以当然 `ptrace` 有一个请求来获得（或修改）寄存器：


* `PTRACE_GETREGS`：这个请求将给出被跟踪者刚刚被停止时的寄存器的状态。
* `PTRACE_SETREGS`：如果跟踪器之前通过调用 `PTRACE_GETREGS` 得到了寄存器的值，它可以在参数结构中修改相应寄存器的值，并使用 `PTRACE_SETREGS` 将寄存器设为新值。
* `PTRACE_PEEKUSER` 和 `PTRACE_POKEUSER`：这些允许从被跟踪者的 `USER` 区读取信息，这里保存了寄存器和其他有用的信息。 这可以用来修改单一寄存器，而避免使用更重的 `PTRACE_{GET,SET}REGS` 请求。


在调试器仅仅修改寄存器是不够的。调试器有时需要读取一部分内存，甚至对其进行修改。GDB 可以使用 `print` 得到一个内存位置或变量的值。`ptrace` 通过下面的方法实现这个功能：


* `PTRACE_PEEKTEXT` 和 `PTRACE_POKETEXT`：这些允许读取和写入被跟踪者地址空间中的一个字。当然，使用这个功能时被跟踪者要被暂停。


真实世界的调试器也有类似断点和观察点的功能。 在接下来的部分中，我将深入体系结构对调试器支持的细节。为了清晰和简洁，本文将只考虑 x86。


### 体系结构的支持


`ptrace` 很酷，但它是如何工作？ 在前面的部分中，我们已经看到 `ptrace` 跟信号有很大关系：`SIGTRAP` 可以在单步跟踪、`execve` 之前以及系统调用前后被传送。信号可以通过一些方式产生，但我们将研究两个具体的例子，以展示信号可以被调试器用来在给定的位置停止程序（有效地创建一个断点！）：


* **未定义的指令**：当一个进程尝试执行一个未定义的指令，CPU 将产生一个异常。此异常通过 CPU 中断处理，内核中相应的中断处理程序被调用。这将导致一个 `SIGILL` 信号被发送给进程。 这依次导致进程被停止，跟踪器通过一个等待事件被通知，然后它可以决定后面做什么。在 x86 上，指令 `ud2` 被确保始终是未定义的。
* **调试中断**：前面的方法的问题是，`ud2` 指令需要占用两个字节的机器码。存在一条特殊的单字节指令能够触发一个中断，它是 `int $3`，机器码是 `0xCC`。 当该中断发出时，内核向进程发送一个 `SIGTRAP`，如前所述，跟踪器被通知。


这很好，但如何我们才能胁迫被跟踪者执行这些指令？ 这很简单：利用 `ptrace` 的 `PTRACE_POKETEXT` 请求，它可以覆盖内存中的一个字。 调试器将使用 `PTRACE_PEEKTEXT` 读取该位置原来的值并替换为 `0xCC` ，然后在其内部状态中记录该处原来的值，以及它是一个断点的事实。 下次被跟踪者执行到该位置时，它将被通过 `SIGTRAP` 信号自动停止。 然后调试器的最终用户可以决定如何继续（例如，检查寄存器）。


好吧，我们已经讲过了断点，那观察点呢？ 当一个特定的内存位置被读或写，调试器如何停止程序？ 当然你不可能为了能够读或写内存而去把每一个指令都覆盖为 `int $3`。有一组调试寄存器为了更有效的满足这个目的而被设计出来：


* `DR0` 到 `DR3`：这些寄存器中的每个都包含一个地址（内存位置），调试器因为某种原因希望被跟踪者在那些地址那里停止。 其原因以掩码方式被设定在 `DR7` 寄存器中。
* `DR4` 和 `DR5`：这些分别是 `DR6` 和 `DR7` 过时的别名。
* `DR6`：调试状态。包含有关 `DR0` 到 `DR3` 中的哪个寄存器导致调试异常被引发的信息。这被 Linux 用来计算与 `SIGTRAP` 信号一起传递给被跟踪者的信息。
* `DR7`：调试控制。通过使用这些寄存器中的位，调试器可以控制如何解释 `DR0` 至 `DR3` 中指定的地址。位掩码控制监视点的尺寸（监视1、2、4 或 8 个字节）以及是否在执行、读取、写入时引发异常，或在读取或写入时引发异常。


由于调试寄存器是进程的 `USER` 区域的一部分，调试器可以使用 `PTRACE_POKEUSER` 将值写入调试寄存器。调试寄存器只与特定进程相关，因此在进程抢占并重新获得 CPU 控制权之前，调试寄存器会被恢复。


### 冰山一角


我们已经浏览了一个调试器的“冰山”：我们已经介绍了 `ptrace`，了解了它的一些功能，然后我们看到了 `ptrace` 是如何实现的。 `ptrace` 的某些部分可以用软件实现，但其它部分必须用硬件来实现，否则实现代价会非常高甚至无法实现。


当然有很多我们没有涉及。例如“调试器如何知道变量在内存中的位置？”等问题由于空间和时间限制而尚未解答，但我希望你从本文中学到了一些东西；如果它激起你的兴趣，网上有足够的资源可以了解更多。


想要了解更多，请查看 [linux.conf.au](https://linux.conf.au/index.html) 中 Levente Kurusa 的演讲 [Let's Write a Debugger!](https://rego.linux.conf.au/schedule/presentation/91/)，于一月 22-26 日在悉尼举办。




---


via: <https://opensource.com/article/18/1/how-debuggers-really-work>


作者：[Levente Kurusa](https://opensource.com/users/lkurusa) 译者：[stephenxs](https://github.com/stephenxs) 校对：[wxy](https://github.com/wxy)


本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出