Maison  >  Article  >  Tutoriel système  >  Explorez les techniques de gestion des variables dans les débogueurs Linux !

Explorez les techniques de gestion des variables dans les débogueurs Linux !

PHPz
PHPzavant
2024-01-15 23:09:05711parcourir
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.
Index des articles de la série
  1. Préparer l'environnement
  2. Point d'arrêt
  3. Registres et mémoire
  4. ELF et NAIN
  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. Extension de la pile
  9. Gérer les variables
  10. Sujets avancés

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 DWARF

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

  • Description de position simple : décrit la position d'une partie contiguë (généralement toutes les parties) d'un objet. Une simple description d'emplacement peut décrire un emplacement dans une mémoire adressable ou un registre, ou son absence (avec ou sans valeur connue). Par exemple, DW_OP_fbreg -32 : une variable stockée entière - 32 octets à partir de la base du cadre de pile.
  • Description d'emplacement composite : décrivant des objets en termes de fragments, chaque objet peut être contenu dans une partie d'un registre ou stocké dans un emplacement mémoire indépendant des autres fragments. Par exemple, DW_OP_reg3 DW_OP_piece 4 DW_OP_reg10 DW_OP_piece 2 : les quatre premiers octets sont dans le registre 3 et les deux derniers octets sont dans une variable du registre 10.
  • Liste de positions : décrit les objets qui ont une durée de vie limitée ou qui changent d'emplacement au cours de leur durée de vie. Par exemple:
      • [ 0]DW_OP_reg0
      • [ 1]DW_OP_reg3
      • [ 2]DW_OP_reg2
  • Une variable dont l'emplacement est déplacé entre les registres en fonction de la valeur actuelle du compteur du programme.

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 naine

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

  • Encodage littéral
    • DW_OP_lit0, DW_OP_lit1...DW_OP_lit31
      • Poussez les littéraux sur la pile
    • DW_OP_addr
      • Poussez l'opérande d'adresse sur la pile
    • DW_OP_constu
      • Poussez la valeur non signée sur la pile
  • Enregistrer la valeur
    • DW_OP_fbreg
      • Poussez la valeur trouvée à la base du cadre de pile, compensée par la valeur donnée
    • DW_OP_breg0, DW_OP_breg1... DW_OP_breg31
      • Poussez le contenu du registre donné plus le décalage donné sur la pile
  • Opérations de pile
    • DW_OP_dup
      • Copiez la valeur en haut de la pile
    • DW_OP_deref
      • Traitez le haut de la pile comme une adresse mémoire et remplacez-le par le contenu de cette adresse
  • Opérations arithmétiques et logiques
    • DW_OP_et
      • Popez les deux valeurs en haut de la pile et repoussez leur ET logique
    • DW_OP_plus
      • Identique à DW_OP_and, mais ajoute de la valeur
  • Opérations de flux de contrôle
    • DW_OP_le, DW_OP_eq, DW_OP_gt, etc.
      • Insérez les deux premières valeurs, comparez-les et appuyez sur 1 si la condition est vraie, 0 sinon
    • DW_OP_bra
      • Branche conditionnelle : si le haut de la pile n'est pas 0, avancez ou reculez dans l'expression via offset
  • Entrez la conversion
    • DW_OP_convert
      • Convertissez la valeur en haut de la pile en un type différent, qui est décrit par une entrée d'informations DWARF au décalage donné
  • Opérations spéciales
    • DW_OP_nop
      • Ne rien faire !
Type NAIN

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 variables

Comme 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, &regs);
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!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer