在上一部分我们学习了关于 DWARF 的信息,以及它如何被用于读取变量和将被执行的机器码与我们的高级语言的源码联系起来。在这一部分,我们将进入实践,实现一些我们调试器后面会使用的 DWARF 原语。我们也会利用这个机会,使我们的调试器可以在命中一个断点时打印出当前的源码上下文。
系列文章索引
随着后面文章的发布,这些链接会逐渐生效。
设置我们的 DWARF 解析器
正如我在这系列文章开始时备注的,我们会使用 libelfin 来处理我们的 DWARF 信息。希望你已经在第一部分设置好了这些,如果没有的话,现在做吧,确保你使用我仓库的 fbreg
分支。
一旦你构建好了 libelfin
,就可以把它添加到我们的调试器。第一步是解析我们的 ELF 可执行程序并从中提取 DWARF 信息。使用 libelfin
可以轻易实现,只需要对调试器
作以下更改:
class debugger {
public:
debugger (std::string prog_name, pid_t pid)
: m_prog_name{std::move(prog_name)}, m_pid{pid} {
auto fd = open(m_prog_name.c_str(), O_RDONLY);
m_elf = elf::elf{elf::create_mmap_loader(fd)};
m_dwarf = dwarf::dwarf{dwarf::elf::create_loader(m_elf)};
}
//...
private:
//...
dwarf::dwarf m_dwarf;
elf::elf m_elf;
};
我们使用了 open
而不是 std::ifstream
,因为 elf 加载器需要传递一个 UNIX 文件描述符给 mmap
,从而可以将文件映射到内存而不是每次读取一部分。
调试信息原语
下一步我们可以实现从程序计数器的值中提取行条目(line entry)以及函数 DWARF 信息条目(function DIE)的函数。我们从 get_function_from_pc
开始:
dwarf::die debugger::get_function_from_pc(uint64_t pc) {
for (auto &cu : m_dwarf.compilation_units()) {
if (die_pc_range(cu.root()).contains(pc)) {
for (const auto& die : cu.root()) {
if (die.tag == dwarf::DW_TAG::subprogram) {
if (die_pc_range(die).contains(pc)) {
return die;
}
}
}
}
}
throw std::out_of_range{"Cannot find function"};
}
这里我采用了朴素的方法,迭代遍历编译单元直到找到一个包含程序计数器的,然后迭代遍历它的子节点直到我们找到相关函数(DW_TAG_subprogram
)。正如我在上一篇中提到的,如果你想要的话你可以处理类似的成员函数或者内联等情况。
接下来是 get_line_entry_from_pc
:
dwarf::line_table::iterator debugger::get_line_entry_from_pc(uint64_t pc) {
for (auto &cu : m_dwarf.compilation_units()) {
if (die_pc_range(cu.root()).contains(pc)) {
auto < = cu.get_line_table();
auto it = lt.find_address(pc);
if (it == lt.end()) {
throw std::out_of_range{"Cannot find line entry"};
}
else {
return it;
}
}
}
throw std::out_of_range{"Cannot find line entry"};
}
同样,我们可以简单地找到正确的编译单元,然后查询行表获取相关的条目。
打印源码
当我们命中一个断点或者逐步执行我们的代码时,我们会想知道处于源码中的什么位置。
void debugger::print_source(const std::string& file_name, unsigned line, unsigned n_lines_context) {
std::ifstream file {file_name};
//获得一个所需行附近的窗口
auto start_line = line