Maison > Article > Tutoriel système > Explorez les techniques de gestion des variables dans les débogueurs Linux !
Présentation | Les variables sont sournoises. Parfois, ils s'assoient volontiers dans la caisse, pour se retrouver sur la pile dès qu'ils se retournent. À des fins d'optimisation, le compilateur peut les jeter complètement par la fenêtre. Quelle que soit la manière dont les variables se déplacent dans la mémoire, nous avons besoin d'un moyen de les suivre et de les manipuler dans le débogueur. Cet article vous apprendra comment gérer les variables dans le débogueur et démontrera une implémentation simple à l'aide de libelfin. |
Avant de commencer, assurez-vous que vous utilisez la version de libelfin fbreg sur ma branche. Celui-ci contient quelques hacks pour prendre en charge l'obtention de l'adresse de base du cadre de pile actuel et l'évaluation d'une liste de positions, dont aucun n'est fourni par Libelfin natif. Vous devrez peut-être transmettre le paramètre -gdwarf-2 à GCC pour générer des messages DWARF compatibles. Mais avant de l'implémenter, je vais détailler le fonctionnement de l'encodage positionnel dans la dernière spécification DWARF 5. Si vous souhaitez en savoir plus, vous pouvez obtenir la norme ici.
Emplacement DWARFLa localisation d'une variable en mémoire à un instant donné est codée dans le message DWARF à l'aide de l'attribut DW_AT_location. Une description d'emplacement peut être une description d'emplacement unique, une description d'emplacement composite ou une liste d'emplacements.
DW_AT_location est codé de trois manières différentes selon le type de description de l'emplacement. exprloc code des descriptions de poste simples et composites. Ils consistent en une longueur d’octet suivie d’une expression DWARF ou d’une description d’emplacement. Listes d'emplacements codées pour loclist et loclistptr, qui fournissent l'index ou le décalage dans la section .debug_loclists, qui décrit la liste d'emplacements réelle.
Expression naineUtilisez des expressions DWARF pour calculer la position réelle d'une variable. Cela inclut une série d'opérations qui manipulent les valeurs de la pile. Il existe de nombreuses opérations DWARF disponibles, je ne les expliquerai donc pas en détail. Au lieu de cela, je vais donner quelques exemples de chaque expression pour vous donner quelque chose sur quoi travailler. N’ayez pas peur non plus : libelfin s’occupera de toute cette complexité pour nous.
Les représentations de type DWARF doivent être suffisamment puissantes pour fournir des représentations de variables utiles aux utilisateurs du débogueur. Les utilisateurs souhaitent souvent pouvoir déboguer au niveau de l'application plutôt qu'au niveau de la machine, et ils doivent comprendre ce que font leurs variables.
Le type DWARF est codé dans DIE avec la plupart des autres informations de débogage. Ils peuvent avoir des propriétés indiquant leur nom, leur encodage, leur taille, leurs octets, etc. Une myriade de balises de type sont disponibles pour représenter des pointeurs, des tableaux, des structures, des typedefs et tout ce que vous pourriez voir dans un programme C ou C++.
Prenons cette structure simple comme exemple :
struct test{ int i; float j; int k[42]; test* next; };
Le DIE parent de cette structure est comme ceci :
< 1><0x0000002a> DW_TAG_structure_type DW_AT_name "test" DW_AT_byte_size 0x000000b8 DW_AT_decl_file 0x00000001 test.cpp DW_AT_decl_line 0x00000001
Ce que dit ci-dessus, c'est que nous avons une structure appelée test, d'une taille de 0xb8, déclarée sur la ligne 1 de test.cpp. Ensuite, il existe un certain nombre de sous-DIE qui décrivent les membres.
< 2><0x00000032> DW_TAG_member DW_AT_name "i" DW_AT_type <0x00000063> DW_AT_decl_file 0x00000001 test.cpp DW_AT_decl_line 0x00000002 DW_AT_data_member_location 0 < 2><0x0000003e> DW_TAG_member DW_AT_name "j" DW_AT_type <0x0000006a> DW_AT_decl_file 0x00000001 test.cpp DW_AT_decl_line 0x00000003 DW_AT_data_member_location 4 < 2><0x0000004a> DW_TAG_member DW_AT_name "k" DW_AT_type <0x00000071> DW_AT_decl_file 0x00000001 test.cpp DW_AT_decl_line 0x00000004 DW_AT_data_member_location 8 < 2><0x00000056> DW_TAG_member DW_AT_name "next" DW_AT_type <0x00000084> DW_AT_decl_file 0x00000001 test.cpp DW_AT_decl_line 0x00000005 DW_AT_data_member_location 176(as signed = -80)
Chaque membre a un nom, un type (qui est un décalage DIE), un fichier et une ligne de déclaration, ainsi qu'un décalage d'octet par rapport à la structure dans laquelle réside son membre. Ses points de type sont les suivants.
< 1><0x00000063> DW_TAG_base_type DW_AT_name "int" DW_AT_encoding DW_ATE_signed DW_AT_byte_size 0x00000004 < 1><0x0000006a> DW_TAG_base_type DW_AT_name "float" DW_AT_encoding DW_ATE_float DW_AT_byte_size 0x00000004 < 1><0x00000071> DW_TAG_array_type DW_AT_type <0x00000063> < 2><0x00000076> DW_TAG_subrange_type DW_AT_type <0x0000007d> DW_AT_count 0x0000002a < 1><0x0000007d> DW_TAG_base_type DW_AT_name "sizetype" DW_AT_byte_size 0x00000008 DW_AT_encoding DW_ATE_unsigned < 1><0x00000084> DW_TAG_pointer_type DW_AT_type <0x0000002a>
Comme vous pouvez le voir, int sur mon ordinateur portable est un type entier signé de 4 octets et float est un nombre à virgule flottante de 4 octets. Le type de tableau d'entiers a 2a éléments en pointant vers un type int comme type d'élément et sizetype (considérez-le comme size_t) comme type d'index. Le type de test * est DW_TAG_pointer_type, qui fait référence au test DIE.
Implémentation d'un simple lecteur de variablesComme mentionné ci-dessus, Libelfin gérera l'essentiel de la complexité pour nous. Cependant, il n’implémente pas toutes les méthodes de représentation des positions des variables, et leur gestion dans notre code deviendra très complexe. Par conséquent, je choisis désormais de prendre en charge uniquement exprloc. Veuillez ajouter la prise en charge d'autres types d'expressions si nécessaire. Si vous êtes vraiment courageux, veuillez soumettre un patch à libelfin pour vous aider à compléter le support nécessaire !
La gestion des variables implique principalement de localiser différentes parties dans la mémoire ou dans les registres, et la lecture ou l'écriture est la même qu'auparavant. Pour simplifier les choses, je vais simplement vous expliquer comment mettre en œuvre la lecture.
Nous devons d’abord indiquer à libelfin comment lire les registres de notre processus. Nous créons une classe qui hérite de expr_context et utilisons ptrace pour tout gérer :
class ptrace_expr_context : public dwarf::expr_context { public: ptrace_expr_context (pid_t pid) : m_pid{pid} {} dwarf::taddr reg (unsigned regnum) override { return get_register_value_from_dwarf_register(m_pid, regnum); } dwarf::taddr pc() override { struct user_regs_struct regs; ptrace(PTRACE_GETREGS, m_pid, nullptr, ®s); return regs.rip; } dwarf::taddr deref_size (dwarf::taddr address, unsigned size) override { //TODO take into account size return ptrace(PTRACE_PEEKDATA, m_pid, address, nullptr); } private: pid_t m_pid; };
La lecture sera gérée par la fonction read_variables dans notre classe de débogueur :
void debugger::read_variables() { using namespace dwarf; auto func = get_function_from_pc(get_pc()); //... }
La première chose que nous avons faite ci-dessus est de trouver la fonction dans laquelle nous nous trouvons actuellement, puis nous devons parcourir les entrées de cette fonction pour trouver les variables :
for (const auto& die : func) { if (die.tag == DW_TAG::variable) { //... } }
Nous obtenons les informations de localisation en recherchant l'entrée DW_AT_location dans DIE :
auto loc_val = die[DW_AT::location];
Ensuite, nous nous assurons qu'il s'agit bien d'une exprloc et demandons à libelfin d'évaluer notre expression :
if (loc_val.get_type() == value::type::exprloc) { ptrace_expr_context context {m_pid}; auto result = loc_val.as_exprloc().evaluate(&context);
Maintenant que nous avons évalué l'expression, nous devons lire le contenu de la variable. Cela peut être en mémoire ou dans des registres, nous traiterons donc les deux cas :
switch (result.location_type) { case expr_result::type::address: { auto value = read_memory(result.value); std::cout << at_name(die) << " (0x" << std::hex << result.value << ") = " << value << std::endl; break; } case expr_result::type::reg: { auto value = get_register_value_from_dwarf_register(m_pid, result.value); std::cout << at_name(die) << " (reg " << result.value << ") = " << value << std::endl; break; } default: throw std::runtime_error{"Unhandled variable location"}; }
Vous pouvez voir qu'en fonction du type de variable, j'ai imprimé la valeur sans explication. Espérons qu'avec ce code, vous pourrez voir comment il existe une prise en charge pour l'écriture de variables ou la recherche de variables avec un nom donné.
Enfin, nous pouvons ajouter ceci à notre analyseur de commandes :
else if(is_prefix(command, "variables")) { read_variables(); }Testez-le
Écrivez une petite fonction avec quelques variables, compilez-la sans optimisation et avec des informations de débogage, puis voyez si vous pouvez lire la valeur de la variable. Essayez d'écrire à l'adresse mémoire où la variable est stockée et voyez comment le programme change de comportement.
Il y a déjà neuf articles, et il reste le dernier ! La prochaine fois, j'aborderai quelques concepts plus avancés qui pourraient vous intéresser. Vous pouvez maintenant trouver le code de cet article ici.
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!