grim7reaper

Un artisan du code

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:

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 :

exemple.c
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 :

command.gdb
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 :

Sortie produite
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 :

  1. netinet.py qui contiendra le code des pretty-printers.
  2. __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).

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

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

Pretty-printer pour l’IPv4
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.

Pretty-printer pour l’IPv6
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.

__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 :

% 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 = 82.66.107.250
$2 = 2a01:e35:2426:bfa0:215:ff:fe42:7fd3
[Inferior 1 (process 2424) exited normally]

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.


  1. je vous conseille d’utiliser au moins GDB 7.8, les versions antérieures ayant un bogue avec les types définis via typedef.

  2. depuis Python 3.3, on utilise plutôt le module ipaddress