Heim > System-Tutorial > LINUX > Eingehende Untersuchung der Haltepunkttechnologie auf Quellcodeebene in Linux-Debuggern!

Eingehende Untersuchung der Haltepunkttechnologie auf Quellcodeebene in Linux-Debuggern!

WBOY
Freigeben: 2024-01-01 21:59:42
nach vorne
921 Leute haben es durchsucht
Einführung Das Setzen von Haltepunkten für Speicheradressen ist nett, aber nicht das benutzerfreundlichste Tool. Wir möchten in der Lage sein, Haltepunkte für Quellcodezeilen und Funktionseintragsadressen festzulegen, damit wir auf derselben Abstraktionsebene wie der Code debuggen können.

In diesem Artikel werden Haltepunkte auf Quellebene zu unserem Debugger hinzugefügt. Mit all den Funktionen, die wir bereits unterstützen, ist dies viel einfacher, als es zunächst klingt. Wir werden auch einen Befehl hinzufügen, um den Typ und die Adresse eines Symbols abzurufen, was zum Auffinden von Code oder Daten und zum Verständnis von Verknüpfungskonzepten nützlich ist.

Serienindex

Mit der Veröffentlichung nachfolgender Artikel werden diese Links nach und nach wirksam.

  1. Bereiten Sie die Umgebung vor
  2. Haltepunkt
  3. Register und Speicher
  4. Elfen und Zwerge
  5. Quellcode und Signale
  6. Schritt-für-Schritt-Ausführung auf Quellcode-Ebene
  7. Haltepunkte auf Quellenebene
  8. Anrufstapel
  9. Variablen lesen
  10. Nächste Schritte
Haltepunkt ZWERG

Elfen und Zwerge Dieser Artikel beschreibt, wie DWARF-Debug-Informationen funktionieren und wie sie verwendet werden können, um Maschinencode einem High-Level-Quellcode zuzuordnen. Denken Sie daran, dass DWARF den Adressbereich einer Funktion und eine Zeilentabelle enthält, mit der Sie Codepositionen zwischen Abstraktionsschichten übersetzen können. Wir werden diese Funktionen verwenden, um unsere Haltepunkte zu implementieren.

Funktionseingang

Das Setzen von Haltepunkten für Funktionsnamen kann etwas kompliziert sein, wenn man an Überladung, Mitgliedsfunktionen usw. denkt, aber wir werden alle Kompilierungseinheiten durchgehen und nach Funktionen suchen, die dem gesuchten Namen entsprechen. Die DWARF-Informationen sehen so aus:

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

Nach dem Login kopieren

Wir möchten DW_AT_name abgleichen und DW_AT_low_pc (die Startadresse der Funktion) verwenden, um unseren Haltepunkt festzulegen.

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);
}
}
}
}
Nach dem Login kopieren

Das Einzige, was an diesem Code etwas seltsam aussieht, ist der ++-Eintrag. Das Problem besteht darin, dass DW_AT_low_pc der Funktion nicht auf die Startadresse des Benutzercodes der Funktion verweist, sondern auf den Anfang des Prologs. Der Compiler gibt normalerweise den Prolog und den Epilog einer Funktion aus, die zum Speichern und Wiederherstellen des Stapels, zum Manipulieren des Stapelzeigers usw. verwendet werden. Das ist für uns nicht sehr nützlich, deshalb erhöhen wir die Eingabezeile um eins, um die erste Zeile des Benutzercodes anstelle des Prologs zu erhalten. Die DWARF-Zeilentabelle verfügt tatsächlich über einige Funktionen zum Markieren des Eintrags als erste Zeile nach dem Funktionsprolog, aber nicht alle Compiler geben ihn aus, daher habe ich mich für den ursprünglichen Ansatz entschieden.

Quellcodezeile

Um einen Haltepunkt auf einer Quellzeile auf hoher Ebene festzulegen, müssen wir die Zeilennummer in eine Adresse in DWARF umwandeln. Wir durchlaufen die Kompilierungseinheiten und suchen nach einer, deren Name mit der angegebenen Datei übereinstimmt, und suchen dann nach dem Eintrag, der der angegebenen Zeile entspricht.

ZWERG sieht ungefähr so ​​aus:

.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
Nach dem Login kopieren

