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().