PRF&PUB:20170619 Writing a Linux Debugger Part 7 Source-level breakpoints

@geekpi
This commit is contained in:
wxy 2017-09-22 09:40:07 +08:00
parent b7d3348dad
commit 062dc8c758

View File

@ -1,45 +1,32 @@
开发一个 Linux 调试器(七):源码断点
开发一个 Linux 调试器(七):源码断点
============================================================
在内存地址上设置断点是可以的,但它没有提供最方便用户的工具。我们希望能够在源代码行和函数入口地址上设置断点,以便我们可以在与代码相同的抽象级别进行调试。
在内存地址上设置断点虽然不错,但它并没有提供最方便用户的工具。我们希望能够在源代码行和函数入口地址上设置断点,以便我们可以在与代码相同的抽象级别进行调试。
这篇文章将会添加源码层断点到我们的调试器中。通过所有我们已经支持的,这比起最初听起来容易得多。我们还将添加一个命令来获取符号的类型和地址,这对于定位代码或数据以及理解链接概念非常有用。
* * *
这篇文章将会添加源码级断点到我们的调试器中。通过所有我们已经支持的功能,这要比起最初听起来容易得多。我们还将添加一个命令来获取符号的类型和地址,这对于定位代码或数据以及理解链接概念非常有用。
### 系列索引
随着后面文章的发布,这些链接会逐渐生效。
1. [准备环境][1]
2. [断点][2]
3. [寄存器和内存][3]
4. [Elves 和 dwarves][4]
5. [源码和信号][5]
6. [源码层逐步执行][6]
7. [源码层断点][7]
6. [源码级逐步执行][6]
7. [源码级断点][7]
8. [调用栈][8]
9. 读取变量
10. 之后步骤
* * *
### 断点
### DWARF
#### DWARF
[Elves 和 dwarves][9] 这篇文章,描述了 DWARF 调试信息是如何工作的以及如何用它来将机器码映射到高层源码中。回想一下DWARF 包含函数的地址范围和一个允许你在抽象层之间转换代码位置的行表。我们将使用这些功能来实现我们的断点。
[Elves 和 dwarves][4] 这篇文章,描述了 DWARF 调试信息是如何工作的以及如何用它来将机器码映射到高层源码中。回想一下DWARF 包含函数的地址范围和一个允许你在抽象层之间转换代码位置的行表。我们将使用这些功能来实现我们的断点。
### 函数入口
#### 函数入口
如果你考虑重载、成员函数等等那么在函数名上设置断点可能有点复杂但是我们将遍历所有的编译单元并搜索与我们正在寻找的名称匹配的函数。DWARF 信息如下所示:
@ -85,13 +72,13 @@ void debugger::set_breakpoint_at_function(const std::string& name) {
}
```
这代码看起来有点奇怪的唯一一点是 `++entry`。 问题是函数的 `DW_AT_low_pc` 不指向该函数的用户代码的起始地址,它指向 prologue 的开始。编译器通常会输出一个函数的 prologue 和 epilogue它们用于执行保存和恢复堆栈、操作堆栈指针等。这对我们来说不是很有用所以我们将入口行加一来获取用户代码的第一行而不是 prologue。DWARF 行表实际上具有一些功能,用于将入口标记为函数 prologue 之后的第一行,但并不是所有编译器都输出该函数,因此我采用了原始的方法。
这代码看起来有点奇怪的唯一一点是 `++entry`。 问题是函数的 `DW_AT_low_pc` 不指向该函数的用户代码的起始地址,它指向 prologue 的开始。编译器通常会输出一个函数的 prologue 和 epilogue它们用于执行保存和恢复堆栈、操作堆栈指针等。这对我们来说不是很有用所以我们将入口行加一来获取用户代码的第一行而不是 prologue。DWARF 行表实际上具有一些功能,用于将入口标记为函数 prologue 之后的第一行,但并不是所有编译器都输出,因此我采用了原始的方法。
### 源码行
#### 源码行
要在高层源码行上设置一个断点,我们要将这个行号转换成 DWARF 中的一个地址。我们将遍历编译单元,寻找一个名称与给定文件匹配的编译单元,然后查找与给定行对应的入口。
DWARF 看去有点像这样:
DWARF 看去有点像这样:
```
.debug_line: line number info for a single cu
@ -119,7 +106,7 @@ IS=val ISA number, DI=val discriminator value
```
所以如果我们想要在 `ab.cpp` 的第五行设置一个断点,我们查找与行 (`0x004004e3`) 相关的入口并设置一个断点。
所以如果我们想要在 `ab.cpp` 的第五行设置一个断点,我们查找与行 (`0x004004e3`) 相关的入口并设置一个断点。
```
void debugger::set_breakpoint_at_source_line(const std::string& file, unsigned line) {
@ -138,13 +125,11 @@ void debugger::set_breakpoint_at_source_line(const std::string& file, unsigned l
}
```
我这里的 `is_suffix` hack这样你可以为 `a/b/c.cpp` 输入 `c.cpp`。当然你应该使用大小写敏感路径处理库或者其他东西。我很懒。`entry.is_stmt` 是检查行表入口是否被标记为一个语句的开头,这是由编译器根据它认为是断点的最佳目标的地址设置的。
* * *
我这里做了 `is_suffix` hack这样你可以输入 `c.cpp` 代表 `a/b/c.cpp` 。当然你实际上应该使用大小写敏感路径处理库或者其它东西,但是我比较懒。`entry.is_stmt` 是检查行表入口是否被标记为一个语句的开头,这是由编译器根据它认为是断点的最佳目标的地址设置的。
### 符号查找
当我们在对象文件层时,符号是王者。函数用符号命名,全局变量用符号命名,得到一个符号,我们得到一个符号,每个人都得到一个符号。 在给定的对象文件中,一些符号可能引用其他对象文件或共享库,链接器将从符号引用创建一个可执行程序。
当我们在对象文件层时,符号是王者。函数用符号命名,全局变量用符号命名,得到一个符号,我们得到一个符号,每个人都得到一个符号。 在给定的对象文件中,一些符号可能引用其他对象文件或共享库,链接器将从符号引用创建一个可执行程序。
可以在正确命名的符号表中查找符号,它存储在二进制文件的 ELF 部分中。幸运的是,`libelfin` 有一个不错的接口来做这件事,所以我们不需要自己处理所有的 ELF 的事情。为了让你知道我们在处理什么,下面是一个二进制文件的 `.symtab` 部分的转储,它由 `readelf` 生成:
@ -222,7 +207,7 @@ Num: Value Size Type Bind Vis Ndx Name
你可以在对象文件中看到用于设置环境的很多符号,最后还可以看到 `main` 符号。
我们对符号的类型、名称和值(地址)感兴趣。我们有一个 `symbol_type` 类型的枚举,并使用一个 `std::string` 作为名称,`std::uintptr_t` 作为地址:
我们对符号的类型、名称和值(地址)感兴趣。我们有一个该类型的 `symbol_type` 枚举,并使用一个 `std::string` 作为名称,`std::uintptr_t` 作为地址:
```
enum class symbol_type {
@ -265,7 +250,7 @@ symbol_type to_symbol_type(elf::stt sym) {
};
```
最后我们要查找符号。为了说明的目的,我循环查找符号表的 ELF 部分,然后收集我在其中找到的任意符号到 `std::vector` 中。更智能的实现建立从名称到符号的映射,这样你只需要查看一次数据就行了。
最后我们要查找符号。为了说明的目的,我循环查找符号表的 ELF 部分,然后收集我在其中找到的任意符号到 `std::vector` 中。更智能的实现可以建立从名称到符号的映射,这样你只需要查看一次数据就行了。
```
std::vector<symbol> debugger::lookup_symbol(const std::string& name) {
@ -287,16 +272,12 @@ std::vector<symbol> debugger::lookup_symbol(const std::string& name) {
}
```
* * *
### 添加命令
一如往常,我们需要添加一些更多的命令来向用户暴露功能。对于断点,我使用 GDB 风格的接口,其中断点类型是通过你传递的参数推断的,而不用要求显式切换:
* `0x<hexadecimal>` -> 断点地址
* `<line>:<filename>` -> 断点行号
* `<anything else>` -> 断点函数名
```
@ -326,11 +307,9 @@ else if(is_prefix(command, "symbol")) {
}
```
* * *
### 测试一下
在一个简单的二进制文件上启动调试器,并设置源代码级别的断点。在一些 `foo` 上设置一个断点,看到我的调试器停在它上面是我这个项目最有价值的时刻之一。
在一个简单的二进制文件上启动调试器,并设置源代码级别的断点。在一些 `foo` 函数上设置一个断点,看到我的调试器停在它上面是我这个项目最有价值的时刻之一。
符号查找可以通过在程序中添加一些函数或全局变量并查找它们的名称来进行测试。请注意,如果你正在编译 C++ 代码,你还需要考虑[名称重整][10]。
@ -342,19 +321,19 @@ else if(is_prefix(command, "symbol")) {
via: https://blog.tartanllama.xyz/c++/2017/06/19/writing-a-linux-debugger-source-break/
作者:[Simon Brand ][a]
作者:[Simon Brand][a]
译者:[geekpi](https://github.com/geekpi)
校对:[校对者ID](https://github.com/校对者ID)
校对:[wxy](https://github.com/wxy)
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
[a]:https://twitter.com/TartanLlama
[1]:https://blog.tartanllama.xyz/c++/2017/03/21/writing-a-linux-debugger-setup/
[2]:https://blog.tartanllama.xyz/c++/2017/03/24/writing-a-linux-debugger-breakpoints/
[3]:https://blog.tartanllama.xyz/c++/2017/03/31/writing-a-linux-debugger-registers/
[4]:https://blog.tartanllama.xyz/c++/2017/04/05/writing-a-linux-debugger-elf-dwarf/
[5]:https://blog.tartanllama.xyz/c++/2017/04/24/writing-a-linux-debugger-source-signal/
[6]:https://blog.tartanllama.xyz/c++/2017/05/06/writing-a-linux-debugger-dwarf-step/
[1]:https://linux.cn/article-8626-1.html
[2]:https://linux.cn/article-8645-1.html
[3]:https://linux.cn/article-8663-1.html
[4]:https://linux.cn/article-8719-1.html
[5]:https://linux.cn/article-8812-1.html
[6]:https://linux.cn/article-8813-1.html
[7]:https://blog.tartanllama.xyz/c++/2017/06/19/writing-a-linux-debugger-source-break/
[8]:https://blog.tartanllama.xyz/c++/2017/06/24/writing-a-linux-debugger-unwinding/
[9]:https://blog.tartanllama.xyz/c++/2017/04/05/writing-a-linux-debugger-elf-dwarf/