Maison Tutoriel système Linux Exploration approfondie de la technologie des points d'arrêt au niveau du code source dans les débogueurs Linux !

Exploration approfondie de la technologie des points d'arrêt au niveau du code source dans les débogueurs Linux !

Jan 01, 2024 pm 09:59 PM
linux linux教程 红帽 linux系统 linux命令 certification Linux chapeau rouge Linux vidéo Linux

Présentation Définir des points d'arrêt sur les adresses mémoire est une bonne chose, mais cela ne fournit pas l'outil le plus convivial. Nous voulons pouvoir définir des points d'arrêt sur les lignes de code source et les adresses d'entrée de fonction afin de pouvoir déboguer au même niveau d'abstraction que le code.

Cet article ajoutera des points d'arrêt au niveau de la source à notre débogueur. Avec toutes les fonctionnalités que nous prenons déjà en charge, c’est beaucoup plus simple qu’il n’y paraît au départ. Nous ajouterons également une commande pour obtenir le type et l'adresse d'un symbole, ce qui est utile pour localiser du code ou des données et comprendre les concepts de liaison.

Index des séries

Ces liens prendront effet progressivement au fur et à mesure de la publication des articles ultérieurs.

  1. Préparer l'environnement
  2. Point d'arrêt
  3. Registres et mémoire
  4. Elfes et nains
  5. Code source et signaux
  6. Exécution étape par étape au niveau du code source
  7. Points d'arrêt au niveau de la source
  8. Pile d'appels
  9. Lire les variables
  10. Prochaines étapes
Point d'arrêt NAIN

Elfes et nains Cet article décrit le fonctionnement des informations de débogage DWARF et comment elles peuvent être utilisées pour mapper le code machine au code source de haut niveau. Rappelez-vous que DWARF contient la plage d'adresses d'une fonction et une table de lignes qui vous permet de traduire les emplacements de code entre les couches d'abstraction. Nous utiliserons ces fonctions pour implémenter nos points d'arrêt.

Entrée de fonction

Définir des points d'arrêt sur les noms de fonctions peut être un peu compliqué si vous pensez à la surcharge, aux fonctions membres, etc., mais nous allons parcourir toutes les unités de compilation et rechercher les fonctions qui correspondent au nom que nous recherchons. Les informations DWARF ressemblent à ceci :

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

Copier après la connexion

Nous voulons faire correspondre DW_AT_name et utiliser DW_AT_low_pc (l'adresse de départ de la fonction) pour définir notre point d'arrêt.

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);
}
}
}
}
Copier après la connexion

La seule chose qui semble un peu étrange dans ce code est l'entrée ++. Le problème est que le DW_AT_low_pc de la fonction ne pointe pas vers l'adresse de début du code utilisateur de la fonction, il pointe vers le début du prologue. Le compilateur génère généralement le prologue et l'épilogue d'une fonction, qui sont utilisés pour effectuer la sauvegarde et la restauration de la pile, manipuler le pointeur de pile, etc. Cela ne nous est pas très utile, nous incrémentons donc la ligne d'entrée de un pour obtenir la première ligne de code utilisateur au lieu du prologue. La table de lignes DWARF a en fait certaines fonctionnalités permettant de marquer l'entrée comme première ligne après le prologue de la fonction, mais tous les compilateurs ne la génèrent pas, j'ai donc opté pour l'approche originale.

Ligne de code source

Pour définir un point d'arrêt sur une ligne source de haut niveau, nous devons convertir le numéro de ligne en adresse dans DWARF. Nous allons parcourir les unités de compilation, en recherchant une dont le nom correspond au fichier donné, puis rechercher l'entrée correspondant à la ligne donnée.

DWARF ressemble un peu à ça :

.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
Copier après la connexion

Donc, si nous voulons définir un point d'arrêt sur la ligne 5 de ab.cpp, nous rechercherons l'entrée liée à la ligne (0x004004e3) et définirons un point d'arrêt.

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;
}
}
}
}
}
Copier après la connexion

J'ai fait un hack is_suffix ici pour que vous puissiez taper c.cpp pour a/b/c.cpp. Bien sûr, vous devriez réellement utiliser une bibliothèque de gestion de chemin sensible à la casse ou quelque chose du genre, mais je suis paresseux. Entry.is_stmt vérifie si l'entrée de la table de lignes est marquée comme le début d'une instruction, qui est définie par le compilateur en fonction de l'adresse qu'il considère comme la meilleure cible pour le point d'arrêt.

