Menetapkan titik putus pada alamat memori video nampaknya bagus, tetapi ia tidak menyediakan alat yang paling mesra pengguna. Kami berharap dapat juga menetapkan titik putus pada baris kod sumber dan alamat kemasukan fungsi supaya kami boleh nyahpepijat pada tahap kekonkretan yang sama seperti kod.
Artikel ini akan menambah titik putus peringkat sumber pada penyahpepijat kami. Dengan semua ciri yang telah kami sokong, ini lebih mudah daripada yang kelihatan pada mulanya. Kami juga akan menambah arahan untuk mendapatkan jenis dan alamat simbol, yang berguna untuk mencari kod atau data dan memahami konsep pemautan.
Indeks Siri
Dengan keluaran artikel terdahulu, pautan ini akan berkuat kuasa secara beransur-ansur.
Sediakan daftar titik putus persekitaran dan ingatan video Bunian dan kerdil kod sumber dan tahap kod sumber isyarat untuk melaksanakan timbunan panggilan titik putus tahap kod sumber dan baca langkah pembolehubah berikut
Titik putus
KERDIL
Bunian dan kerdil Artikel ini menerangkan cara maklumat penyahpepijatan DWARF berfungsi dan cara menggunakannya untuk memetakan kod mesin kepada kod sumber peringkat tinggi. Ingat bahawa DWARF mengandungi julat alamat fungsi dan jadual baris yang membolehkan anda menukar lokasi kod antara lapisan perwakilan. Kami akan menggunakan fungsi ini untuk melaksanakan titik putus kami.
Entri fungsi
Jika anda menganggap prinsip sistem pengendalian Linux seperti lebihan beban, fungsi ahli, dll., menetapkan titik putus pada nama fungsi boleh menjadi agak rumit, dan kami akan merentasi semua unit kompilasi dan mencari fungsi yang sepadan dengan nama yang kami cari. Maklumat DWARF adalah seperti berikut:
DW_TAG_compile_unit DW_AT_producerclang version 3.9.1 (tags/RELEASE_391/final) DW_AT_languageDW_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_pc0x00400670 DW_AT_high_pc 0x0040069c LOCAL_SYMBOLS: DW_TAG_subprogram DW_AT_low_pc0x00400670 DW_AT_high_pc 0x0040069c DW_AT_namefoo ... ... DW_TAG_subprogram DW_AT_low_pc0x00400700 DW_AT_high_pc 0x004007a0 DW_AT_namebar ...
Kami ingin memadankan DW_AT_name dan menggunakan DW_AT_low_pc (alamat permulaan fungsi) untuk menetapkan titik putus kami.
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); } } } }
Satu-satunya perkara yang kelihatan agak pelik tentang kod ini ialah kemasukan ++. Masalahnya ialah DW_AT_low_pc fungsi tidak menunjuk ke alamat permulaan kod pengguna untuk fungsi tersebut, ia menunjuk ke permulaan prolog. Pengkompil biasanya mengeluarkan prolog dan epilog fungsi, yang digunakan untuk menyimpan dan memulihkan tindanan, mengendalikan penuding jadual tindanan, dsb. Ini tidak begitu berguna kepada kami, jadi kami menambah baris masukan dengan satu untuk mendapatkan baris pertama kod pengguna dan bukannya prolog. Jadual baris DWARF sebenarnya mempunyai beberapa fungsi untuk menandakan entri sebagai baris pertama selepas prolog fungsi, tetapi tidak semua penyusun mengeluarkannya, jadi saya menggunakan cara asal.
Barisan kod sumber
Untuk menetapkan titik putus pada baris kod sumber peringkat tinggi, kita perlu menukar nombor talian kepada alamat dalam DWARF. Kami akan beralih melalui unit kompilasi, mencari satu yang namanya sepadan dengan fail yang diberikan, dan kemudian mencari entri yang sepadan dengan baris yang diberikan.
DWARF kelihatan seperti ini:
.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 [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
Jadi jika kita ingin menetapkan titik putus pada baris 5 ab.cpp, kita akan mencari entri yang berkaitan dengan baris (0x004004e3) dan menetapkan titik putus.
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_suffixhack,这样你可以输入c.cpp代表a/b/c.cpp。其实你实际上应当使用大小写敏感路径处理库或则其它东西,而且我比较懒。entry.is_stmt是检测行表入口是否被标记为一个句子的开头,这是由编译器按照它觉得是断点的最佳目标的地址设置的。
符号查找
当我们在对象文件层时,符号是王者。函数用符号命名红旗linux系统,全局变量用符号命名,你得到一个符号,我们得到一个符号,每位人都得到一个符号。在给定的对象文件中linux vector 头文件,一些符号可能引用其他对象文件或共享库,链接器将从符号引用创建一个可执行程序。
可以在正确命名的符号表中查找符号,它储存在二补码文件的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 debugger::lookup_symbol(const std::string& name) { std::vector 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风格的插口linux vector 头文件,其中断点类型是通过你传递的参数推论的,而不用要求显式切换:
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]); } }
对于符号,我们将查找符号并复印出我们发觉的任何匹配项:
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; } }
测试一下
在一个简单的二补码文件上启动调试器,并设置源代码级别的断点。在一些foo函数上设置一个断点,见到我的调试器停在它前面是我这个项目最有价值的时刻之一。
符号查找可以通过在程序中添加一些函数或全局变量并查找它们的名称来进行测试。请注意,假若你正在编译C++代码,你还须要考虑名称重整。
Atas ialah kandungan terperinci Sediakan daftar persekitaran dan ingatan Elves dan dwarf kod sumber dan tahap kod sumber isyarat untuk pelaksanaan langkah demi langkah. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!