Linux中國

開發一個 Linux 調試器(七):源碼級斷點

在內存地址上設置斷點雖然不錯,但它並沒有提供最方便用戶的工具。我們希望能夠在源代碼行和函數入口地址上設置斷點,以便我們可以在與代碼相同的抽象級別中進行調試。

這篇文章將會添加源碼級斷點到我們的調試器中。通過所有我們已經支持的功能,這要比起最初聽起來容易得多。我們還將添加一個命令來獲取符號的類型和地址,這對於定位代碼或數據以及理解鏈接概念非常有用。

系列索引

隨著後面文章的發布,這些鏈接會逐漸生效。

  1. 準備環境
  2. 斷點
  3. 寄存器和內存
  4. Elves 和 dwarves
  5. 源碼和信號
  6. 源碼級逐步執行
  7. 源碼級斷點
  8. 調用棧
  9. 讀取變數
  10. 之後步驟

斷點

DWARF

Elves 和 dwarves 這篇文章,描述了 DWARF 調試信息是如何工作的,以及如何用它來將機器碼映射到高層源碼中。回想一下,DWARF 包含了函數的地址範圍和一個允許你在抽象層之間轉換代碼位置的行表。我們將使用這些功能來實現我們的斷點。

函數入口

如果你考慮重載、成員函數等等,那麼在函數名上設置斷點可能有點複雜,但是我們將遍歷所有的編譯單元,並搜索與我們正在尋找的名稱匹配的函數。DWARF 信息如下所示:

< 0><0x0000000b>  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               0x0040069c

LOCAL_SYMBOLS:
< 1><0x0000002e>    DW_TAG_subprogram
                      DW_AT_low_pc                0x00400670
                      DW_AT_high_pc               0x0040069c
                      DW_AT_name                  foo
                      ...
...
<14><0x000000b0>    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 prologue
                set_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 cu
Source lines (from CU-DIE at .debug_info offset 0x0000000b):