Wenn wir also einen Haltepunkt in Zeile 5 von ab.cpp setzen möchten, suchen wir nach dem Eintrag in Bezug auf Zeile (0x004004e3) und setzen einen Haltepunkt.

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;
}
}
}
}
}
Nach dem Login kopieren

Ich habe hier einen is_suffix-Hack gemacht, damit Sie c.cpp für a/b/c.cpp eingeben können. Natürlich sollten Sie tatsächlich eine Bibliothek zur Pfadbehandlung verwenden, bei der die Groß-/Kleinschreibung beachtet wird, aber ich bin faul. Entry.is_stmt prüft, ob der Zeilentabelleneintrag als Anfang einer Anweisung markiert ist, die vom Compiler basierend auf der Adresse festgelegt wird, die seiner Meinung nach das beste Ziel für den Haltepunkt ist.

Symbolsuche

Wenn wir uns auf der Objektdateiebene befinden, sind Symbole König. Funktionen werden mit Symbolen benannt, globale Variablen werden mit Symbolen benannt, Sie erhalten ein Symbol, wir erhalten ein Symbol, jeder erhält ein Symbol. In einer bestimmten Objektdatei verweisen einige Symbole möglicherweise auf andere Objektdateien oder gemeinsam genutzte Bibliotheken, und der Linker erstellt aus den Symbolverweisen ein ausführbares Programm.

Symbole können in einer entsprechend benannten Symboltabelle nachgeschlagen werden, die im ELF-Abschnitt der Binärdatei gespeichert ist. Glücklicherweise verfügt libelfin dafür über eine nette Schnittstelle, sodass wir uns nicht um den ganzen ELF-Kram selbst kümmern müssen. Um Ihnen eine Vorstellung davon zu geben, womit wir es zu tun haben, ist hier ein Dump des .symtab-Teils der Binärdatei, generiert von 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
Nach dem Login kopieren

In der Objektdatei können Sie viele Symbole sehen, die zum Einrichten der Umgebung verwendet werden, und schließlich können Sie das Hauptsymbol sehen.

Uns interessieren der Typ, der Name und der Wert (Adresse) des Symbols. Wir haben eine symbol_type-Aufzählung dieses Typs und verwenden einen std::string als Namen und std::uintptr_t als Adresse:

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;
};
Nach dem Login kopieren

Wir müssen den Symboltyp, den wir von libelfin erhalten, unserer Enumeration zuordnen, da wir nicht möchten, dass Abhängigkeiten diese Schnittstelle beschädigen. Glücklicherweise habe ich für alles den gleichen Namen gewählt, sodass es einfach war:

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;
}
};
Nach dem Login kopieren

Schließlich müssen wir nach Symbolen suchen. Zur Veranschaulichung durchlaufe ich den ELF-Teil der Symboltabelle und sammle alle Symbole, die ich dort finde, in einem std::vector. Eine intelligentere Implementierung könnte eine Zuordnung von Namen zu Symbolen ermöglichen, sodass Sie die Daten nur einmal betrachten müssen.

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;
}
Nach dem Login kopieren
添加命令

一如往常,我们需要添加一些更多的命令来向用户暴露功能。对于断点,我使用 GDB 风格的接口,其中断点类型是通过你传递的参数推断的,而不用要求显式切换:

  • 0x -> 断点地址
  • : -> 断点行号
  •  -> 断点函数名
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]);
}
}
Nach dem Login kopieren

对于符号,我们将查找符号并打印出我们发现的任何匹配项:

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;
}
}
Nach dem Login kopieren
测试一下

在一个简单的二进制文件上启动调试器,并设置源代码级别的断点。在一些 foo 函数上设置一个断点,看到我的调试器停在它上面是我这个项目最有价值的时刻之一。

符号查找可以通过在程序中添加一些函数或全局变量并查找它们的名称来进行测试。请注意,如果你正在编译 C++ 代码,你还需要考虑名称重整。

本文就这些了。下一次我将展示如何向调试器添加堆栈展开支持。

你可以在这里找到这篇文章的代码。

Das obige ist der detaillierte Inhalt vonEingehende Untersuchung der Haltepunkttechnologie auf Quellcodeebene in Linux-Debuggern!. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Quelle:linuxprobe.com
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage