校对完毕

校对完毕  谢谢。
This commit is contained in:
jasminepeng 2017-06-20 16:55:27 +08:00 committed by GitHub
parent 09063e0100
commit d47fd08cb2

View File

@ -1,7 +1,7 @@
开发 Linux 调试器第二部分:断点
============================================================
在该系列的第一部分,我们写了一个小的进程启动器作为我们调试器的基础。在这篇博客中,我们会学习断点如何在 x86 Linux 上工作以及给我们工具添加设置断点的能力。
在该系列的第一部分,我们写了一个小的进程启动器作为我们调试器的基础。在这篇博客中,我们会学习在 x86 Linux 上断点如何工作以及给我们工具添加设置断点的能力。
* * *
@ -13,19 +13,18 @@
2. [断点][3]
3. 寄存器和内存
4. Elves 和 dwarves
5. 逐步、源码和信号
6. 使用 DWARF 调试信息逐步执行
5. 源码和信号
6. 源码层逐步执行
7. 源码层断点
8. 调用栈
9. 读取变量
10. 下一步
10.之后步骤
译者注ELF[Executable and Linkable Format](https://en.wikipedia.org/wiki/Executable_and_Linkable_Format "Executable and Linkable Format") 可执行文件格式DWARF一种广泛使用的调试数据格式参考 [WIKI](https://en.wikipedia.org/wiki/DWARF "DWARF WIKI")
* * *
### 断点如何形成?
有两种类型的断点:硬件和软件。硬件断点通常涉及设置和体系结构相关的寄存器来为你产生断点,而软件断点则涉及修改正在执行的代码。在这篇文章中我们只会关注软件断点,因为它们比较简单,而且足够完成你想要的功能。在 x86 机器上任一时刻你最多只能有 4 个硬件断点,但是它们能使你通过读取或者写入给定地址生效,而不是只有当执行到那里的时候。
有两种类型的断点:硬件和软件。硬件断点通常涉及设置和体系结构相关的寄存器来为你产生断点,而软件断点则涉及修改正在执行的代码。在这篇文章中我们只会关注软件断点,因为它们比较简单,而且可以设置任意多断点。在 x86 机器上任一时刻你最多只能有 4 个硬件断点,但是它们能使你通过读取或者写入给定地址生效,而不是只有当执行到那里的时候。
我前面说软件断点是通过修改正在执行的代码实现的,那么问题就来了:
@ -35,11 +34,11 @@
第一个问题的答案显然是 `ptrace`。我们之前已经用它来启动我们的程序以便跟踪和继续它的执行,但我们也可以用它来读或者写内存。
当执行到断点时,我们的更改要让处理器暂停并给程序发送信号。在 x86 机器上这是通过 `int 3` 重写该地址上的指令实现的。x86 机器有个`interrupt vector table`(中断向量表),操作系统能用它来为多种事件注册处理程序,例如页故障、保护故障和无效操作码。它就像是注册错误处理回调函数,但是在硬件层面的。当处理器执行 `int 3` 指令时,控制权就被传递给断点中断处理器,对于 Linux 来说,就是给进程发送 `SIGTRAP` 信号。你可以在下图中看到这个进程,我们用 `0xcc`-`int 3` 的指令编码 - 覆盖了 `mov` 指令的第一个字节。
当执行到断点时,我们的更改要让处理器暂停并给程序发送信号。在 x86 机器上这是通过 `int 3` 重写该地址上的指令实现的。x86 机器有个`interrupt vector table`(中断向量表),操作系统能用它来为多种事件注册处理程序,例如页故障、保护故障和无效操作码。它就像是注册错误处理回调函数,但是在硬件层面的。当处理器执行 `int 3` 指令时,控制权就被传递给断点中断处理器,对于 Linux 来说,就是给进程发送 `SIGTRAP` 信号。你可以在下图中看到这个进程,我们用 `0xcc` 覆盖了 `mov` 指令的第一个字节,它是 `init 3` 的指令代码
![断点](http://blog.tartanllama.xyz/assets/breakpoint.png)
最后一个谜题是调试器如何被告知中断的。如果你回顾前面的文章,我们可以用 `waitpid` 来监听被发送给被调试程序的信号。这里我们也可以这样做:设置断点、继续执行程序、调用 `waitpid` 然后等待直到发生 `SIGTRAP`。然后就可以通过打印已运行到的源码位置、或改变有图形用户界面的调试器中关注的代码行从而将这个断点传达给用户。
最后一个谜题是调试器如何被告知中断的。如果你回顾前面的文章,我们可以用 `waitpid` 来监听被发送给被调试程序的信号。这里我们也可以这样做:设置断点、继续执行程序、调用 `waitpid` 然后等待直到发生 `SIGTRAP`。然后就可以通过打印已运行到的源码位置、或改变有图形用户界面的调试器中关注的代码行将这个断点传达给用户。
* * *
@ -153,13 +152,13 @@ void debugger::handle_command(const std::string& line) {
### 从断点继续执行
如果你尝试这样做,你可能会发现如果你从断点处继续执行,不会发生任何事情。这是因为断点仍然在内存中,因此一直被命中。简单的解决办法就是停用这个断点、运行到下一步、再次启用这个断点、然后继续执行。不幸的是我们还需要更改程序计数器指回断点前面,我们会将这部分内容留到下一篇博客学习了如何操作寄存器之后
如果你尝试这样做,你可能会发现如果你从断点处继续执行,不会发生任何事情。这是因为断点仍然在内存中,因此一直被命中。简单的解决办法就是停用这个断点、运行到下一步、再次启用这个断点、然后继续执行。不过我们还需要更改程序计数器,指回到断点前面,这部分内容会留到下一篇关于操作寄存器的文章中介绍
* * *
### 测试它
当然,如果你不知道要设置的地址,设置断点并非很有帮助。后面我们会学习如何在函数名或者代码行设置断点,但现在我们可以通过手动实现。
当然,如果你不知道要设置的地址,在某些地址设置断点并非很有用。后面我们会学习如何在函数名或者代码行设置断点,但现在我们可以通过手动实现。
测试你调试器的简单方法是写一个 hello world 程序,这个程序输出到 `std::err`(为了避免缓存),并在调用输出操作符的地方设置断点。如果你继续执行被调试的程序,执行很可能会停止而不会输出任何东西。然后你可以重启调试器并在调用之后设置一个断点,现在你应该看到成功地输出了消息。