開發(fā)一個(gè)Linux調(diào)試器(五):源碼和信號(hào)
在上一部分我們學(xué)習(xí)了關(guān)于 DWARF 的信息,以及它如何被用于讀取變量和將被執(zhí)行的機(jī)器碼與我們的高級(jí)語言的源碼聯(lián)系起來。在這一部分,我們將進(jìn)入實(shí)踐,實(shí)現(xiàn)一些我們調(diào)試器后面會(huì)使用的 DWARF 原語。我們也會(huì)利用這個(gè)機(jī)會(huì),使我們的調(diào)試器可以在***一個(gè)斷點(diǎn)時(shí)打印出當(dāng)前的源碼上下文。
系列文章索引
隨著后面文章的發(fā)布,這些鏈接會(huì)逐漸生效。
- 準(zhǔn)備環(huán)境
- 斷點(diǎn)
- 寄存器和內(nèi)存
- Elves 和 dwarves
- 源碼和信號(hào)
- 源碼級(jí)逐步執(zhí)行
- 源碼級(jí)斷點(diǎn)
- 調(diào)用棧展開
- 讀取變量
- 下一步
設(shè)置我們的 DWARF 解析器
正如我在這系列文章開始時(shí)備注的,我們會(huì)使用 libelfin 來處理我們的 DWARF 信息。希望你已經(jīng)在***部分設(shè)置好了這些,如果沒有的話,現(xiàn)在做吧,確保你使用我倉庫的 fbreg 分支。
一旦你構(gòu)建好了 libelfin,就可以把它添加到我們的調(diào)試器。***步是解析我們的 ELF 可執(zhí)行程序并從中提取 DWARF 信息。使用 libelfin 可以輕易實(shí)現(xiàn),只需要對(duì)調(diào)試器作以下更改:
- 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,因?yàn)?elf 加載器需要傳遞一個(gè) UNIX 文件描述符給 mmap,從而可以將文件映射到內(nèi)存而不是每次讀取一部分。
調(diào)試信息原語
下一步我們可以實(shí)現(xiàn)從程序計(jì)數(shù)器的值中提取行條目(line entry)以及函數(shù) DWARF 信息條目(function DIE)的函數(shù)。我們從 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"};
- }
這里我采用了樸素的方法,迭代遍歷編譯單元直到找到一個(gè)包含程序計(jì)數(shù)器的,然后迭代遍歷它的子節(jié)點(diǎn)直到我們找到相關(guān)函數(shù)(DW_TAG_subprogram)。正如我在上一篇中提到的,如果你想要的話你可以處理類似的成員函數(shù)或者內(nèi)聯(lián)等情況。
接下來是 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"};
- }
同樣,我們可以簡單地找到正確的編譯單元,然后查詢行表獲取相關(guān)的條目。
打印源碼
當(dāng)我們***一個(gè)斷點(diǎn)或者逐步執(zhí)行我們的代碼時(shí),我們會(huì)想知道處于源碼中的什么位置。
- void debugger::print_source(const std::string& file_name, unsigned line, unsigned n_lines_context) {
- std::ifstream file {file_name};
- //獲得一個(gè)所需行附近的窗口
- 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;
- }
- }
- //如果我們?cè)诋?dāng)前行則輸出光標(biāo)
- std::cout << (current_line==line ? "> " : " ");
- //輸出行直到 end_line
- while (current_line <= end_line && file.get(c)) {
- std::cout << c;
- if (c == '\n') {
- ++current_line;
- //如果我們?cè)诋?dāng)前行則輸出光標(biāo)
- std::cout << (current_line==line ? "> " : " ");
- }
- }
- //輸出換行確保恰當(dāng)?shù)厍蹇樟肆?nbsp;
- std::cout << std::endl;
- }
現(xiàn)在我們可以打印出源碼了,我們需要將這些通過鉤子添加到我們的調(diào)試器。實(shí)現(xiàn)這個(gè)的一個(gè)好地方是當(dāng)調(diào)試器從一個(gè)斷點(diǎn)或者(最終)逐步執(zhí)行得到一個(gè)信號(hào)時(shí)。到了這里,我們可能想要給我們的調(diào)試器添加一些更好的信號(hào)處理。
更好的信號(hào)處理
我們希望能夠得知什么信號(hào)被發(fā)送給了進(jìn)程,同樣我們也想知道它是如何產(chǎn)生的。例如,我們希望能夠得知是否由于***了一個(gè)斷點(diǎn)從而獲得一個(gè) SIGTRAP,還是由于逐步執(zhí)行完成、或者是產(chǎn)生了一個(gè)新線程等等導(dǎo)致的。幸運(yùn)的是,我們可以再一次使用 ptrace。可以給 ptrace 的一個(gè)命令是 PTRACE_GETSIGINFO,它會(huì)給你被發(fā)送給進(jìn)程的***一個(gè)信號(hào)的信息。我們類似這樣使用它:
- siginfo_t debugger::get_signal_info() {
- siginfo_t info;
- ptrace(PTRACE_GETSIGINFO, m_pid, nullptr, &info);
- return info;
- }
這會(huì)給我們一個(gè) siginfo_t 對(duì)象,它能提供以下信息:
- siginfo_t {
- int si_signo; /* 信號(hào)編號(hào) */
- int si_errno; /* errno 值 */
- int si_code; /* 信號(hào)代碼 */
- int si_trapno; /* 導(dǎo)致生成硬件信號(hào)的陷阱編號(hào)
- (大部分架構(gòu)中都沒有使用) */
- pid_t si_pid; /* 發(fā)送信號(hào)的進(jìn)程 ID */
- uid_t si_uid; /* 發(fā)送信號(hào)進(jìn)程的用戶 ID */
- int si_status; /* 退出值或信號(hào) */
- clock_t si_utime; /* 消耗的用戶時(shí)間 */
- clock_t si_stime; /* 消耗的系統(tǒng)時(shí)間 */
- sigval_t si_value; /* 信號(hào)值 */
- int si_int; /* POSIX.1b 信號(hào) */
- void *si_ptr; /* POSIX.1b 信號(hào) */
- int si_overrun; /* 計(jì)時(shí)器 overrun 計(jì)數(shù);
- POSIX.1b 計(jì)時(shí)器 */
- int si_timerid; /* 計(jì)時(shí)器 ID; POSIX.1b 計(jì)時(shí)器 */
- void *si_addr; /* 導(dǎo)致錯(cuò)誤的內(nèi)存地址 */
- long si_band; /* Band event (在 glibc 2.3.2 和之前版本中是 int 類型) */
- int si_fd; /* 文件描述符 */
- short si_addr_lsb; /* 地址的最不重要位
- (自 Linux 2.6.32) */
- void *si_lower; /* 出現(xiàn)地址違規(guī)的下限 (自 Linux 3.19) */
- void *si_upper; /* 出現(xiàn)地址違規(guī)的上限 (自 Linux 3.19) */
- int si_pkey; /* PTE 上導(dǎo)致錯(cuò)誤的保護(hù)鍵 (自 Linux 4.6) */
- void *si_call_addr; /* 系統(tǒng)調(diào)用指令的地址
- (自 Linux 3.5) */
- int si_syscall; /* 系統(tǒng)調(diào)用嘗試次數(shù)
- (自 Linux 3.5) */
- unsigned int si_arch; /* 嘗試系統(tǒng)調(diào)用的架構(gòu)
- (自 Linux 3.5) */
- }
我只需要使用 si_signo 就可以找到被發(fā)送的信號(hào),使用 si_code 來獲取更多關(guān)于信號(hào)的信息。放置這些代碼的***位置是我們的 wait_for_signal 函數(shù):
- 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;
- }
- }
現(xiàn)在再來處理 SIGTRAP。知道當(dāng)***一個(gè)斷點(diǎn)時(shí)會(huì)發(fā)送 SI_KERNEL 或 TRAP_BRKPT,而逐步執(zhí)行結(jié)束時(shí)會(huì)發(fā)送 TRAP_TRACE 就足夠了:
- void debugger::handle_sigtrap(siginfo_t info) {
- switch (info.si_code) {
- //如果***了一個(gè)斷點(diǎn)其中的一個(gè)會(huì)被設(shè)置
- case SI_KERNEL:
- case TRAP_BRKPT:
- {
- set_pc(get_pc()-1); //將程序計(jì)數(shù)器的值設(shè)置為它應(yīng)該指向的地方
- 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;
- }
- //如果信號(hào)是由逐步執(zhí)行發(fā)送的,這會(huì)被設(shè)置
- case TRAP_TRACE:
- return;
- default:
- std::cout << "Unknown SIGTRAP code " << info.si_code << std::endl;
- return;
- }
- }
這里有一大堆不同風(fēng)格的信號(hào)你可以處理。查看 man sigaction 獲取更多信息。
由于當(dāng)我們收到 SIGTRAP 信號(hào)時(shí)我們已經(jīng)修正了程序計(jì)數(shù)器的值,我們可以從 step_over_breakpoint 中移除這些代碼,現(xiàn)在它看起來類似:
- 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();
- }
- }
- }
測試
現(xiàn)在你應(yīng)該可以在某個(gè)地址設(shè)置斷點(diǎn),運(yùn)行程序然后看到打印出了源碼,而且正在被執(zhí)行的行被光標(biāo)標(biāo)記了出來。
后面我們會(huì)添加設(shè)置源碼級(jí)別斷點(diǎn)的功能。同時(shí),你可以從這里獲取該博文的代碼。