Pretty-printer avec GDB
Depuis la version 7.01 (sortie le 2009/10/06), GDB embarque un interpréteur Python cela qui permet d’utiliser Python comme langage de scripting pour étendre les fonctionnalités de GDB.
Dans cet article je vais aborder la création de pretty-printer en Python.
Le problème
Comme un exemple vaut mieux qu’un long discours, je vais montrer ici un cas pratique : l’affichage d’adresses IP.
Les systèmes POSIX représentent les adresses via deux structures:
-
in_addr
pour les IPv4 : une structure qui doit contenir au minimum un champs_addr
de typein_addr_t
(qui est équivalent àuint32_t
). -
in6_addr
pour les IPv6 : une structure qui doit contenir au minimum un champs6_addr
de typeuint8_t[16]
.
Ces informations, parmi d’autres, sont disponibles ici.
De par la définition de ces structures, si on veut les afficher dans GDB on n’obtient quelque chose qui n’est pas très lisible pour un humain.
Soit le programme suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #include <stdlib.h> #include <arpa/inet.h> int main(void) { struct sockaddr_in ipv4; struct sockaddr_in6 ipv6; if (inet_pton(AF_INET, "82.66.107.250", &ipv4.sin_addr) != 1) return EXIT_FAILURE; if (inet_pton(AF_INET6, "2a01:e35:2426:bfa0:215:ff:fe42:7fd3", &ipv6.sin6_addr) != 1) return EXIT_FAILURE; return EXIT_SUCCESS; } |
Si on lance GDB dessus en utilisant le fichier de commande suivant :
1 2 3 4 5 6 7 8 9 10 | break example.c:17 command silent print ipv4->sin_addr print ipv6->sin6_addr continue end run quit |
On obtient :
1 2 3 4 5 6 7 8 | % gdb -q ./example --command=command.gdb 2> /dev/null Reading symbols from ./example...done. Breakpoint 1 at 0x8065581: file example.c, line 17. [Thread debugging using libthread_db enabled] Using host libthread_db library "/usr/lib/libthread_db.so.1". $1 = {s_addr = 4201333330} $2 = {__in6_u = {__u6_addr8 = "*\001\016\065$&\277\240\002\025\000\377\376B", <incomplete sequence \323>, __u6_addr16 = {298, 13582, 9764, 41151, 5378, 65280, 17150, 54143}, __u6_addr32 = {890110250, 2696881700, 4278195458, 3548332798}}} [Inferior 1 (process 2043) exited normally] |
Le moins que l’on puisse dire c’est que ce n’est pas
super human-friendly ($1
étant l’IPv4 et $2
l’IPv6).
Les pretty-printers
Nous allons créer un module python my_pp
. Dans le
répertoire my_pp, nous aurons deux fichiers :
-
netinet.py
qui contiendra le code des pretty-printers. -
__init__.py
qui se chargera d’enregistrer nos pretty-printers auprès de GDB.
Commençons avec le premier fichier (j’ai omis les docstrings pour raccourcir le code).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | from struct import pack from socket import inet_ntop, AF_INET, AF_INET6 from gdb import lookup_type class ipv4Printer(object): def __init__(self, val): self.addr = pack('I', int(val['s_addr'])) def to_string(self): return inet_ntop(AF_INET, self.addr) class ipv6Printer(object): def __init__(self, val): # IPv6 addresses have a size of 128 bits (== 16 octets). N = 16 addr = val.cast(lookup_type("uint8_t").array(N)) self.addr = pack('B'*N, *[int(addr[i]) for i in range(N)]) def to_string(self): return inet_ntop(AF_INET6, self.addr) |
Allons y étape par étape.
1 2 3 4 | from struct import pack from socket import inet_ntop, AF_INET, AF_INET6 from gdb import lookup_type |
Les adresses IP étant stockées sous forme binaire, on importe le
module struct
pour pouvoir les manipuler. Le
module socket
va nous servir à les convertir en chaîne de
caractères2. Enfin, on importe le
module gdb
.
1 2 3 4 5 6 | class ipv4Printer(object): def __init__(self, val): self.addr = pack('I', int(val['s_addr'])) def to_string(self): return inet_ntop(AF_INET, self.addr) |
Lorsque GDB va instancier un objet ipv4Printer
, il va lui passer
en argument une structure de type in_addr
. Comme dit
précédemment, cette structure contient un champ s_addr
qui est
un uint32_t
. Notre méthode d’initialisation se contente donc
d’extraire ce champ dans le membre addr
de notre objet.
Ensuite, la méthode to_string
est appelée par GDB à chaque fois
qu’il doit afficher un objet de type in_addr
. Cette méthode
renvoie la chaîne de caractère représentant l’adresse IP contenue
dans addr
.
1 2 3 4 5 6 7 8 9 | class ipv6Printer(object): def __init__(self, val): # IPv6 addresses have a size of 128 bits (== 16 octets). N = 16 addr = val.cast(lookup_type("uint8_t").array(N)) self.addr = pack('B'*N, *[int(addr[i]) for i in range(N)]) def to_string(self): return inet_ntop(AF_INET6, self.addr) |
En théorie; la version IPv6 devrait être aussi simple que la version IPv4. En théorie… En pratique c’est malheureusement plus compliqué.
En effet, si POSIX requiert que la structure in6_addr_t
possède
un champ nommé s6_addr
le fait est que, la plupart du temps, ce
membre n’existe pas vraiment: c’est un symbole de préprocesseur. En tant que
tel, il n’est pas accessible dans GDB (sauf si on utilise certaines options de
compilation bien spécifiques telle que -g3
et -gdwarf-2
par exemple). Pis encore, Windows ne définit pas ce
membre du tout (Windows ne cherche pas vraiment à être conforme POSIX).
La solution que j’ai trouvée est de traiter la structure in6_addr
comme un tableau de 16 octets. Cela fonctionne et me semble relativement
portable étant donné que la structure in6_addr
est définie comme
une union dont l’un des champs est effectivement un uint8_t[16]
au moins sur les systèmes d’exploitation suivant :
La méthode __init__
de notre objet va donc caster la
valeur puis la stocker dans l’attribut addr
. Au niveau de la
méthode to_string
, c’est quasiment la même chose que
pour ipv4Printer
(on remplace seulement AF_INET
par AF_INET6
).
Enfin, l’enregistrement de nos deux classes se fait dans le fichier
__init__.py
.
1 2 3 4 5 6 7 8 | from gdb.printing import RegexpCollectionPrettyPrinter import netinet def netinet_pp(): pp = RegexpCollectionPrettyPrinter("netinet") pp.add_printer('in_addr', '^in_addr$', netinet.ipv4Printer) pp.add_printer('in6_addr', '^in6_addr$', netinet.ipv6Printer) return pp |
On commence par importer le module gdb.printing
, puis
nos pretty-printers. L’enregistrement se fait via un
objet RegexpCollectionPrettyPrinter
(via lequel on peut donner un
nom à notre groupe de pretty-printers) dans lequel nous ajoutons
nos pretty-printers. Lors de l’ajout, on donne un nom, une expression
rationnelle qui va définir les noms des types que nous pouvons afficher, puis
la classe du pretty-printer.
Maintenant que le code est fait, voyons comment utiliser tout ça.
La mise en œuvre
La mise en œuvre est très simple, elle peut se faire via le fichier .gdbinit. Il suffit d’y ajouter ces lignes :
1 2 3 4 5 6 7 8 9 10 | # Python python from sys import path from gdb.printing import register_pretty_printer path.insert(0, '/chemin/vers/répertoire/contenant/my_pp') import my_pp register_pretty_printer(gdb.current_objfile(), my_pp.netinet_pp()) |
Je pense que ça se passe de commentaire.
Le résultat
Et maintenant, si on lance GDB sur notre exemple on obtient :
Ce qui est, il faut l’admettre, bien plus lisible.
Pour conclure, je mentionnerai que la commande info
pretty-printer
permet d’afficher la liste des pretty-printers
actuellement chargé dans GDB. Il est également possible d’utiliser l’affichage
« brut » de GDB, sans passer par un pretty-printer (même s’il en
existe un pour le type) en utilisant le modificateur /r
de la
commande print
(par exemple print /r toto
).
Voilà, ça sera tout pour cette introduction aux pretty-printers. La documentation officielle est ici pour ceux qui veulent approfondir.