Rumah  >  Artikel  >  Tutorial sistem  >  Penerokaan mendalam teknologi titik putus tahap kod sumber dalam penyahpepijat Linux!

Penerokaan mendalam teknologi titik putus tahap kod sumber dalam penyahpepijat Linux!

WBOY
WBOYke hadapan
2024-01-01 21:59:42897semak imbas
Pengenalan Menetapkan titik putus pada alamat memori adalah bagus, tetapi ia tidak menyediakan alat yang paling mesra pengguna. Kami mahu dapat menetapkan titik putus pada baris kod sumber dan alamat entri fungsi supaya kami boleh nyahpepijat pada tahap abstraksi 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 didengari 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

Pautan ini akan berkuat kuasa secara beransur-ansur apabila artikel berikutnya diterbitkan.

  1. Sediakan persekitaran
  2. Titik putus
  3. Daftar dan Memori
  4. Bunian dan kerdil
  5. Kod sumber dan isyarat
  6. Pelaksanaan langkah demi langkah pada tahap kod sumber
  7. Titik putus tahap sumber
  8. Timbunan panggilan
  9. Baca pembolehubah
  10. Langkah seterusnya
Titik putus KERDIL

Bunian dan kerdil Artikel ini menerangkan cara maklumat nyahpepijat DWARF berfungsi dan cara ia boleh digunakan untuk memetakan kod mesin kepada kod sumber peringkat tinggi. Ingat bahawa DWARF mengandungi julat alamat fungsi dan jadual baris yang membolehkan anda menterjemah lokasi kod antara lapisan abstraksi. Kami akan menggunakan fungsi ini untuk melaksanakan titik putus kami.

Fungsi pintu masuk

Menetapkan titik putus pada nama fungsi boleh menjadi agak rumit jika anda berfikir tentang lebihan beban, fungsi ahli, dsb., tetapi kami akan meneliti semua unit kompilasi dan mencari fungsi yang sepadan dengan nama yang kami cari. Maklumat DWARF kelihatan seperti ini:

< 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
                      ...

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 melakukan penyimpanan dan memulihkan tindanan, memanipulasi penuding 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 pendekatan asal.

Baris kod sumber

Untuk menetapkan titik putus pada talian 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;
}
}
}
}
}

Saya melakukan penggodaman is_suffix di sini supaya anda boleh menaip c.cpp untuk a/b/c.cpp. Sudah tentu anda sebenarnya harus menggunakan perpustakaan pengendalian laluan sensitif huruf besar atau sesuatu, tetapi saya malas. entry.is_stmt menyemak sama ada entri jadual baris ditandakan sebagai permulaan pernyataan, yang ditetapkan oleh pengkompil berdasarkan alamat yang difikirkan sebagai sasaran terbaik untuk titik putus.

Pencarian simbol

Apabila kita berada di peringkat fail objek, simbol adalah raja. Fungsi dinamakan dengan simbol, pembolehubah global dinamakan dengan simbol, anda mendapat simbol, kami mendapat simbol, semua orang mendapat simbol. Dalam fail objek tertentu, beberapa simbol mungkin merujuk fail objek lain atau perpustakaan kongsi, dan pemaut akan mencipta atur cara boleh laku daripada rujukan simbol.

Simbol boleh dicari dalam jadual simbol yang dinamakan dengan betul, yang disimpan dalam bahagian ELF pada fail binari. Nasib baik, libelfin mempunyai antara muka yang bagus untuk melakukan ini, jadi kami tidak perlu mengendalikan sendiri semua perkara ELF. Untuk memberi anda gambaran tentang perkara yang kami hadapi, berikut ialah longgokan bahagian .symtab binari, yang dijana oleh 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

Anda boleh melihat banyak simbol yang digunakan untuk menyediakan persekitaran dalam fail objek, dan akhirnya anda boleh melihat simbol utama.

Kami berminat dengan jenis simbol, nama dan nilai (alamat). Kami mempunyai enum symbol_type jenis itu, dan gunakan std::string sebagai nama dan std::uintptr_t sebagai alamat:

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;
};

Kami perlu memetakan jenis simbol yang kami dapat daripada libelfin ke enum kami, kerana kami tidak mahu kebergantungan memecahkan antara muka ini. Nasib baik saya memilih nama yang sama untuk semuanya jadi mudah:

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;
}
};

Akhirnya kita kena cari simbol. Untuk tujuan ilustrasi, saya melingkari bahagian ELF pada jadual simbol dan mengumpul sebarang simbol yang saya temui di sana ke dalam std::vector. Pelaksanaan yang lebih bijak boleh mewujudkan pemetaan dari nama ke simbol supaya anda hanya perlu melihat data sekali sahaja.

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 风格的接口,其中断点类型是通过你传递的参数推断的,而不用要求显式切换:

  • 0xa89ebccb7e59f823059381ddb62fc1de -> 断点地址
  • 15a73cc5312745b1b00671f6e690e36a:2334ac29606bf8a170583e4f7533b1f4 -> 断点行号
  • a174120acd7c32eac8380378272c5779 -> 断点函数名
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 Penerokaan mendalam teknologi titik putus tahap kod sumber dalam penyahpepijat Linux!. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan:
Artikel ini dikembalikan pada:linuxprobe.com. Jika ada pelanggaran, sila hubungi admin@php.cn Padam