Nous avons examiné dans l’article précédent comment compiler une bibliothèque dynamique et gérer correctement ses numéros majeurs et mineurs de version afin d’en faciliter la maintenance, tant pour le développeur (de la bibliothèque mais aussi pour celui des applications qui l’utilisent) que pour l’administrateur du système sur lequel elle est installée. Nous allons à présent examiner comment effectuer le débogage de notre bibiliothèque et des applications qui l’appellent.
Suivi des appels
Le premier utilitaire a connaître est ltrace
. Il permet d’envoyer vers la sortie d’erreur du processus une trace de tous les appels de fonctions de bibliothèques dynamiques. Nous nous replaçons dans la même situation que pour l’article précédent avec les répertoires suivants
factorielle/src
contient le code source de la bibliothèquefactorielle/include
où se trouvent les fichiers d’en-tête de la bibliothèquefactorielle/lib
contenant la bibliothèque compiléefactorielle/test
dans lequel on trouve codes sources et fichiers exécutables des applications appelant la bibliothèque
Voici un aperçu du contenu de notre répertoire
[~] cd factorielle [factorielle]$ ls include lib src test [factorielle]$ ls include/ fact.h [factorielle]$ ls -l lib/ total 8 lrwxrwxrwx 1 cpb cpb 12 2012-02-04 05:04 libfact.so -> libfact.so.2 lrwxrwxrwx 1 cpb cpb 14 2012-02-04 05:04 libfact.so.2 -> libfact.so.2.0 -rwxrwxr-x 1 cpb cpb 6661 2012-02-04 05:04 libfact.so.2.0 [factorielle]$ ls src/ fact.c fact.o [factorielle]$ ls test/ calcule-factorielle calcule-factorielle.c [factorielle]$
Les détails pour créer les fichiers exécutables et les liens symboliques se trouvaient dans le précédent article. Voici également un exemple d’exécution du programme de test.
[factorielle]$ export LD_LIBRARY_PATH=lib/ [factorielle]$ ./test/calcule-factorielle 7 7! = 5040 [factorielle]$
Employons ltrace
pour observer les appels de fonctions de bibliothèques.
[factorielle]$ ltrace ./test/calcule-factorielle 4 5 6 __libc_start_main(0x80485b4, 4, 0xbf896624, 0x80486b0, 0x8048720 __isoc99_sscanf(0xbf89745f, 0x8048796, 0xbf896578, 0x80486d1, 0x8048500) = 1 factorielle(4, 0xbf896570, 0xbf896578, 0x80486d1, 0x8048500) = 0 fprintf(0xb7896500, "%ld! = %lldn", 4, ...4! = 24 ) = 8 __isoc99_sscanf(0xbf897461, 0x8048796, 0xbf896578, 24, 0) = 1 factorielle(5, 0xbf896570, 0xbf896578, 24, 0) = 0 fprintf(0xb7896500, "%ld! = %lldn", 5, ...5! = 120 ) = 9 __isoc99_sscanf(0xbf897463, 0x8048796, 0xbf896578, 120, 0) = 1 factorielle(6, 0xbf896570, 0xbf896578, 120, 0) = 0 fprintf(0xb7896500, "%ld! = %lldn", 6, ...6! = 720 ) = 9 +++ exited (status 0) +++ [factorielle]$
Tout d’abord il y a un mélange à l’écran entre la sortie standard et la sortie d’erreur, ce qui rend les comptes-rendus d’appel difficiles à lire. Envoyons la sortie d’erreur dans un fichier.
[factorielle]$ ltrace ./test/calcule-factorielle 4 5 6 2>traces.txt 4! = 24 5! = 120 6! = 720 [factorielle]$
Si notre processus utilise sa sortie d’erreur, on peut demander à ltrace
d’envoyer ses messages directement dans un fichier à l’aide de son option -o
.
[factorielle]$ ltrace -o traces.txt ./test/calcule-factorielle 4 5 6 4! = 24 5! = 120 6! = 720 [factorielle]$
Voyons ce que contient notre fichier de traces.
[factorielle]$ cat traces.txt __libc_start_main(0x80485b4, 4, 0xbf9384f4, 0x80486b0, 0x8048720 <unfinished ...> _isoc99_sscanf(0xbf93a45f, 0x8048796, 0xbf938448, 0x80486d1, 0x8048500) = 1 factorielle(4, 0xbf938440, 0xbf938448, 0x80486d1, 0x8048500) = 0 fprintf(0xb771d500, "%ld! = %lldn", 4, ...) = 8 __isoc99_sscanf(0xbf93a461, 0x8048796, 0xbf938448, 24, 0) = 1 factorielle(5, 0xbf938440, 0xbf938448, 24, 0) = 0 fprintf(0xb771d500, "%ld! = %lldn", 5, ...) = 9 __isoc99_sscanf(0xbf93a463, 0x8048796, 0xbf938448, 120, 0) = 1 factorielle(6, 0xbf938440, 0xbf938448, 120, 0) = 0 fprintf(0xb771d500, "%ld! = %lldn", 6, ...) = 9 +++ exited (status 0) +++ [factorielle]$
Nous voyons bien nos appels de la fonction factorielle()
mais ce qui est curieux, c’est que ltrace
affiche cinq arguments pour notre routine, alors qu’elle n’en comporte que deux normalement (voir fact.h
). En fait, ltrace
s’appuie sur un fichier de configuration nommé /etc/ltrace.conf
qui contient le nombre et le type des arguments des fonctions des bibliothèques dynamiques du système. S’il ne trouve pas la fonction dans ce fichier, il affiche cinq arguments par défaut.
/etc/ltrace.conf ; ltrace.conf ; ; ~/.ltrace.conf will also be read, if it exists. The -F option may be ; used to suppress the automatic inclusion of both this file and ; ~/.ltrace.conf, and load a different config file or config files ; instead. [...] ; arpa/inet.h int inet_aton(string,addr); string inet_ntoa(addr); ; It isn't an ADDR but an hexa number... addr inet_addr(string); [...] ; stdio.h int fclose(file); int feof(file); int ferror(file); int fflush(file); char fgetc(file); addr fgets(+string, int, file); int fileno(file); file fopen(string,string); file fopen64(string,string); int fprintf(file,format); int fputc(char,file); int fputs(string,file); ulong fread(addr,ulong,ulong,file); ulong fread_unlocked(addr,ulong,ulong,file); ulong fwrite(string,ulong,ulong,file); ulong fwrite_unlocked(string,ulong,ulong,file); int pclose(addr); void perror(string); addr popen(string, string); int printf(format); int puts(string); int remove(string); int snprintf(+string2,ulong,format); int sprintf(+string,format); [...] int SYS_waitpid(int,addr,int); ulong SYS_readv(int,addr,int); ulong SYS_writev(int,addr,int); int SYS_mprotect(addr,int,int); int SYS_access(string,octal);
Bien entendu notre fonction ne figure pas dans ce fichier. Nous pourrions le modifier (si la bibliothèque était installée dans un emplacement du système accessible à tous les utilisateurs), mais je propose plutôt de créer un fichier supplémentaire .ltrace.conf
que nous plaçons dans notre répertoire personnel (ltrace
vient le chercher à cet emplacement).
[factorielle]$ cat ~/.ltrace.conf int factorielle(long,addr); [factorielle]$ ltrace -o traces.txt ./test/calcule-factorielle 4 5 6 4! = 24 5! = 120 6! = 720 [factorielle]$ cat traces.txt __libc_start_main(0x80485b4, 4, 0xbf8b3764, 0x80486b0, 0x8048720 __isoc99_sscanf(0xbf8b545c, 0x8048796, 0xbf8b36b8, 0x80486d1, 0x8048500) = 1 factorielle(4, 0xbf8b36b0) = 0 fprintf(0xb7704500, "%ld! = %lldn", 4, ...) = 8 __isoc99_sscanf(0xbf8b545e, 0x8048796, 0xbf8b36b8, 24, 0) = 1 factorielle(5, 0xbf8b36b0) = 0 fprintf(0xb7704500, "%ld! = %lldn", 5, ...) = 9 __isoc99_sscanf(0xbf8b5460, 0x8048796, 0xbf8b36b8, 120, 0) = 1 factorielle(6, 0xbf8b36b0) = 0 fprintf(0xb7704500, "%ld! = %lldn", 6, ...) = 9 +++ exited (status 0) +++ [factorielle]$
Nous pourrions aussi utiliser l’option -F
de ltrace
pour préciser les fichiers ltrace.conf
à utiliser.
Cette fois-ci le résultat est parfait, nous voyons bien les appels de notre fonction, avec la valeur à calculer, l’adresse du résultat à remplir et le statut de retour, 0
signifiant « tout va bien ».
L’utilisation de ltrace
, et de sa commande cousine strace
pour les appels-système, est très utile pour la mise au point d’applications ou de bibliothèques. Son aspect non-intrusif (pas d’option de compilation particulière) et le fait que le code source ne soit pas nécessaire le rend utilisable même pour des programmes livrés sous forme binaire seulement. Je me souviens de l’avoir employé avec succès il y a quelques années sur une application à qui je devais fournir un fichier de configuration, mais pour lequel la documentation mentionnait un répertoire invalide. J’ai donc lancé ltrace
sur l’application sans fournir de fichier de configuration. Bien entendu l’application a refusé de démarrer, mais j’ai pu chercher dans les traces (en filtrant avec grep
) les lignes d’ouverture de fichiers – avec fopen()
– et voir les tentatives successives avant échec (quelque chose comme /home/cpb/.APPLICATION/
, /usr/lib/APPLICATION
, /etc/APPLICATION
…)
Débogage avec Gdb
Dans la plupart des cas, on considère, lorsqu’on fait la mise au point d’une application, les appels de bibliothèques comme des invocations élémentaires, des fonctions sur le contenu desquelles on ne se pose pas de question. Pourtant, les bibliothèques dynamiques peuvent elles aussi nécessiter une mise au point et une analyse pas-à-pas de leur fonctionnement.
Essayons d’utiliser le débogueur gdb
sur l’exécutable que nous avons produit dans le précédent article.
[factorielle]$ gdb ./test/calcule-factorielle GNU gdb (Ubuntu/Linaro 7.3-0ubuntu2) 7.3-2011.08 Copyright (C) 2011 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "i686-linux-gnu". For bug reporting instructions, please see: <http://bugs.launchpad.net/gdb-linaro/>... Reading symbols from /home/cpb/factorielle/test/calcule-factorielle...(no debugging symbols found)...done. (gdb)
Le débogueur a bien trouvé notre exécutable et l’a chargé en mémoire. Toutefois, il nous indique qu’il ne dispose pas de la table de symboles nécessaires au débogage. Essayons quand même de poser un point d’arrêt en début de fonction main
.
(gdb) break main Breakpoint 1 at 0x80485b9 (gdb)
Cela fonctionne. Lançons le programme avec une valeur en argument.
(gdb) run 5 Starting program: /home/cpb/factorielle/test/calcule-factorielle 5 Breakpoint 1, 0x080485b9 in main () (gdb)
Nous sommes arrêtés en debut de main()
, essayons d’avancer d’une instruction.
(gdb) next Single stepping until exit from function main, which has no line number information. 5! = 120 0xb7e62113 in __libc_start_main () from /lib/i386-linux-gnu/libc.so.6 (gdb)
Et oui, le débogueur n’a pas de notion de ligne de code, il va d’une seule traite jusqu’à la fin de main()
. Nous ne pouvons que laisser le processus se terminer et quitter le débogueur.
(gdb) cont Continuing. [Inferior 1 (process 15847) exited normally] Undefined command: "exit". Try "help". (gdb) quit [factorielle]$
Compilons notre exécutable avec l’option -g
pour y intégrer une table de correspondance entre les adresses mémoire et les lignes de code source. Puis réitérons l’expérience.
[factorielle]$ gcc -I include/ -L lib/ -o test/calcule-factorielle test/calcule-factorielle.c -l fact -g [factorielle]$ gdb ./test/calcule-factorielle GNU gdb (Ubuntu/Linaro 7.3-0ubuntu2) 7.3-2011.08 Copyright (C) 2011 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "i686-linux-gnu". For bug reporting instructions, please see: <http://bugs.launchpad.net/gdb-linaro/> Reading symbols from /home/cpb/factorielle/test/calcule-factorielle...done. (gdb) break main Breakpoint 1 at 0x80485bf: file test/calcule-factorielle.c, line 11. (gdb) run 5 Starting program: /home/cpb/factorielle/test/calcule-factorielle 5 Breakpoint 1, main (argc=2, argv=0xbffff274) at test/calcule-factorielle.c:11 11 if (argc < 2) { (gdb)
Cette fois nous remarquons que gdb
nous affiche correctement la ligne 11. Continuons à avancer en pas-à-pas avec la commande step
.
(gdb) step 15 for (i = 1; i < argc; i ++) (gdb) step 16 if (sscanf(argv[i], "%ld", & n) == 1) { (gdb) step 17 if (factorielle(n, & f) == 0) (gdb)
Malheureusement gdb
ne dispose pas des sources de la bibliothèque, aussi ne nous permet-il pas de rentrer dans la fonction factorielle()
, pas plus qu’il ne le fait pour sscanf()
ou fprintf()
.
(gdb) step 18 fprintf(stdout, "%ld! = %lldn", n, f); (gdb) step 5! = 120 15 for (i = 1; i < argc; i ++) (gdb) step 22 return EXIT_SUCCESS; (gdb) step 23 } (gdb) step 0xb7e62113 in __libc_start_main () from /lib/i386-linux-gnu/libc.so.6 (gdb) step Single stepping until exit from function __libc_start_main, which has no line number information. [Inferior 1 (process 27455) exited normally] (gdb) quit [factorielle]$
Débogage de la bibliothèque
Nous devons compiler notre bibliothèque avec l’option -g
ainsi.
[factorielle]$ gcc -I include/ -o src/fact.o -c src/fact.c -g [factorielle]$ gcc -shared -I include/ -Wl,-soname,libfact.so.2 -o lib/libfact.so.2.0 src/fact.o [factorielle]$ gcc -I include/ -L lib/ -o test/calcule-factorielle test/calcule-factorielle.c -l fact -g
Puis lancer le débogage.
[factorielle]$ gdb ./test/calcule-factorielle GNU gdb (Ubuntu/Linaro 7.3-0ubuntu2) 7.3-2011.08 Copyright (C) 2011 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "i686-linux-gnu". For bug reporting instructions, please see: <http://bugs.launchpad.net/gdb-linaro/>... Reading symbols from /home/cpb/factorielle/test/calcule-factorielle...done. (gdb) break main Breakpoint 1 at 0x80485bf: file test/calcule-factorielle.c, line 11. (gdb) run 5 Starting program: /home/cpb/factorielle/test/calcule-factorielle 5 (gdb) break main Breakpoint 1, main (argc=2, argv=0xbffff274) at test/calcule-factorielle.c:11 11 if (argc < 2) { (gdb) step 15 for (i = 1; i < argc; i ++) (gdb) step 16 if (sscanf(argv[i], "%ld", & n) == 1) { (gdb) step 17 if (factorielle(n, & f) == 0) (gdb) step factorielle (n=5, result=0xbffff1c0) at src/fact.c:5 5 * result = 1; (gdb)
Nous sommes bien dans notre routine factorielle()
, continuons quelques instructions en pas-à-pas…
(gdb) step 6 if (n < 0) (gdb) step 9 (*result) = (*result) * n; (gdb) step 10 n = n - 1; (gdb) step 11 } while (n > 1); (gdb) step 9 (*result) = (*result) * n; (gdb) step 10 n = n - 1; (gdb)
Nous pouvons également examiner l’état des variables. La présentation du résultat par gdb
est un peu surprenante de prime abord, car il préfixe les expressions d’un $
suivi d’un numéro d’ordre de ses évaluations. Ceci permet d’écrire facilement des frontaux graphiques (comme ddd
, xxgdb
, Eclipse, etc.) qui récupèrent les valeurs renvoyées.
(gdb) print *result $1 = 20 (gdb) print n $2 = 4 (gdb) cont Continuing. 5! = 120 [Inferior 1 (process 30206) exited normally] (gdb) quit [factorielle]$
Emplacement des sources
L’emplacement des sources de la bibliothèque est mentionné dans le fichier exécutable lors de la compilation avec l’option -g
. Toutefois elles peuvent être déplacées ou la bibliothèque peut être compilée sur une autre machine que celle utilisée pour le débogage. Il faut donc disposer d’un moyen d’indiquer à gdb
l’endroit où se trouvent les fichiers source de la bibliothèque. Faisons un essai en déplaçant les sources de la bibliothèque dans un répertoire totalement indépendant.
[factorielle]$ mkdir -p ~/tmp/sources [factorielle]$ mv src/* ~/tmp/sources/ [factorielle]$ gdb ./test/calcule-factorielle GNU gdb (Ubuntu/Linaro 7.3-0ubuntu2) 7.3-2011.08 Copyright (C) 2011 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "i686-linux-gnu". For bug reporting instructions, please see: <http://bugs.launchpad.net/gdb-linaro/>... Reading symbols from /home/cpb/factorielle/test/calcule-factorielle...done. (gdb) break factorielle Breakpoint 1 at 0x80484f0 (gdb) run 5 Starting program: /home/cpb/factorielle/test/calcule-factorielle 5 Breakpoint 1, factorielle (n=5, result=0xbffff1c0) at src/fact.c:5 5 src/fact.c: Aucun fichier ou dossier de ce type. in src/fact.c (gdb) quit A debugging session is active. Inferior 1 [process 31536] will be killed. Quit anyway? (y or n) y [factorielle]$
Évidemment le débogage échoue. Utilisons à présent la commande directory
de gdb
.
[factorielle]$ gdb ./test/calcule-factorielle GNU gdb (Ubuntu/Linaro 7.3-0ubuntu2) 7.3-2011.08 Copyright (C) 2011 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "i686-linux-gnu". For bug reporting instructions, please see: <http://bugs.launchpad.net/gdb-linaro/>... Reading symbols from /home/cpb/factorielle/test/calcule-factorielle...done. (gdb) directory ~/tmp/sources/ Source directories searched: /home/cpb/tmp/sources:$cdir:$cwd (gdb) break factorielle Breakpoint 1 at 0x80484f0 (gdb) run 5 Starting program: /home/cpb/factorielle/test/calcule-factorielle 5 Breakpoint 1, factorielle (n=5, result=0xbffff1c0) at src/fact.c:5 5 * result = 1; (gdb) step 6 if (n < 0) (gdb) step 9 (*result) = (*result) * n; (gdb) cont Continuing. 5! = 120 [Inferior 1 (process 31845) exited normally] (gdb) quit [factorielle]$
Cela fonctionne parfaitement.
Conclusion
Nous avons examiné deux étapes importantes pour la mise au point d’une bibliothèque : le suivi des appels et le débogage pas-à-pas. Nous examinerons les tests en couverture dans le prochain article.
Bonjour,
L’utilitaire ltrace ne trace pas les librairies dynamiques chargées avec la commande dlopen.
Je suis à la recherche un outil pour tracer l’utilisation d’une librairie dynamique chargée avec dlopen. Avez-vous connaissance un outil pour ce type de cas ?
Merci pour la qualité de votre blog.
Mala
Bonjour,
En effet,
ltrace
ne permet pas de suivre l’exécution des bibliothèques chargées avecdlopen()
. Je ne connais pas d’outils permettant cela. Une astuce (limitée) consiste à forcer au démarrage le chargement des bibliothèques – à condition de connaître leur nom – en utilisant la variable d’environnementLD_PRELOAD
.bonjour,
Après plusieurs tests, voici mon scénario
> export LD_PRELOAD= »/xxx/yyy/ma_lib.so »
>/usr/bin/ltrace -o /root/traces.txt -l /xxx/yyy/ma_lib.so -F /root/.ltrace.conf /xxx/zzz/mon_prgramme
/usr/bin/ltrace: symbol lookup error: /xxx/yyy/ma_lib.so: undefined symbol: netsnmp_oid_stash_no_free
>ltrace
ltrace: symbol lookup error: /xxx/yyy/ma_lib.so: undefined symbol: netsnmp_oid_stash_no_free
J’ai le sentiment que le système cherche a charger ma_lib.so dans ltrace puis plante.
Il existe une astuce pour ce type de cas ?
ps: Je suis dans un environnemt buildroot.
Cordialement,
Mala
Je me demande s’il ne faudrait pas se construire un petit lanceur pour l’application, qui s’occupe de remplir la variable d’environnement ainsi :
Il faudrait alors l’invoquer via
ltrace
.Je n’ai pas testé le fonctionnement.
J’ai testé l’idée du lanceur mais j’ai eu des symboles non reconnus en raison de ma version de ltrace.
J’ai actualisé ma version de ltrace (0.5.3->0.7.2) dans mon buildroot car depuis la version 0.6 :
on peut lire :
*** Support tracing of symbols from libraries opened with dlopen
Remarque : Dans ce cas, il faut utiliser l’option -x pour indiquer les fonctions que l’on souhaite tracer.
Mala