Recherche de symboles

Quand on est au niveau du fichier objet, les symboles sont rois. Les fonctions sont nommées avec des symboles, les variables globales sont nommées avec des symboles, vous obtenez un symbole, nous obtenons un symbole, tout le monde obtient un symbole. Dans un fichier objet donné, certains symboles peuvent référencer d'autres fichiers objets ou bibliothèques partagées, et l'éditeur de liens créera un programme exécutable à partir des références de symboles.

Les symboles peuvent être recherchés dans une table de symboles correctement nommée, qui est stockée dans la section ELF du fichier binaire. Heureusement, libelfin dispose d'une interface agréable pour ce faire, nous n'avons donc pas besoin de gérer nous-mêmes tous les éléments ELF. Pour vous donner une idée de ce à quoi nous avons affaire, voici un dump de la partie .symtab du binaire, généré par 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
Copier après la connexion

Vous pouvez voir de nombreux symboles utilisés pour configurer l'environnement dans le fichier objet, et enfin vous pouvez voir le symbole principal.

Nous sommes intéressés par le type, le nom et la valeur (adresse) du symbole. Nous avons une énumération symbol_type de ce type et utilisons un std::string comme nom et std::uintptr_t comme 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;
};
Copier après la connexion

Nous devons mapper le type de symbole que nous obtenons de libelfin à notre énumération, car nous ne voulons pas que les dépendances brisent cette interface. Heureusement, j'ai choisi le même nom pour tout donc c'était facile :

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;
}
};
Copier après la connexion

Enfin, nous devons rechercher des symboles. À des fins d'illustration, je parcours la partie ELF de la table des symboles et collecte tous les symboles que j'y trouve dans un std :: vector. Une implémentation plus intelligente pourrait établir un mappage du nom au symbole afin que vous n'ayez qu'à examiner les données une seule fois.

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;
}
Copier après la connexion
添加命令

一如往常,我们需要添加一些更多的命令来向用户暴露功能。对于断点,我使用 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]);
}
}
Copier après la connexion

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

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;
}
}
Copier après la connexion
测试一下

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

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

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

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

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn

Outils d'IA chauds

Undresser.AI Undress

Undresser.AI Undress

Application basée sur l'IA pour créer des photos de nu réalistes

AI Clothes Remover

AI Clothes Remover

Outil d'IA en ligne pour supprimer les vêtements des photos.

Undress AI Tool

Undress AI Tool

Images de déshabillage gratuites

Clothoff.io

Clothoff.io

Dissolvant de vêtements AI

AI Hentai Generator

AI Hentai Generator

Générez AI Hentai gratuitement.

Outils chauds

Bloc-notes++7.3.1

Bloc-notes++7.3.1

Éditeur de code facile à utiliser et gratuit

SublimeText3 version chinoise

SublimeText3 version chinoise

Version chinoise, très simple à utiliser

Envoyer Studio 13.0.1

Envoyer Studio 13.0.1

Puissant environnement de développement intégré PHP

Dreamweaver CS6

Dreamweaver CS6

Outils de développement Web visuel

SublimeText3 version Mac

SublimeText3 version Mac

Logiciel d'édition de code au niveau de Dieu (SublimeText3)

Différence entre Centos et Ubuntu Différence entre Centos et Ubuntu Apr 14, 2025 pm 09:09 PM

Les principales différences entre Centos et Ubuntu sont: l'origine (Centos provient de Red Hat, pour les entreprises; Ubuntu provient de Debian, pour les particuliers), la gestion des packages (Centos utilise Yum, se concentrant sur la stabilité; Ubuntu utilise APT, pour une fréquence de mise à jour élevée), le cycle de support (CentOS fournit 10 ans de soutien, Ubuntu fournit un large soutien de LT tutoriels et documents), utilisations (Centos est biaisé vers les serveurs, Ubuntu convient aux serveurs et aux ordinateurs de bureau), d'autres différences incluent la simplicité de l'installation (Centos est mince)

Le choix de Centos après l'arrêt de l'entretien Le choix de Centos après l'arrêt de l'entretien Apr 14, 2025 pm 08:51 PM

