新闻资讯
Group news
青岛广盛源肥业有限公司    您的位置: 首页  >  新闻资讯  >  正文

需要了解的Linux调试器之源码级断点

2019年10月12日 文章来源:网络整理 热度:111℃ 作者:刘英

在内存地址上设置断点虽然不错,但它并没有提供最方便用户的工具。我们希望能够在源代码行和函数入口地址上设置断点,以便我们可以在与代码相同的抽象级别中进行调试。

这篇文章将会添加源码级断点到我们的调试器中。通过所有我们已经支持的功能,这要比起最初听起来容易得多。我们还将添加一个命令来获取符号的类型和地址,这对于定位代码或数据以及理解链接概念非常有用。

系列索引

随着后面文章的发布,这些链接会逐渐生效。

准备环境

断点

寄存器和内存

Elves 和 dwarves

源码和信号

源码级逐步执行

源码级断点

调用栈

读取变量

之后步骤

断点

DWARF

Elves 和 dwarves 这篇文章,描述了 DWARF 调试信息是如何工作的,以及如何用它来将机器码映射到高层源码中。回想一下,DWARF 包含了函数的地址范围和一个允许你在抽象层之间转换代码位置的行表。我们将使用这些功能来实现我们的断点。

函数入口

如果你考虑重载、成员函数等等,那么在函数名上设置断点可能有点复杂,但是我们将遍历所有的编译单元,并搜索与我们正在寻找的名称匹配的函数。DWARF 信息如下所示:

< 0> DW_TAG_compile_unit DW_AT_producer clang version 3.9.1 (tags/RELEASE_391/final) DW_AT_language DW_LANG_C_plus_plus DW_AT_name /super/secret/path/MiniDbg/examples/variable.cpp DW_AT_stmt_list 0x00000000 DW_AT_comp_dir /super/secret/path/MiniDbg/build DW_AT_low_pc 0x00400670 DW_AT_high_pc 0x0040069cLOCAL_SYMBOLS:< 1> DW_TAG_subprogram DW_AT_low_pc 0x00400670 DW_AT_high_pc 0x0040069c DW_AT_name foo ...... DW_TAG_subprogram DW_AT_low_pc 0x00400700 DW_AT_high_pc 0x004007a0 DW_AT_name bar ...

我们想要匹配 DW_AT_name 并使用 DW_AT_low_pc(函数的起始地址)来设置我们的断点。

void debugger::set_breakpoint_at_function(const std::string& name) {for (const auto& cu : m_dwarf.compilaTIon_units()) {for (const auto& die : cu.root()) {if (die.has(dwarf::DW_AT::name) && at_name(die) == name) {auto low_pc = at_low_pc(die);auto entry = get_line_entry_from_pc(low_pc);++entry; //skip prologueset_breakpoint_at_address(entry->address);}}}}

这代码看起来有点奇怪的唯一一点是 ++entry。 问题是函数的 DW_AT_low_pc 不指向该函数的用户代码的起始地址,它指向 prologue 的开始。编译器通常会输出一个函数的 prologue 和 epilogue,它们用于执行保存和恢复堆栈、操作堆栈指针等。这对我们来说不是很有用,所以我们将入口行加一来获取用户代码的第一行而不是 prologue。DWARF 行表实际上具有一些功能,用于将入口标记为函数 prologue 之后的第一行,但并不是所有编译器都输出它,因此我采用了原始的方法。

源码行

要在高层源码行上设置一个断点,我们要将这个行号转换成 DWARF 中的一个地址。我们将遍历编译单元,寻找一个名称与给定文件匹配的编译单元,然后查找与给定行对应的入口。

DWARF 看上去有点像这样:

.debug_line: line number info for a single cuSource lines (from CU-DIE at .debug_info offset 0x0000000b):NS new statement, BB new basic block, ET end of text sequencePE prologue end, EB epilogue beginIS=val ISA number, DI=val discriminator value[lno,col] NS BB ET PE EB IS= DI= uri: "filepath"0x004004a7 [ 1, 0] NS uri: "/super/secret/path/a.hpp"0x004004ab [ 2, 0] NS0x004004b2 [ 3, 0] NS0x004004b9 [ 4, 0] NS0x004004c1 [ 5, 0] NS0x004004c3 [ 1, 0] NS uri: "/super/secret/path/b.hpp"0x004004c7 [ 2, 0] NS0x004004ce [ 3, 0] NS0x004004d5 [ 4, 0] NS0x004004dd [ 5, 0] NS0x004004df [ 4, 0] NS uri: "/super/secret/path/ab.cpp"0x004004e3 [ 5, 0] NS0x004004e8 [ 6, 0] NS0x004004ed [ 7, 0] NS0x004004f4 [ 7, 0] NS ET

所以如果我们想要在 ab.cpp 的第五行设置一个断点,我们将查找与行 (0x004004e3) 相关的入口并设置一个断点。

void debugger::set_breakpoint_at_source_line(const std::string& file, unsigned line) {for (const auto& cu : m_dwarf.compilaTIon_units()) {if (is_suffix(file, at_name(cu.root()))) {const auto& lt = cu.get_line_table();for (const auto& entry : lt) {if (entry.is_stmt && entry.line == line) {set_breakpoint_at_address(entry.address);return;}}}}}

我这里做了 is_suffix hack,这样你可以输入 c.cpp 代表 a/b/c.cpp 。当然你实际上应该使用大小写敏感路径处理库或者其它东西,但是我比较懒。entry.is_stmt 是检查行表入口是否被标记为一个语句的开头,这是由编译器根据它认为是断点的最佳目标的地址设置的。

符号查找

当我们在对象文件层时,符号是王者。函数用符号命名,全局变量用符号命名,你得到一个符号,我们得到一个符号,每个人都得到一个符号。 在给定的对象文件中,一些符号可能引用其他对象文件或共享库,链接器将从符号引用创建一个可执行程序。

上一篇:Linux驱动技术中的异步通知技术


下一篇:关于Epoll,你应该知道的那些细节

友情链接
Links