Mise au point de bibliothèque dynamique (2/3)

Publié par cpb
Fév 04 2012

(English translation)

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èque
  • factorielle/include où se trouvent les fichiers d’en-tête de la bibliothèque
  • factorielle/lib contenant la bibliothèque compilée
  • factorielle/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.

5 Réponses

  1. mala dit :

    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

    • cpb dit :

      Bonjour,

      En effet, ltrace ne permet pas de suivre l’exécution des bibliothèques chargées avec dlopen(). 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’environnement LD_PRELOAD.

  2. mala dit :

    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

    • cpb dit :

      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 :

      #include <stdio.h>
      #include <stdlib.h>
      #include <unistd.h>
      
      int main(int argc, char * argv[])
      {
        if (argc != 3) {
          fprintf(stderr, "usage : %s  \n", argv[0]);
          exit(EXIT_FAILURE);
        }
        if (setenv(LD_PRELOAD, argv[2], 1) != 0) {
          perror("setenv");
          exit(EXIT_FAILURE);
        }
        execlp(argv[1], argv[1], NULL);
        perror(argv[1]);
        return EXIT_FAILURE;
      }

      Il faudrait alors l’invoquer via ltrace.

      $ ltrace -o /root/traces.txt  -f  ./mon_lanceur /xxx/zzz/mon_programme /xxx/yyy/ma_lib.so
      

      Je n’ai pas testé le fonctionnement.

  3. mala dit :

    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

URL de trackback pour cette page