PRF:20110127 How debuggers work Part 2 - Breakpoints.md

@wi-cuckoo
This commit is contained in:
wxy 2017-05-29 16:05:21 +08:00
parent da147ff2f6
commit 0912df10d1

View File

@ -1,42 +1,41 @@
[调试器如何工作: Part 2 - 断点][26]
调试器工作原理(二):断点
============================================================
这是调试器如何工作系列文章的第二部分,阅读本文前,请确保你已经读过[第一部分][27]。
这是调试器工作原理系列文章的第二部分,阅读本文前,请确保你已经读过[第一部分][27]。
### 关于本文
不会去说明调试器如何打断点。断点是调试的两大利器之一,另一个是可以在调试进程的内存中检查变量值。我们在系列的第一部分已经预览过值检查啦,但是断点对我们来说依然神秘。不过本文过后,他们就不再如此了。
将会演示如何在调试器中实现断点。断点是调试的两大利器之一,另一个是可以在调试进程的内存中检查变量值。我们在系列的第一部分已经了解过值检查,但是断点对我们来说依然神秘。不过本文过后,它们就不再如此了。
### 软件中断
为了在 x86 架构机器上实现断点,软件中断(也被称作 陷阱)被派上用场。在我们深入细节之前,我想先大致解释一下中断和陷阱的概念。
为了在 x86 架构机器上实现断点,软件中断(也被称作“陷阱”)被会派上用场。在我们深入细节之前,我想先大致解释一下中断和陷阱的概念。
一个 CPU 有一条单独的执行流,一条指令接一条的执行 [[1]][19]。为了能够处理异步的事件,如 IO 和 硬件定时器CPU 使用了中断。一个硬件中断通常是一个特定的电子信号,并附加了一个特别的”响应电路”。该电路通知中断激活,并让 CPU 停止当前执行保存状态然后跳转到一个预定义的地址也就是中断处理程序的位置。当处理程序完成其工作后CPU 又从之前停止的地方重新恢复运行。
CPU 有一条单独的执行流,一条指令接一条的执行(在更高的层面看是这样的,但是在底层的细节上来说,现在的许多 CPU 都会并行执行多个指令,这其中的一些指令就不是按照原本的顺序执行的)。为了能够处理异步的事件,如 IO 和 硬件定时器CPU 使用了中断。硬件中断通常是一个特定的电子信号,并附加了一个特别的”响应电路”。该电路通知中断激活,并让 CPU 停止当前执行保存状态然后跳转到一个预定义的地址也就是中断处理程序的位置。当处理程序完成其工作后CPU 又从之前停止的地方重新恢复运行。
软件中断在规则上与硬件相似但实际操作中有些不同。CPU 支持一些特殊的指令来允许软件模拟出一个中断。当这样的一个指令被执行时CPU 像对待一个硬件中断那样 —— 停止正常的执行流,保存状态,然后跳转到一个处理程序。这种“中断”使得许多现代操作系统的惊叹设计得以高效地实现(如任务调度,虚拟内存,内存保护,调试)
软件中断在规则上与硬件相似但实际操作中有些不同。CPU 支持一些特殊的指令来允许软件模拟出一个中断。当这样的一个指令被执行时CPU 像对待一个硬件中断那样 —— 停止正常的执行流,保存状态,然后跳转到一个处理程序。这种“中断”使得许多现代 OS 的惊叹设计得以高效地实现(如任务调度,虚拟内存,内存保护,调试)
许多编程错误(如被 0 除)也被 CPU 当做中断对待,常常也叫做异常”, 这时候硬件和软件中断之间的界限就模糊了,很难说这种异常到底是硬件中断还是软件中断。但我已经偏离今天主题太远了,所以现在让我们回到断点上来。
许多编程错误(如被 0 除)也被 CPU 当做中断对待,常常也叫做异常”, 这时候硬件和软件中断之间的界限就模糊了,很难说这种异常到底是硬件中断还是软件中断。但我已经偏离今天主题太远了,所以现在让我们回到断点上来。
### int 3 理论
前面说了很多,现在简单来说断点就是一个部署在 CPU 上的特殊中断,叫 int 3。int 是一个 “中断指令”的 x86 术语该指令是对一个预定义中断处理的调用。x86 支持 8 位的 int 指令操作数,决定了中断的数量,所以理论上可以支持 256 个中断。前 32 个中断为 CPU 自己保留,而 int 3 就是本文关注的 —— 它被叫做 “调试器专用中断”。
前面说了很多,现在简单来说断点就是一个部署在 CPU 上的特殊中断,叫 `int 3``int` 是一个 “中断指令”的 x86 术语该指令是对一个预定义中断处理的调用。x86 支持 8 位的 int 指令操作数,决定了中断的数量,所以理论上可以支持 256 个中断。前 32 个中断为 CPU 自己保留,而 int 3 就是本文关注的 —— 它被叫做 “调试器专用中断”。
避免更深的解释,我将引用“圣经”里一段话[[2]][20]
避免更深的解释,我将引用“圣经”里一段话(这里说的“圣经”,当然指的是英特尔的体系结构软件开发者手册, 卷 2A
> The INT 3 instruction generates a special one byte opcode (CC) that is intended for calling the debug exception handler. (This one byte form is valuable because it can be used to replace the first byte of any instruction with a breakpoint, including other one byte instructions, without over-writing other code).
> INT 3 指令生成一个以字节操作码CC用于调用该调试异常处理程序。这个一字节格式是非常有用的因为它可以用于使用断点来替换任意指令的第一个字节 ,包括哪些一字节指令,而不会覆写其它代码)
上述引用非常重要,但是目前去解释它还是为时过早。本文后面我们会回过头再看。
### int 3 实践
没错,知道事物背后的理论非常不错,不过,这些理论到底意思是啥?我们怎样使用 int 3 部署断点?或者怎么翻译成通用的编程术语 —— _请给我看代码_
没错,知道事物背后的理论非常不错,不过,这些理论到底意思是啥?我们怎样使用 `int 3` 部署断点?或者怎么翻译成通用的编程术语 —— _请给我看代码_
实际上,实现非常简单。一旦你的程序执行了 int 3 指令,操作系统就会停止程序 [[3]][21]。在 Linux这也是本文比较关心的地方 上,操作系统会发送给进程一个信号 ——  SIGTRAP
实际上,实现非常简单。一旦你的程序执行了 `int 3` 指令, OS 就会停止程序( OS 是怎么做到像这样停止进程的? OS 注册其 int 3 的控制程序到 CPU 即可,就这么简单)。在 Linux这也是本文比较关心的地方 上, OS 会发送给进程一个信号 ——  `SIGTRAP`
就是这样,真的。现在回想一下本系列的第一部分, 追踪进程(调试程序) 会得到其子进程(或它所连接的被调试进程)所得到的所有信号的通知,接下来你就知道了。
That's all there is to it - honest! Now recall from the first part of the series that a tracing (debugger) process gets notified of all the signals its child (or the process it attaches to for debugging) gets, and you can start getting a feel of where we're going.
That's it, no more computer architecture 101 jabber. It's time for examples and code.
就这样, 没有更多的电脑架构基础术语了。该是例子和代码的时候了。
### 手动设置断点
@ -81,9 +80,9 @@ msg2 db 'world!', 0xa
len2 equ $ - msg2
```
我现在在使用汇编语言,是为了当我们面对 C 代码的时候能清楚一些编译细节。上面代码做的事情非常简单就是在一行打印出“hello,”然后在下一行打印出“world!”。这与之前文章中的程序非常类似。
我现在在使用汇编语言,是为了当我们面对 C 代码的时候,能清楚一些编译细节。上面代码做的事情非常简单,就是在一行打印出 “hello,”,然后在下一行打印出 “world!”。这与之前文章中的程序非常类似。
现在我想在第一次打印和第二次打印之间设置一个断点。我们看到在第一条 int 0x80 后面,指令 mov edx, len2。首先我们需要知道该指令所映射的地址。运行 objdump -d:
现在我想在第一次打印和第二次打印之间设置一个断点。我们看到在第一条 `int 0x80` ,其后指令 `mov edx, len2`(等等,再次 int是的Linux 使用 `int 0x80` 来实现用户进程到系统内核的系统调用。用户将系统调用的号码及其参数放到寄存器,并执行 `int 0x80`。然后 CPU 会跳到相应的中断处理程序,其中, OS 注册了一个过程,该过程查看寄存器并决定要执行的系统调用。)首先,我们需要知道该指令所映射的地址。运行 `objdump -d`:
```
traced_printer2: file format elf32-i386
@ -112,15 +111,15 @@ Disassembly of section .text:
80480b1: cd 80 int $0x80
```
所以,我们要设置断点的地址是 0x8048096\。等等,这不是调试器工作的真是姿势,不是吗真正的调试器是在代码行和函数上设置断点而不是赤裸裸的内存地址完全正确但是目前我们仍然还没到那一步为了更像_真正的_调试器一样设置断点我们仍不得不首先理解一些符号和调试信息。所以现在我们就得面对内存地址。
所以,我们要设置断点的地址是 `0x8048096`。等等,这不是调试器工作的真实姿势,对吧真正的调试器是在代码行和函数上设置断点而不是赤裸裸的内存地址完全正确但是目前我们仍然还没到那一步为了更像_真正的_调试器一样设置断点我们仍不得不首先理解一些符号和调试信息。所以现在我们就得面对内存地址。
在这点上我真想又偏离一下主题。所以现在你有两个选择如果你真的感兴趣想知道_为什么_那个地址应该是 0x8048096它代表着什么那就看下面的部分。否则你只是想了解断点你可以跳过这部分。
在这点上我真想又偏离一下主题。所以现在你有两个选择如果你真的感兴趣想知道_为什么_那个地址应该是 `0x8048096`,它代表着什么,那就看下面的部分。否则你只是想了解断点,你可以跳过这部分。
### 题外话 —— 程序地址和入口
坦白说0x8048096 本身没多大意义,仅仅是偏移可执行 text 部分开端的一些字节。如果你看上面导出来的列表,你会看到 text 部分从地址 0x08048080\ 开始。这告诉操作系统在分配给进程的虚拟地址空间里,将该地址映射到 text 部分开始的地方。在 Linux 上面,这些地址可以是绝对地址(i.e. 当这些),因为通过虚拟地址系统,每个进程获得自己的一块内存,并且将整个 32 位地址空间看自己的(称为 “线性” 地址)。
坦白说,`0x8048096` 本身没多大意义,仅仅是可执行程序的 text 部分开端偏移的一些字节。如果你看上面导出来的列表,你会看到 text 部分从地址 `0x08048080` 开始。这告诉 OS 在分配给进程的虚拟地址空间里,将该地址映射到 text 部分开始的地方。在 Linux 上面,这些地址可以是绝对地址(例如,当可执行程序加载到内存中时它不做重定位),因为通过虚拟地址系统,每个进程获得自己的一块内存,并且将整个 32 位地址空间看自己的(称为 “线性” 地址)。
如果我们使用 readelf 命令检查 ELF 文件头部,我们会看到:
如果我们使用 `readelf` 命令检查 ELF 文件头部ELF可执行和可链接格式是 Linux 上用于对象文件、共享库和可执行程序的文件格式),我们会看到:
```
$ readelf -h traced_printer2
@ -146,11 +145,12 @@ ELF Header:
Section header string table index: 3
```
注意头部里的 "entry point address",它同样指向 0x8048080\。所以我们在系统层面解释该 elf 文件的编码信息,它意思是:
1. 映射 text 部分(包含所给的内容)到地址 0x8048080
2. 从入口 —— 地址 0x8048080 处开始执行
注意头部里的 `Entry point address`,它同样指向 `0x8048080`。所以我们在系统层面解释该 elf 文件的编码信息,它意思是:
但是,为什么是 0x8048080 呢?事实证明是一些历史原因。一些 google 的结果把我引向源头,声明每个进程的地址空间的前 128M 是保留在栈里的。128M 对应为 0x8000000该地址是可执行程序其他部分可能开始的地方。而 0x8048080比较特别是 Linux ld 链接器用作默认的入口地址。该入口可以通过给 ld 传递 -Ttextargument 参数改变。
1. 映射 text 部分(包含所给的内容)到地址 `0x8048080`
2. 从入口 —— 地址 `0x8048080` 处开始执行
但是,为什么是 `0x8048080` 呢?事实证明是一些历史原因。一些 Google 的结果把我引向源头,宣传每个进程的地址空间的前 128M 是保留在栈里的。128M 对应为 `0x8000000`,该地址是可执行程序其他部分可以开始的地方。而 `0x8048080`,比较特别,是 Linux `ld` 链接器使用的默认入口地址。该入口可以通过给 `ld` 传递 `-Ttext` 参数改变。
总结一下,这地址没啥特别的,我们可以随意修改它。只要 ELF 可执行文件被合理的组织并且头部里的入口地址与真正的程序代码text 部分)开始的地址匹配,一切都没问题。
@ -161,14 +161,14 @@ ELF Header:
1. 记住存储在目标地址的数据
2. 用 int 指令替换掉目标地址的第一个字节
然后,当调试器要求 OS 运行该进程的时候(通过上一次文章中提过的 PTRACE_CONT进程就会跑起来直到遇到 int 3,此处进程会停止运行,并且 OS 会发送一个信号给调试器。这里调试器会收到一个信号表明其子进程(或者说被追踪进程)停止了。调试器可以做以下工作:
然后,当调试器要求 OS 运行该进程的时候(通过上一篇文章中提过的 `PTRACE_CONT`),进程就会运行起来直到遇到 `int 3`,此处进程会停止运行,并且 OS 会发送一个信号给调试器。调试器会收到一个信号表明其子进程(或者说被追踪进程)停止了。调试器可以做以下工作:
1. 在目标地址,用原来的正常执行指令替换掉 int 3 指令
2. Roll the instruction pointer of the traced process back by one. This is needed because the instruction pointer now points  _after_  the int 3, having already executed it.
3. 允许用户在某些地方可以与进程交互,, since the process is still halted at the desired target address。这里你的调试器可以让你取得变量值,调用栈等等。
4. 当用户想继续运行,调试器会小心地把断点放回目标地址去,除非用户要求取消该断点。
2. 将被追踪进程的指令指针回退一步。这是因为现在指令指针位于刚刚执行过的 int 3 之后。
3. 允许用户以某些方式与进程交互,因为该进程仍然停止在特定的目标地址。这里你的调试器可以让你取得变量值,调用栈等等。
4. 当用户想继续运行,调试器会小心地把断点放回目标地址去(因为它在第 1 步时被移走了),除非用户要求取消该断点。
让我们来看看,这些步骤是如何翻译成具体代码的。我们会用到 part 1 里的调试器 “模板”fork 一个子进程并追踪它)。任何情况下,文末会有一个完整样例源代码的链接
让我们来看看,这些步骤是如何翻译成具体代码的。我们会用到第一篇里的调试器 “模板”fork 一个子进程并追踪它)。无论如何,文末会有一个完整样例源代码的链接
```
/* Obtain and show child's instruction pointer */
@ -181,7 +181,7 @@ unsigned data = ptrace(PTRACE_PEEKTEXT, child_pid, (void*)addr, 0);
procmsg("Original data at 0x%08x: 0x%08x\n", addr, data);
```
这里调试器从被追踪的进程中取回了指令指针,也检查了在 0x8048096\ 的字。当开始追踪运行文章开头的汇编代码,将会打印出:
这里调试器从被追踪的进程中取回了指令指针,也检查了在 `0x8048096` 的字。当开始追踪运行文章开头的汇编代码,将会打印出:
```
[13028] Child started. EIP = 0x08048080
@ -200,13 +200,13 @@ unsigned readback_data = ptrace(PTRACE_PEEKTEXT, child_pid, (void*)addr, 0);
procmsg("After trap, data at 0x%08x: 0x%08x\n", addr, readback_data);
```
注意到 int 3 是如何被插入到目标地址的。此处打印:
注意到 `int 3` 是如何被插入到目标地址的。此处打印:
```
[13028] After trap, data at 0x08048096: 0x000007cc
```
正如预料的那样 —— 0xba 被 0xcc 替换掉了。现在调试器运行子进程并等待它在断点处停止:
正如预料的那样 —— `0xba``0xcc` 替换掉了。现在调试器运行子进程并等待它在断点处停止:
```
/* Let the child run to the breakpoint and wait for it to
@ -260,12 +260,12 @@ ptrace(PTRACE_CONT, child_pid, 0, 0);
### 更多关于 int 3
现在可以回过头去看看 int 3 和 Intel 手册里那个神秘的说明,原文如下:
现在可以回过头去看看 `int 3` 和因特尔手册里那个神秘的说明,原文如下:
> This one byte form is valuable because it can be used to replace the first byte of any instruction with a breakpoint, including other one byte instructions, without over-writing other code
> 这个一字节格式是非常有用的,因为它可以用于使用断点来替换任意指令的第一个字节 ,包括哪些一字节指令,而不会覆写其它代码
int 指令在 x86 机器上占两个字节 —— 0xcd 紧跟在中断数后 [[6]][24]。int 3 已经编码为 cd 03但是为其还保留有一个单字节指令 —— 0xcc
int 指令在 x86 机器上占两个字节 —— `0xcd` 紧跟着中断数(细心的读者可以在上面列出的转储中发现 `int 0x80` 翻译成了 `cd 80`)。`int 3` 被编码为 `cd 03`,但是为其还保留了一个单字节指令 —— `0xcc`
为什么这样呢?因为这可以允许我们插入一个断点,而不需要重写多余的指令。这非常重要,考虑下面的代码:
@ -278,9 +278,9 @@ foo:
.. some code ..
```
假设你想在 dec eax 这里放置一个断点。这对应一个单字节指令(操作码为 0x48。由于替换断点的指令长于一个字节我们不得不强制覆盖掉下个指令call的一部分这就会篡改 call 指令,并很可能导致一些完全不合理的事情发生。这样一来 jz foo 会导致什么?都不用说在 dec eax 这里停止CPU 就已经直接去执行后面一些未知指令了。
假设你想在 `dec eax` 这里放置一个断点。这对应一个单字节指令(操作码为 `0x48`)。由于替换断点的指令长于一个字节,我们不得不强制覆盖掉下个指令(`call`)的一部分,这就会篡改 `call` 指令,并很可能导致一些完全不合理的事情发生。这样一来跳转到 `foo` 分支的 `jz foo` 指令会导致什么?就会不在 dec eax 这里停止CPU 径直去执行后面一些无效的指令了。
而有了单字节的 int 3 指令,这个问题就解决了。一个字节在 x86 上面的其他指令短得多,这样我们可以保证仅在我们想停止的指令处有改变
而有了单字节的 `int 3` 指令,这个问题就解决了。 1 字节是在 x86 上面所能找到的最短指令,这样我们可以保证仅改变我们想中断的指令
### 封装一些晦涩的细节
@ -290,7 +290,7 @@ foo:
目前为止,为了简单,我把注意力放在了目标汇编代码。现在是时候往上一个层次,去看看我们如何追踪一个 C 程序。
事实证明没那么简单 —— 找到放置断点位置的难度增加了。考虑下面样例程序:
事实证明并不是非常难 —— 找到放置断点位置有一点难罢了。考虑下面样例程序:
```
#include <stdio.h>
@ -309,7 +309,7 @@ int main()
}
```
假设我想在 do_stuff 入口处放置一个断点。我会先使用 objdump 反汇编一下可执行文件,但是打印出的东西太多。尤其看到很多无用,也不感兴趣的 C 程序运行时的初始化代码。所以我们仅看一下 do_stuff 部分:
假设我想在 `do_stuff` 入口处放置一个断点。我会先使用 `objdump` 反汇编一下可执行文件,但是打印出的东西太多。尤其看到很多无用,也不感兴趣的 C 程序运行时的初始化代码。所以我们仅看一下 `do_stuff` 部分:
```
080483e4 <do_stuff>:
@ -322,7 +322,7 @@ int main()
80483f7: c3 ret
```
那么,我们将会把断点放在 0x080483e4这是 do_stuff 第一条指令执行的地方。而且,该函数是在循环里面调用的,我们想要在断点处一直停止执行直到循环结束。我们将会使用 debuglib 来简化该流程,下面是完整的调试函数:
那么,我们将会把断点放在 `0x080483e4`,这是 `do_stuff` 第一条指令执行的地方。而且,该函数是在循环里面调用的,我们想要在断点处一直停止执行直到循环结束。我们将会使用 debuglib 来简化该流程,下面是完整的调试函数:
```
void run_debugger(pid_t child_pid)
@ -366,7 +366,7 @@ void run_debugger(pid_t child_pid)
}
```
为了避免去处理 EIP 标志位和目的进程的内存空间太麻烦,我们仅需要调用 create_breakpoint, resume_from_breakpoint  cleanup_breakpoint。让我们来看看追踪上面的 C 代码样例会输出啥
为了避免修改 EIP 标志位和目的进程的内存空间的麻烦,我们仅需要调用 `create_breakpoint``resume_from_breakpoint` 和 `cleanup_breakpoint`。让我们来看看追踪上面的 C 代码样例会输出什么
```
$ bp_use_lib traced_c_loop
@ -396,11 +396,11 @@ world!
[这里是][25]本文用到的完整源代码文件。在归档中你可以找到:
* debuglib.h and debuglib.c - the simple library for encapsulating some of the inner workings of a debugger
* bp_manual.c - the "manual" way of setting breakpoints presented first in this article. Uses the debuglib library for some boilerplate code.
* bp_use_lib.c - uses debuglib for most of its code, as demonstrated in the second code sample for tracing the loop in a C program.
* debuglib.h 和 debuglib.c - 封装了调试器的一些内部工作的示例库
* bp_manual.c - 这篇文章开始部分介绍的“手动”设置断点的方法。一些样板代码使用了 debuglib 库。
* bp_use_lib.c - 大部分代码使用了 debuglib 用于在第二个代码范例中演示在 C 程序的循环中追踪。
### 引
### 引
在准备本文的时候,我搜集了如下的资源和文章:
@ -413,28 +413,13 @@ world!
* [GDB Internals][18]
[1] On a high-level view this is true. Down in the gory details, many CPUs today execute multiple instructions in parallel, some of them not in their original order.
[2] The bible in this case being, of course, Intel's Architecture software developer's manual, volume 2A.
[3] How can the OS stop a process just like that? The OS registered its own handler for int 3 with the CPU, that's how!
[4] Wait, int again? Yes! Linux uses int 0x80 to implement system calls from user processes into the OS kernel. The user places the number of the system call and its arguments into registers and executes int 0x80. The CPU then jumps to the appropriate interrupt handler, where the OS registered a procedure that looks at the registers and decides which system call to execute.
[5] ELF (Executable and Linkable Format) is the file format used by Linux for object files, shared libraries and executables.
[6] An observant reader can spot the translation of int 0x80 into cd 80 in the dumps listed above.
--------------------------------------------------------------------------------
via: http://eli.thegreenplace.net/2011/01/27/how-debuggers-work-part-2-breakpoints
作者:[Eli Bendersky][a]
译者:[译者ID](https://github.com/译者ID)
校对:[校对者ID](https://github.com/校对者ID)
译者:[wi-cuckoo](https://github.com/wi-cuckoo)
校对:[wxy](https://github.com/wxy)
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
@ -465,4 +450,4 @@ via: http://eli.thegreenplace.net/2011/01/27/how-debuggers-work-part-2-breakpoin
[24]:http://eli.thegreenplace.net/2011/01/27/how-debuggers-work-part-2-breakpoints#id12
[25]:https://github.com/eliben/code-for-blog/tree/master/2011/debuggers_part2_code
[26]:http://eli.thegreenplace.net/2011/01/27/how-debuggers-work-part-2-breakpoints
[27]:http://eli.thegreenplace.net/2011/01/23/how-debuggers-work-part-1/
[27]:https://linux.cn/article-8418-1.html