如何利用linux调试器打印源码?

2017-08-29 18:10:00
Angela
转贴
161

在这一部分,我们将进入实践,实现一些我们调试器后面会使用的 DWARF 原语。我们也会利用这个机会,使我们的调试器可以在命中一个断点时打印出当前的源码上下文。

-- Simon Brand


本文导航


◈ -设置我们的 DWARF 解析器                                                                                                            10%

◈ -调试信息原语                                                                                                                                    21%

◈ -打印源码                                                                                                                                            36%

◈ -更好的信号处理                                                                                                                                 49%

◈ -测试                                                                                                                                                    92%



设置我们的 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 &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 {
                    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 <= n_lines_context ? 1 : line - n_lines_context;
        auto end_line = line + n_lines_context + (line < n_lines_context ? n_lines_context - line : 0) + 1;

        char c{};
        auto current_line = 1u;
        //跳过 start_line 之前的行
        while (current_line != start_line && file.get(c)) {
            if (c == '\n') {
                ++current_line;
            }
        }

        //如果我们在当前行则输出光标
        std::cout << (current_line==line ? "> " : "  ");

        //输出行直到 end_line
        while (current_line <= end_line && file.get(c)) {
            std::cout << c;
            if (c == '\n') {
                ++current_line;
                //如果我们在当前行则输出光标
                std::cout << (current_line==line ? "> " : "  ");
            }
        }

        //输出换行确保恰当地清空了流
        std::cout << std::endl;
    }


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

更好的信号处理

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


    siginfo_t debugger::get_signal_info() {
        siginfo_t info;
        ptrace(PTRACE_GETSIGINFO, m_pid, nullptr, &info);
        return info;
    }
这会给我们一个 siginfo_t 对象,它能提供以下信息:


    siginfo_t {
        int      si_signo;     /* 信号编号 */
        int      si_errno;     /* errno 值 */
        int      si_code;      /* 信号代码 */
        int      si_trapno;    /* 导致生成硬件信号的陷阱编号
                                  (大部分架构中都没有使用) */
        pid_t    si_pid;       /* 发送信号的进程 ID */
        uid_t    si_uid;       /* 发送信号进程的用户 ID */
        int      si_status;    /* 退出值或信号 */
        clock_t  si_utime;     /* 消耗的用户时间 */
        clock_t  si_stime;     /* 消耗的系统时间 */
        sigval_t si_value;     /* 信号值 */
        int      si_int;       /* POSIX.1b 信号 */
        void    *si_ptr;       /* POSIX.1b 信号 */
        int      si_overrun;   /* 计时器 overrun 计数;
                                  POSIX.1b 计时器 */
        int      si_timerid;   /* 计时器 ID; POSIX.1b 计时器 */
        void    *si_addr;      /* 导致错误的内存地址 */
        long     si_band;      /* Band event (在 glibc 2.3.2 和之前版本中是 int 类型) */
        int      si_fd;        /* 文件描述符 */
        short    si_addr_lsb;  /* 地址的最不重要位
                                  (自 Linux 2.6.32) */
        void    *si_lower;     /* 出现地址违规的下限 (自 Linux 3.19) */
        void    *si_upper;     /* 出现地址违规的上限 (自 Linux 3.19) */
        int      si_pkey;      /* PTE 上导致错误的保护键 (自 Linux 4.6) */
        void    *si_call_addr; /* 系统调用指令的地址
                                  (自 Linux 3.5) */
        int      si_syscall;   /* 系统调用尝试次数
                                  (自 Linux 3.5) */
        unsigned int si_arch;  /* 尝试系统调用的架构
                                  (自 Linux 3.5) */
    }
我只需要使用 si_signo 就可以找到被发送的信号,使用 si_code 来获取更多关于信号的信息。放置这些代码的最好位置是我们的 wait_for_signal 函数:


    void debugger::wait_for_signal() {
        int wait_status;
        auto options = 0;
        waitpid(m_pid, &wait_status, options);

        auto siginfo = get_signal_info();

        switch (siginfo.si_signo) {
        case SIGTRAP:
            handle_sigtrap(siginfo);
            break;
        case SIGSEGV:
            std::cout << "Yay, segfault. Reason: " << siginfo.si_code << std::endl;
            break;
        default:
            std::cout << "Got signal " << strsignal(siginfo.si_signo) << std::endl;
        }
    }
现在再来处理 SIGTRAP。知道当命中一个断点时会发送 SI_KERNEL 或 TRAP_BRKPT,而逐步执行结束时会发送 TRAP_TRACE 就足够了:


    void debugger::handle_sigtrap(siginfo_t info) {
        switch (info.si_code) {
        //如果命中了一个断点其中的一个会被设置
        case SI_KERNEL:
        case TRAP_BRKPT:
        {
            set_pc(get_pc()-1); //将程序计数器的值设置为它应该指向的地方
            std::cout << "Hit breakpoint at address 0x" << std::hex << get_pc() << std::endl;
            auto line_entry = get_line_entry_from_pc(get_pc());
            print_source(line_entry->file->path, line_entry->line);
            return;
        }
        //如果信号是由逐步执行发送的,这会被设置
        case TRAP_TRACE:
            return;
        default:
            std::cout << "Unknown SIGTRAP code " << info.si_code << std::endl;
            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();
            }
        }
    }


测试

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

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




via: https://blog.tartanllama.xyz/c++/2017/04/24/writing-a-linux-debugger-source-signal/

作者:TartanLlama 译者:ictlyh 校对:wxy


发表评论
评论通过审核后显示。
联系我们