開發(fā)一個(gè)Linux調(diào)試器(七):源碼級(jí)斷點(diǎn)
在內(nèi)存地址上設(shè)置斷點(diǎn)雖然不錯(cuò),但它并沒有提供最方便用戶的工具。我們希望能夠在源代碼行和函數(shù)入口地址上設(shè)置斷點(diǎn),以便我們可以在與代碼相同的抽象級(jí)別中進(jìn)行調(diào)試。
這篇文章將會(huì)添加源碼級(jí)斷點(diǎn)到我們的調(diào)試器中。通過所有我們已經(jīng)支持的功能,這要比起最初聽起來容易得多。我們還將添加一個(gè)命令來獲取符號(hào)的類型和地址,這對(duì)于定位代碼或數(shù)據(jù)以及理解鏈接概念非常有用。
系列索引
隨著后面文章的發(fā)布,這些鏈接會(huì)逐漸生效。
- 準(zhǔn)備環(huán)境
- 斷點(diǎn)
- 寄存器和內(nèi)存
- Elves 和 dwarves
- 源碼和信號(hào)
- 源碼級(jí)逐步執(zhí)行
- 源碼級(jí)斷點(diǎn)
- 調(diào)用棧
- 讀取變量
- 之后步驟
斷點(diǎn)
DWARF
Elves 和 dwarves 這篇文章,描述了 DWARF 調(diào)試信息是如何工作的,以及如何用它來將機(jī)器碼映射到高層源碼中。回想一下,DWARF 包含了函數(shù)的地址范圍和一個(gè)允許你在抽象層之間轉(zhuǎn)換代碼位置的行表。我們將使用這些功能來實(shí)現(xiàn)我們的斷點(diǎn)。
函數(shù)入口
如果你考慮重載、成員函數(shù)等等,那么在函數(shù)名上設(shè)置斷點(diǎn)可能有點(diǎn)復(fù)雜,但是我們將遍歷所有的編譯單元,并搜索與我們正在尋找的名稱匹配的函數(shù)。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(函數(shù)的起始地址)來設(shè)置我們的斷點(diǎn)。
- 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);
- }
- }
- }
- }
這代碼看起來有點(diǎn)奇怪的唯一一點(diǎn)是 ++entry。 問題是函數(shù)的 DW_AT_low_pc 不指向該函數(shù)的用戶代碼的起始地址,它指向 prologue 的開始。編譯器通常會(huì)輸出一個(gè)函數(shù)的 prologue 和 epilogue,它們用于執(zhí)行保存和恢復(fù)堆棧、操作堆棧指針等。這對(duì)我們來說不是很有用,所以我們將入口行加一來獲取用戶代碼的***行而不是 prologue。DWARF 行表實(shí)際上具有一些功能,用于將入口標(biāo)記為函數(shù) prologue 之后的***行,但并不是所有編譯器都輸出它,因此我采用了原始的方法。
源碼行
要在高層源碼行上設(shè)置一個(gè)斷點(diǎn),我們要將這個(gè)行號(hào)轉(zhuǎn)換成 DWARF 中的一個(gè)地址。我們將遍歷編譯單元,尋找一個(gè)名稱與給定文件匹配的編譯單元,然后查找與給定行對(duì)應(yīng)的入口。
DWARF 看上去有點(diǎn)像這樣:
- .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 的第五行設(shè)置一個(gè)斷點(diǎn),我們將查找與行 (0x004004e3) 相關(guān)的入口并設(shè)置一個(gè)斷點(diǎn)。
- 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 。當(dāng)然你實(shí)際上應(yīng)該使用大小寫敏感路徑處理庫(kù)或者其它東西,但是我比較懶。entry.is_stmt 是檢查行表入口是否被標(biāo)記為一個(gè)語(yǔ)句的開頭,這是由編譯器根據(jù)它認(rèn)為是斷點(diǎn)的***目標(biāo)的地址設(shè)置的。
符號(hào)查找
當(dāng)我們?cè)趯?duì)象文件層時(shí),符號(hào)是王者。函數(shù)用符號(hào)命名,全局變量用符號(hào)命名,你得到一個(gè)符號(hào),我們得到一個(gè)符號(hào),每個(gè)人都得到一個(gè)符號(hào)。 在給定的對(duì)象文件中,一些符號(hào)可能引用其他對(duì)象文件或共享庫(kù),鏈接器將從符號(hào)引用創(chuàng)建一個(gè)可執(zhí)行程序。
可以在正確命名的符號(hào)表中查找符號(hào),它存儲(chǔ)在二進(jìn)制文件的 ELF 部分中。幸運(yùn)的是,libelfin 有一個(gè)不錯(cuò)的接口來做這件事,所以我們不需要自己處理所有的 ELF 的事情。為了讓你知道我們?cè)谔幚硎裁?,下面是一個(gè)二進(jìn)制文件的 .symtab 部分的轉(zhuǎn)儲(chǔ),它由 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
你可以在對(duì)象文件中看到用于設(shè)置環(huán)境的很多符號(hào),***還可以看到 main 符號(hào)。
我們對(duì)符號(hào)的類型、名稱和值(地址)感興趣。我們有一個(gè)該類型的 symbol_type 枚舉,并使用一個(gè) 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 獲得的符號(hào)類型映射到我們的枚舉,因?yàn)槲覀儾幌M蕾囮P(guān)系破環(huán)這個(gè)接口。幸運(yùn)的是,我為所有的東西選了同樣的名字,所以這樣很簡(jiǎn)單:
- 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;
- }
- };
***我們要查找符號(hào)。為了說明的目的,我循環(huán)查找符號(hào)表的 ELF 部分,然后收集我在其中找到的任意符號(hào)到 std::vector 中。更智能的實(shí)現(xiàn)可以建立從名稱到符號(hào)的映射,這樣你只需要查看一次數(shù)據(jù)就行了。
- 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;
- }
添加命令
一如往常,我們需要添加一些更多的命令來向用戶暴露功能。對(duì)于斷點(diǎn),我使用 GDB 風(fēng)格的接口,其中斷點(diǎn)類型是通過你傳遞的參數(shù)推斷的,而不用要求顯式切換:
- 0x<hexadecimal> -> 斷點(diǎn)地址
- <line>:<filename> -> 斷點(diǎn)行號(hào)
- <anything else> -> 斷點(diǎn)函數(shù)名
- else if(is_prefix(command, "break")) {
- if (args[1][0] == '0' && args[1][1] == 'x') {
- std::string addr {args[1], 2};
- set_breakpoint_at_address(std::stol(addr, 0, 16));
- }
- else if (args[1].find(':') != std::string::npos) {
- auto file_and_line = split(args[1], ':');
- set_breakpoint_at_source_line(file_and_line[0], std::stoi(file_and_line[1]));
- }
- else {
- set_breakpoint_at_function(args[1]);
- }
- }
對(duì)于符號(hào),我們將查找符號(hào)并打印出我們發(fā)現(xiàn)的任何匹配項(xiàng):
- else if(is_prefix(command, "symbol")) {
- auto syms = lookup_symbol(args[1]);
- for (auto&& s : syms) {
- std::cout << s.name << ' ' << to_string(s.type) << " 0x" << std::hex << s.addr << std::endl;
- }
- }
測(cè)試一下
在一個(gè)簡(jiǎn)單的二進(jìn)制文件上啟動(dòng)調(diào)試器,并設(shè)置源代碼級(jí)別的斷點(diǎn)。在一些 foo 函數(shù)上設(shè)置一個(gè)斷點(diǎn),看到我的調(diào)試器停在它上面是我這個(gè)項(xiàng)目最有價(jià)值的時(shí)刻之一。
符號(hào)查找可以通過在程序中添加一些函數(shù)或全局變量并查找它們的名稱來進(jìn)行測(cè)試。請(qǐng)注意,如果你正在編譯 C++ 代碼,你還需要考慮名稱重整。
本文就這些了。下一次我將展示如何向調(diào)試器添加堆棧展開支持。
你可以在這里找到這篇文章的代碼。