CentOS a été interrompu, les alternatives comprennent: 1. Rocky Linux (meilleure compatibilité); 2. Almalinux (compatible avec CentOS); 3. Serveur Ubuntu (configuration requise); 4. Red Hat Enterprise Linux (version commerciale, licence payante); 5. Oracle Linux (compatible avec Centos et Rhel). Lors de la migration, les considérations sont: la compatibilité, la disponibilité, le soutien, le coût et le soutien communautaire.

Comment installer CentOS Comment installer CentOS Apr 14, 2025 pm 09:03 PM

Étapes d'installation de CentOS: Téléchargez l'image ISO et Burn Bootable Media; démarrer et sélectionner la source d'installation; sélectionnez la langue et la disposition du clavier; configurer le réseau; partitionner le disque dur; définir l'horloge système; créer l'utilisateur racine; sélectionnez le progiciel; démarrer l'installation; Redémarrez et démarrez à partir du disque dur une fois l'installation terminée.

Comment utiliser Docker Desktop Comment utiliser Docker Desktop Apr 15, 2025 am 11:45 AM

Comment utiliser Docker Desktop? Docker Desktop est un outil pour exécuter des conteneurs Docker sur les machines locales. Les étapes à utiliser incluent: 1. Installer Docker Desktop; 2. Démarrer Docker Desktop; 3. Créer une image Docker (à l'aide de DockerFile); 4. Build Docker Image (en utilisant Docker Build); 5. Exécuter Docker Container (à l'aide de Docker Run).

Explication détaillée du principe docker Explication détaillée du principe docker Apr 14, 2025 pm 11:57 PM

Docker utilise les fonctionnalités du noyau Linux pour fournir un environnement de fonctionnement d'application efficace et isolé. Son principe de travail est le suivant: 1. Le miroir est utilisé comme modèle en lecture seule, qui contient tout ce dont vous avez besoin pour exécuter l'application; 2. Le Système de fichiers Union (UnionFS) empile plusieurs systèmes de fichiers, ne stockant que les différences, l'économie d'espace et l'accélération; 3. Le démon gère les miroirs et les conteneurs, et le client les utilise pour l'interaction; 4. Les espaces de noms et les CGROUP implémentent l'isolement des conteneurs et les limitations de ressources; 5. Modes de réseau multiples prennent en charge l'interconnexion du conteneur. Ce n'est qu'en comprenant ces concepts principaux que vous pouvez mieux utiliser Docker.

Comment afficher le processus Docker Comment afficher le processus Docker Apr 15, 2025 am 11:48 AM

Méthode de visualisation du processus docker: 1. Commande Docker CLI: Docker PS; 2. Commande CLI Systemd: Docker d'état SystemCTL; 3. Docker Compose CLI Commande: Docker-Compose PS; 4. Process Explorer (Windows); 5. / Répertoire proc (Linux).

Quelle configuration de l'ordinateur est requise pour VScode Quelle configuration de l'ordinateur est requise pour VScode Apr 15, 2025 pm 09:48 PM

Vs Code Système Exigences: Système d'exploitation: Windows 10 et supérieur, MacOS 10.12 et supérieur, processeur de distribution Linux: minimum 1,6 GHz, recommandé 2,0 GHz et au-dessus de la mémoire: minimum 512 Mo, recommandée 4 Go et plus d'espace de stockage: Minimum 250 Mo, recommandée 1 Go et plus d'autres exigences: connexion du réseau stable, xorg / wayland (Linux) recommandé et recommandée et plus

Que faire si l'image Docker échoue Que faire si l'image Docker échoue Apr 15, 2025 am 11:21 AM

Dépannage des étapes pour la construction d'image Docker échouée: cochez la syntaxe Dockerfile et la version de dépendance. Vérifiez si le contexte de construction contient le code source et les dépendances requis. Affichez le journal de construction pour les détails d'erreur. Utilisez l'option - cibler pour créer une phase hiérarchique pour identifier les points de défaillance. Assurez-vous d'utiliser la dernière version de Docker Engine. Créez l'image avec --t [Image-Name]: Debug Mode pour déboguer le problème. Vérifiez l'espace disque et assurez-vous qu'il est suffisant. Désactivez SELINUX pour éviter les interférences avec le processus de construction. Demandez de l'aide aux plateformes communautaires, fournissez Dockerfiles et créez des descriptions de journaux pour des suggestions plus spécifiques.

See all articles