linux 测试工具源码,开发一个Linux调试器(五):源码和信号

7646aaf47b48352f6483b01ec48b6b00.png

在上一部分我们学习了关于 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)) {

returndie;

}

}

}

}

}

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 &lt = cu.get_line_table();

auto it = lt.find_address(pc);

if (it == lt.end()) {

throw std::out_of_range{"Cannot find line entry"};

}

else{

returnit;

}

}

}

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 <= n_lines_context ? 1 : line - n_lines_context;

auto end_line = line + n_lines_context + (line 

charc{};

auto current_line = 1u;

//跳过 start_line 之前的行

while (current_line != start_line && file.get(c)) {

if (c == '\n') {

++current_line;

}

}

//如果我们在当前行则输出光标

std::cout < ":"  ");

//输出行直到 end_line

while (current_line <= end_line && file.get(c)) {

std::cout <

if (c == '\n') {

++current_line;

//如果我们在当前行则输出光标

std::cout < ":"  ");

}

}

//输出换行确保恰当地清空了流

std::cout <

}

现在我们可以打印出源码了,我们需要将这些通过钩子添加到我们的调试器。实现这个的一个好地方是当调试器从一个断点或者(最终)逐步执行得到一个信号时。到了这里,我们可能想要给我们的调试器添加一些更好的信号处理。

更好的信号处理

我们希望能够得知什么信号被发送给了进程,同样我们也想知道它是如何产生的。例如,我们希望能够得知是否由于***了一个断点从而获得一个 SIGTRAP,还是由于逐步执行完成、或者是产生了一个新线程等等导致的。幸运的是,我们可以再一次使用 ptrace。可以给 ptrace 的一个命令是 PTRACE_GETSIGINFO,它会给你被发送给进程的***一个信号的信息。我们类似这样使用它:

siginfo_t debugger::get_signal_info() {

siginfo_t info;

ptrace(PTRACE_GETSIGINFO, m_pid, nullptr, &info);

returninfo;

}

这会给我们一个 siginfo_t 对象,它能提供以下信息:

siginfo_t {

intsi_signo;     /* 信号编号 */

intsi_errno;     /* errno 值 */

intsi_code;      /* 信号代码 */

intsi_trapno;    /* 导致生成硬件信号的陷阱编号

(大部分架构中都没有使用) */

pid_t    si_pid;       /* 发送信号的进程 ID */

uid_t    si_uid;       /* 发送信号进程的用户 ID */

intsi_status;    /* 退出值或信号 */

clock_t  si_utime;     /* 消耗的用户时间 */

clock_t  si_stime;     /* 消耗的系统时间 */

sigval_t si_value;     /* 信号值 */

intsi_int;       /* POSIX.1b 信号 */

void    *si_ptr;       /* POSIX.1b 信号 */

intsi_overrun;   /* 计时器 overrun 计数;

POSIX.1b 计时器 */

intsi_timerid;   /* 计时器 ID; POSIX.1b 计时器 */

void    *si_addr;      /* 导致错误的内存地址 */

long     si_band;      /* Band event (在 glibc 2.3.2 和之前版本中是 int类型) */

intsi_fd;        /* 文件描述符 */

short    si_addr_lsb;  /* 地址的最不重要位

(自 Linux 2.6.32) */

void    *si_lower;     /* 出现地址违规的下限 (自 Linux 3.19) */

void    *si_upper;     /* 出现地址违规的上限 (自 Linux 3.19) */

intsi_pkey;      /* PTE 上导致错误的保护键 (自 Linux 4.6) */

void    *si_call_addr; /* 系统调用指令的地址

(自 Linux 3.5) */

intsi_syscall;   /* 系统调用尝试次数

(自 Linux 3.5) */

unsigned intsi_arch;  /* 尝试系统调用的架构

(自 Linux 3.5) */

}

我只需要使用 si_signo 就可以找到被发送的信号,使用 si_code 来获取更多关于信号的信息。放置这些代码的***位置是我们的 wait_for_signal 函数:

void debugger::wait_for_signal() {

intwait_status;

auto options = 0;

waitpid(m_pid, &wait_status, options);

auto siginfo = get_signal_info();

switch (siginfo.si_signo) {

caseSIGTRAP:

handle_sigtrap(siginfo);

break;

caseSIGSEGV:

std::cout <

break;

default:

std::cout <

}

}

现在再来处理 SIGTRAP。知道当***一个断点时会发送 SI_KERNEL 或 TRAP_BRKPT,而逐步执行结束时会发送 TRAP_TRACE 就足够了:

void debugger::handle_sigtrap(siginfo_t info) {

switch (info.si_code) {

//如果***了一个断点其中的一个会被设置

caseSI_KERNEL:

caseTRAP_BRKPT:

{

set_pc(get_pc()-1); //将程序计数器的值设置为它应该指向的地方

std::cout <

auto line_entry = get_line_entry_from_pc(get_pc());

print_source(line_entry->file->path, line_entry->line);

return;

}

//如果信号是由逐步执行发送的,这会被设置

caseTRAP_TRACE:

return;

default:

std::cout <

return;

}

}

这里有一大堆不同风格的信号你可以处理。查看 man sigaction 获取更多信息。

由于当我们收到 SIGTRAP 信号时我们已经修正了程序计数器的值,我们可以从 step_over_breakpoint 中移除这些代码,现在它看起来类似:

void debugger::step_over_breakpoint() {

if (m_breakpoints.count(get_pc())) {

auto& bp = m_breakpoints[get_pc()];

if (bp.is_enabled()) {

bp.disable();

ptrace(PTRACE_SINGLESTEP, m_pid, nullptr, nullptr);

wait_for_signal();

bp.enable();

}

}

}

测试

现在你应该可以在某个地址设置断点,运行程序然后看到打印出了源码,而且正在被执行的行被光标标记了出来。

后面我们会添加设置源码级别断点的功能。同时,你可以从这里获取该博文的代码。

【编辑推荐】

【责任编辑:庞桂玉 TEL:(010)68476606】

点赞 0

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值