NS new statement, BB new basic block, ET end of text sequence
PE prologue end, EB epilogue begin
IS=val ISA number, DI=val discriminator value
<pc>        [lno,col] NS BB ET PE EB IS= DI= uri: "filepath"
0x004004a7  [   1, 0] NS uri: "/super/secret/path/a.hpp"
0x004004ab  [   2, 0] NS
0x004004b2  [   3, 0] NS
0x004004b9  [   4, 0] NS
0x004004c1  [   5, 0] NS
0x004004c3  [   1, 0] NS uri: "/super/secret/path/b.hpp"
0x004004c7  [   2, 0] NS
0x004004ce  [   3, 0] NS
0x004004d5  [   4, 0] NS
0x004004dd  [   5, 0] NS
0x004004df  [   4, 0] NS uri: "/super/secret/path/ab.cpp"
0x004004e3  [   5, 0] NS
0x004004e8  [   6, 0] NS
0x004004ed  [   7, 0] NS
0x004004f4  [   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 是檢查行表入口是否被標記為一個語句的開頭,這是由編譯器根據它認為是斷點的最佳目標的地址設置的。

符號查找

當我們在對象文件層時,符號是王者。函數用符號命名,全局變數用符號命名,你得到一個符號,我們得到一個符號,每個人都得到一個符號。 在給定的對象文件中,一些符號可能引用其他對象文件或共享庫,鏈接器將從符號引用創建一個可執行程序。

可以在正確命名的符號表中查找符號,它存儲在二進位文件的 ELF 部分中。幸運的是,libelfin 有一個不錯的介面來做這件事,所以我們不需要自己處理所有的 ELF 的事情。為了讓你知道我們在處理什麼,下面是一個二進位文件的 .symtab 部分的轉儲,它由 readelf 生成:

Num:    Value          Size Type    Bind   Vis      Ndx Name
 0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
 1: 0000000000400238     0 SECTION LOCAL  DEFAULT    1
 2: 0000000000400254     0 SECTION LOCAL  DEFAULT    2
 3: 0000000000400278     0 SECTION LOCAL  DEFAULT    3
 4: 00000000004002c8     0 SECTION LOCAL  DEFAULT    4
 5: 0000000000400430     0 SECTION LOCAL  DEFAULT    5
 6: 00000000004004e4     0 SECTION LOCAL  DEFAULT    6
 7: 0000000000400508     0 SECTION LOCAL  DEFAULT    7
 8: 0000000000400528     0 SECTION LOCAL  DEFAULT    8
 9: 0000000000400558     0 SECTION LOCAL  DEFAULT    9
10: 0000000000400570     0 SECTION LOCAL  DEFAULT   10
11: 0000000000400714     0 SECTION LOCAL  DEFAULT   11
12: 0000000000400720     0 SECTION LOCAL  DEFAULT   12
13: 0000000000400724     0 SECTION LOCAL  DEFAULT   13
14: 0000000000400750     0 SECTION LOCAL  DEFAULT   14
15: 0000000000600e18     0 SECTION LOCAL  DEFAULT   15
16: 0000000000600e20     0 SECTION LOCAL  DEFAULT   16
17: 0000000000600e28     0 SECTION LOCAL  DEFAULT   17
18: 0000000000600e30     0 SECTION LOCAL  DEFAULT   18
19: 0000000000600ff0     0 SECTION LOCAL  DEFAULT   19
20: 0000000000601000     0 SECTION LOCAL  DEFAULT   20
21: 0000000000601018     0 SECTION LOCAL  DEFAULT   21
22: 0000000000601028     0 SECTION LOCAL  DEFAULT   22
23: 0000000000000000     0 SECTION LOCAL  DEFAULT   23
24: 0000000000000000     0 SECTION LOCAL  DEFAULT   24
25: 0000000000000000     0 SECTION LOCAL  DEFAULT   25
26: 0000000000000000     0 SECTION LOCAL  DEFAULT   26
27: 0000000000000000     0 SECTION LOCAL  DEFAULT   27
28: 0000000000000000     0 SECTION LOCAL  DEFAULT   28
29: 0000000000000000     0 SECTION LOCAL  DEFAULT   29
30: 0000000000000000     0 SECTION LOCAL  DEFAULT   30
31: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS init.c
32: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
33: 0000000000600e28     0 OBJECT  LOCAL  DEFAULT   17 __JCR_LIST__
34: 00000000004005a0     0 FUNC    LOCAL  DEFAULT   10 deregister_tm_clones
35: 00000000004005e0     0 FUNC    LOCAL  DEFAULT   10 register_tm_clones
36: 0000000000400620     0 FUNC    LOCAL  DEFAULT   10 __do_global_dtors_aux
37: 0000000000601028     1 OBJECT  LOCAL  DEFAULT   22 completed.6917
38: 0000000000600e20     0 OBJECT  LOCAL  DEFAULT   16 __do_global_dtors_aux_fin
39: 0000000000400640     0 FUNC    LOCAL  DEFAULT   10 frame_dummy
40: 0000000000600e18     0 OBJECT  LOCAL  DEFAULT   15 __frame_dummy_init_array_
41: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS /super/secret/path/MiniDbg/
42: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crtstuff.c
43: 0000000000400818     0 OBJECT  LOCAL  DEFAULT   14 __FRAME_END__
44: 0000000000600e28     0 OBJECT  LOCAL  DEFAULT   17 __JCR_END__
45: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS
46: 0000000000400724     0 NOTYPE  LOCAL  DEFAULT   13 __GNU_EH_FRAME_HDR
47: 0000000000601000     0 OBJECT  LOCAL  DEFAULT   20 _GLOBAL_OFFSET_TABLE_
48: 0000000000601028     0 OBJECT  LOCAL  DEFAULT   21 __TMC_END__
49: 0000000000601020     0 OBJECT  LOCAL  DEFAULT   21 __dso_handle
50: 0000000000600e20     0 NOTYPE  LOCAL  DEFAULT   15 __init_array_end
51: 0000000000600e18     0 NOTYPE  LOCAL  DEFAULT   15 __init_array_start
52: 0000000000600e30     0 OBJECT  LOCAL  DEFAULT   18 _DYNAMIC
53: 0000000000601018     0 NOTYPE  WEAK   DEFAULT   21 data_start
54: 0000000000400710     2 FUNC    GLOBAL DEFAULT   10 __libc_csu_fini
55: 0000000000400570    43 FUNC    GLOBAL DEFAULT   10 _start
56: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
57: 0000000000400714     0 FUNC    GLOBAL DEFAULT   11 _fini
58: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@@GLIBC_
59: 0000000000400720     4 OBJECT  GLOBAL DEFAULT   12 _IO_stdin_used
60: 0000000000601018     0 NOTYPE  GLOBAL DEFAULT   21 __data_start
61: 00000000004006a0   101 FUNC    GLOBAL DEFAULT   10 __libc_csu_init
62: 0000000000601028     0 NOTYPE  GLOBAL DEFAULT   22 __bss_start
63: 0000000000601030     0 NOTYPE  GLOBAL DEFAULT   22 _end
64: 0000000000601028     0 NOTYPE  GLOBAL DEFAULT   21 _edata
65: 0000000000400670    44 FUNC    GLOBAL DEFAULT   10 main
66: 0000000000400558     0 FUNC    GLOBAL DEFAULT    9 _init

你可以在對象文件中看到用於設置環境的很多符號,最後還可以看到 main 符號。

我們對符號的類型、名稱和值(地址)感興趣。我們有一個該類型的 symbol_type 枚舉,並使用一個 std::string 作為名稱,std::uintptr_t 作為地址:

enum class symbol_type {
    notype,            // No type (e.g., absolute symbol)
    object,            // Data object
    func,              // Function entry point
    section,           // Symbol is associated with a section
    file,              // Source file associated with the
};                     // object file

std::string to_string (symbol_type st) {
    switch (st) {
    case symbol_type::notype: return "notype";
    case symbol_type::object: return "object";
    case symbol_type::func: return "func";
    case symbol_type::section: return "section";
    case symbol_type::file: return "file";
    }
}

struct symbol {
    symbol_type type;
    std::string name;
    std::uintptr_t addr;
};

我們需要將從 libelfin 獲得的符號類型映射到我們的枚舉,因為我們不希望依賴關係破環這個介面。幸運的是,我為所有的東西選了同樣的名字,所以這樣很簡單:

symbol_type to_symbol_type(elf::stt sym) {
    switch (sym) {
    case elf::stt::notype: return symbol_type::notype;
    case elf::stt::object: return symbol_type::object;
    case elf::stt::func: return symbol_type::func;
    case elf::stt::section: return symbol_type::section;
    case elf::stt::file: return symbol_type::file;
    default: return symbol_type::notype;
    }
};

最後我們要查找符號。為了說明的目的,我循環查找符號表的 ELF 部分,然後收集我在其中找到的任意符號到 std::vector 中。更智能的實現可以建立從名稱到符號的映射,這樣你只需要查看一次數據就行了。

std::vector<symbol> debugger::lookup_symbol(const std::string& name) {
    std::vector<symbol> syms;

    for (auto &sec : m_elf.sections()) {
        if (sec.get_hdr().type != elf::sht::symtab && sec.get_hdr().type != elf::sht::dynsym)
            continue;

        for (auto sym : sec.as_symtab()) {
            if (sym.get_name() == name) {
                auto &d = sym.get_data();
                syms.push_back(symbol{to_symbol_type(d.type()), sym.get_name(), d.value});
            }
        }
    }

    return syms;
}

添加命令

一如往常,我們需要添加一些更多的命令來向用戶暴露功能。對於斷點,我使用 GDB 風格的介面,其中斷點類型是通過你傳遞的參數推斷的,而不用要求顯式切換:

  • 0x<hexadecimal> -> 斷點地址
  • <line>:<filename> -> 斷點行號
  • <anything else> -> 斷點函數名
    else if(is_prefix(command, "break")) {
        if (args[1][0] == &apos;0&apos; && args[1][1] == &apos;x&apos;) {
            std::string addr {args[1], 2};
            set_breakpoint_at_address(std::stol(addr, 0, 16));
        }
        else if (args[1].find(&apos;:&apos;) != std::string::npos) {
            auto file_and_line = split(args[1], &apos;:&apos;);
            set_breakpoint_at_source_line(file_and_line[0], std::stoi(file_and_line[1]));
        }
        else {
            set_breakpoint_at_function(args[1]);
        }
    }

對於符號,我們將查找符號並列印出我們發現的任何匹配項:

else if(is_prefix(command, "symbol")) {
    auto syms = lookup_symbol(args[1]);
    for (auto&& s : syms) {
        std::cout << s.name << &apos; &apos; << to_string(s.type) << " 0x" << std::hex << s.addr << std::endl;
    }
}

測試一下

在一個簡單的二進位文件上啟動調試器,並設置源代碼級別的斷點。在一些 foo 函數上設置一個斷點,看到我的調試器停在它上面是我這個項目最有價值的時刻之一。

符號查找可以通過在程序中添加一些函數或全局變數並查找它們的名稱來進行測試。請注意,如果你正在編譯 C++ 代碼,你還需要考慮名稱重整

本文就這些了。下一次我將展示如何向調試器添加堆棧展開支持。

你可以在這裡找到這篇文章的代碼。

via: https://blog.tartanllama.xyz/c++/2017/06/19/writing-a-linux-debugger-source-break/

作者:Simon Brand 譯者:geekpi 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國