蚊章汇 - 断点
type: channel
link: https://linuxcn.bwsl.wang/tag/%E6%96%AD%E7%82%B9/
items: 开发一个 Linux 调试器（二）：断点

开发一个 Linux 调试器（二）：断点
type: item
date: 2017-06-27T16:53:00+08:00
description: 在该系列的第一部分，我们写了一个小的进程启动器，作为我们调试器的基础。在这篇博客中，我们会学习在 x86 Linux 上断点是如何工作的，以及如何给我们工具添加设置断点的能力。系列文章索引随着后面文章的发布，这些链接会逐渐生效。准备环境断点寄存器和内存Elves 和 dwarves源码和信号源码层逐步执行源码层断点调用栈读取变量 10.之后步骤断点是如何形成的？有两种类型的断点：硬件和软件。硬件断点通常涉及到设置与体系结构相关的寄存器来为你产生断点，而软件断点则涉及到修改正在执行的代码。在这篇文章中我们只会关注软件断点，因为它们比较简单，而且可以设置任意多断点。在 x86 机器上任一时刻你最多只能有 4 个硬件断点，但是它们能让你在读取或者写入给定地址时触发，而不是只有当代码执行到那里的时候。我前面说软件断点是通过修改正在执行的代码实现的，那么问题就来了：我们如何修改代码？为了设置断点我们要做什么修改？如何告知调试器？第一个问题的答案显然是 ptrace。我们之前已经用它为我们的程序设置跟踪并继续程序的执行，但我们也可以用它来读或者写内存。当执行到断点时，我们的更改要让处理器暂停并给程序发送信号。在 x86 机器上这是通过 int 3 重写该地址上的指令实现的。x86 机器有个 中断向量表 （ interrupt vector table ） ，操作系统能用它来为多种事件注册处理程序，例如页故障、保护故障和无效操作码。它就像是注册错误处理回调函数，但是是在硬件层面的。当处理器执行 int 3 指令时，控制权就被传递给断点中断处理器，对于 Linux 来说，就是给进程发送 SIGTRAP 信号。你可以在下图中看到这个进程，我们用 0xcc 覆盖了 mov 指令的第一个字节，它是 init 3 的指令代码。谜题的最后一个部分是调试器如何被告知中断的。如果你回顾前面的文章，我们可以用 waitpid 来监听被发送给被调试的程序的信号。这里我们也可以这样做：设置断点、继续执行程序、调用 waitpid 并等待直到发生 SIGTRAP。然后就可以通过打印已运行到的源码位置、或改变有图形用户界面的调试器中关注的代码行，将这个断点传达给用户。实现软件断点我们会实现一个 breakpoint 类来表示某个位置的断点，我们可以根据需要启用或者停用该断点。class breakpoint { public: breakpoint(pid_t pid, std::intptr_t addr) : m_pid{pid}, m_addr{addr}, m_enabled{false}, m_saved_data{} {} void enable(); void disable(); auto is_enabled() const -> bool { return m_enabled; } auto get_address() const -> std::intptr_t { return m_addr; } private: pid_t m_pid; std::intptr_t m_addr; bool m_enabled; uint64_t m_saved_data; //data which used to be at the breakpoint address }; 这里的大部分代码都是跟踪状态；真正神奇的地方是 enable 和 disable 函数。正如我们上面学到的，我们要用 int 3 指令 - 编码为 0xcc - 替换当前指定地址的指令。我们还要保存该地址之前的值，以便后面恢复该代码；我们不想忘了执行用户（原来）的代码。void breakpoint::enable() { m_saved_data = ptrace(PTRACE_PEEKDATA, m_pid, m_addr, nullptr); uint64_t int3 = 0xcc; uint64_t data_with_int3 = ((m_saved_data & ~0xff) | int3); //set bottom byte to 0xcc ptrace(PTRACE_POKEDATA, m_pid, m_addr, data_with_int3); m_enabled = true; } PTRACE_PEEKDATA 请求告知 ptrace 如何读取被跟踪进程的内存。我们给它一个进程 ID 和一个地址，然后它返回给我们该地址当前的 64 位内容。 (m_saved_data & ~0xff) 把这个数据的低位字节置零，然后我们用它和我们的 int 3 指令按位或（OR）来设置断点。最后我们通过 PTRACE_POKEDATA 用我们的新数据覆盖那部分内存来设置…