Déboguer un programme Python avec gdb
Par haypo, vendredi 22 août 2008 à 01:08 :: Python :: #160 :: rss
Python est un langage interprété par une machine virtuelle appelée CPython. Dumoins, CPython est l'implémentation de référence, il en existe d'autres moins répendues (PyPy, Jython, IronPython, etc.). Pour ceux qui ne le savent pas encore, CPython est écrit en langage C. Lorsque CPython plante (« Fatal error: ... », erreur de segmentation ou autre), il est difficile de connaître les raisons du plantage. Ce billet devrait vous éclairer un peu si vous en êtes arrivé à passer par gdb pour déboguer Python.
Recompiler Python sans les optimisations
Avant toute chose, je vous conseille vivement de recompiler Python pour désactiver les optimisations. Par défaut, Python demande à gcc d'optimiser au maximum. Or quand on débogue un programme optimisé (-O2 ou -O3), gdb se comporte bizzarement. Récupérez les sources de Python et ses dépendances. Sous Debian, on peut le faire en deux commandes :
apt-get build-dep python2.5 apt-get source python2.5
Modifiez le script configure : remplacez « -O3 » et « -g -O3 » par « -O0 -ggdb », puis compilez Python. Sous Debian :
cd python2.5-<tab> vim configure (... modifiez le fichier, sauvez, quittez ...) dpkg-buildpackage -us -uc
Sinon :
./configure && make
Il n'est pas nécessaire d'installer Python : utilisez simplement le programme généré à la racine (./python) avec gdb.
Quelques informations sur le code source de CPython
Macros importantes :
- op : nom courant pour indiquer « object pointer »
- Py_INCREF(op) / Py_DECREF(op) : incrémente / décrémente le compteur de référence de l'objet
- Py_XINCREF(op) / Py_XDECREF(op) : idem, mais ne fait rien si op vaut NULL
Répertoires principaux :
- Objects/*.c : définition des objets de base (int, str, ...)
- Modules/*.c : code source de modules (bas niveau) écrit en C (_ctypes, _sre, zlib, ...)
- Lib/*.py : modules (haut niveau) écrits en Python
Fichiers intéressants :
- Python/ceval.c : boucle principale qui évalue le bytecode Python. Fichier très compliqué où il ne vaut mieux pas mettre les doigts ;-)
- Python/errors.c : gestion des erreurs
- Python/pythonrun.c : initialisation des modules et objets, gestion de la console
Macros gdbinit
Dans le code source de Python, on y trouve un fichier Misc/gdbinit. C'est une suite de macros pour gdb aidant au debug Python. Commandes principales :
- pystack : affiche la backtrace Python, plusieurs lignes de la forme « nom du fichier (ligne): nom de la fonction »
- pyo : affiche le contenu d'une variable Python (plante si la variable n'est pas du type PyObject !)
Copiez Misc/gdbinit dans ~/.gdbinit pour activer les macros.
Exemple d'une session de débogage
Un backtrace Python ressemble à ça :
(gdb) where (...) #3 0xb7ab63ff in time_sleep (...) at Modules/timemodule.c:198 #4 0x08123b5d in PyCFunction_Call (...) at Objects/methodobject.c:81 #5 0x0809523b in call_function (...) at Python/ceval.c:3403 #6 0x08091e84 in PyEval_EvalFrameEx (...) at Python/ceval.c:2205 #7 0x080954fe in fast_function (...) at Python/ceval.c:3491 #8 0x08095335 in call_function (...) at Python/ceval.c:3424 #9 0x08091e84 in PyEval_EvalFrameEx (...) at Python/ceval.c:2205 (...) #22 0x080b9a4b in run_mod (...) at Python/pythonrun.c:1553 (...) #26 0x080c7a55 in Py_Main (...) at Modules/main.c:592 #27 0x0805a1e9 in main (...) at ./Modules/python.c:57
Notes :
- Les premières frames montrent où Python en est actuellement : ici (frame #0..#3) Python est en train de dormir
- La fonction PyEval_EvalFrameEx() est le cœur de l'interprète Python. Elle interprète le bytecode, gère les exceptions, s'occupe d'empiler/dépiler les frames, etc.
La backtrace Python n'est pas réellement exploitable. Il vaut mieux utiliser pystack :
(gdb) pystack /home/haypo/fusil3000/fusil/mas/univers.py (31): execute /home/haypo/fusil3000/fusil/application.py (196): executeProject /home/haypo/fusil3000/fusil/application.py (221): runProject /home/haypo/fusil3000/fusil/application.py (242): main fusil-python (693): <module>
Ah c'est tout de suite plus clair ! Pour afficher une variable, utilisez la commande pyo :
(gdb) pyo f object : <frame object at 0x835e92c> type : frame refcount: 1 address : 0x835e92c
Débogage avec gdb sans les macros
Le gros problème des macros gdbinit est qu'elles ont besoin que le processus soit actif car des fonctions CPython sont réellement appelée par ces macros ! Donc si on travaille sur un fichier core ou que les macros ne fonctionnent pas, il faut passer en « mode manuel » (à la mano quoi ;-)).
(gdb) frame 5 #5 0x0809524f in call_function (pp_stack=0xbfdd3cfc, oparg=1) at Python/ceval.c:3403 3403 C_TRACE(x, PyCFunction_Call(func,callargs,NULL)); (gdb) print callargs $3 = (PyObject *) 0xb788502c (gdb) print *callargs $4 = {ob_refcnt = 1, ob_type = 0x8168e40} (gdb) print callargs->ob_type.tp_name $5 = 0x814bc25 "tuple"
N'importe quel objet Python a au moins deux attributs : ob_type (pointeur vers son type) et ob_refcnt (compteur de références).
On sait qu'on a un tuple, on peut donc maintenant caster avec le type PyTupleObject :
(gdb) print *(PyTupleObject*)callargs $7 = {ob_base = {ob_base = {ob_refcnt = 1, ob_type = 0x8168e40}, ob_size = 1}, ob_item = {0x81b5fc4}} (gdb) print (*(PyTupleObject*)callargs).ob_item[0] $9 = (PyObject *) 0x81b5fc4 (gdb) print (*(PyTupleObject*)callargs).ob_item[0]->ob_type.tp_name $10 = 0x816154b "float" (gdb) print *((PyFloatObject*)(*(PyTupleObject*)callargs).ob_item[0]) $12 = {ob_base = {ob_refcnt = 3, ob_type = 0x818dbe0}, ob_fval = 0.001}
Tout ça pour voir que callargs est un tuple contenant un nombre flottant : « (0.001,) » !
Pour récupérer la backtrace, il faut itérer sur les frames « PyEval_EvalEx » en partant du début. Déjà , voyons quelles sont les frames :
(gdb) where (...) #6 0x08091e98 in PyEval_EvalFrameEx (...) at Python/ceval.c:2205 (...) #9 0x08091e98 in PyEval_EvalFrameEx (...) at Python/ceval.c:2205 (...) #12 0x08091e98 in PyEval_EvalFrameEx (...) at Python/ceval.c:2205 (...)
C'est donc les frames 6, 9, 12, etc. Commandes pour récupérer le nom du fichier et la ligne :
(gdb) frame 6 (gdb) printf "%s\n", ((PyStringObject*)co->co_filename)->ob_sval /home/haypo/prog/fusil/fusil/mas/univers.py (gdb) lineno 31 (gdb) frame 9 (gdb) printf "%s\n", ((PyStringObject*)co->co_filename)->ob_sval /home/haypo/prog/fusil/fusil/application.py (gdb) lineno 196 (...)
On retrouve la même chose que pystack :
- /home/haypo/fusil3000/fusil/mas/univers.py:31
- /home/haypo/fusil3000/fusil/application.py:196
- ...
Mots de la fin
J'espère que vous n'aurez pas à en arriver là (utiliser gdb), mais parfois on n'a pas le choix.
Pour finir, une astuce pour utiliser Valgrind avec Python : il existe un fichier Misc/valgrind-python.supp dans le code source de Python. Utilisez-le avec un commande du style « valgrind --suppressions=(...)/Misc/valgrind-python.supp -- python script.py » pour ignorer les très nombreux faux positifs sur PyObject_Free() et PyObject_Realloc().
Commentaires
1. Le mardi 12 octobre 2010 à 10:44, par gucci shoes
2. Le lundi 30 juillet 2012 à 10:44, par Unsownenrosse
Ajouter un commentaire
Les commentaires pour ce billet sont fermés.