Archives de la catégorie ‘Linux’

Options de compilation pour Linux industriel (1)

Embarqué, Formations, Linux, Temps-réel | Publié par cpb
fév 19 2012

make menuconfig

J’ai remarqué une question récurrente, tant durant mes sessions de formation sur Linux industriel qu’au cours de prestations d’ingénierie concernant des systèmes temps-réel ou embarqués : « Quelles sont les options du noyau qui influent sur [un sujet donné] ? » Le sujet en question a généralement trait aux mécanismes d’ordonnancement, à la vitesse de boot, à la taille du code produit, etc.

Donner une réponse exhaustive est impossible mais au fil du temps, j’ai constitué une petite liste des options les plus importantes à vérifier lors de la compilation du noyau, pour optimiser certains aspects. Ces paramètres sont parfois complémentaires parfois antagonistes. Par exemple le souci d’économie énergétique sur un système embarqué autonome est aux antipodes des problématiques de temps de réponse aux interruptions en temps-réel.

Bien entendu, je ne traite pas ici des options permettant au kernel de fonctionner (reconnaître les périphériques présents, disposer des protocoles réseau, gérer les types de systèmes de fichiers nécessaires…) Je considère qu’elles sont indispensables pour que le noyau démarre. Ce qui m’intéresse ici, ce sont les options de compilation susceptibles d’améliorer le comportement dans un environnement industriel.

Je prends comme version de référence le noyau linux-3.2.tar.bz2 et je vais passer en revue les principaux menus de configuration que l’on peut observer avec  « make menuconfig » « make xconfig » « make gconfig » etc. Les paramètres décrits ci-dessous proviennent d’une configuration pour processeur x86, mais je décrirai également quelques options spécifiques à d’autres architectures.

 Menu General Setup

  • Cross-compiler tool prefix : lors d’une compilation croisée (pour un autre processeur), on indiquera ici le préfixe nécessaire pour trouver la cross-toolchain (que l’on peut générer de différentes manières). Par exemple /opt/cross-arm/usr/bin/arm-linux- indiquera que les appels au compilateur gcc devront se transformer en /opt/cross-arm/usr/bin/arm-linux-gcc, et ainsi de suite pour les autres outils de compilation (as, ld, ar, etc.). Il s’agit d’une option que l’on peut également surcharger en remplissant la variable d’environnement CROSS_COMPILE avant d’appeler make.
  • Kernel compression mode : pour optimiser la vitesse de boot on préférera une compression LZO, pour réduire au maximum la taille du noyau en mémoire flash (avant transfert dans la Ram) on choisira LZMA, pour avoir un équilibre entre ces deux paramètres j’utiliserais XZ. Sur certaines architectures, toutes les compressions ne sont pas disponibles.
  • Support for paging of anonymous memory (swap) : sur la plupart des systèmes industriels, l’utilisation de swap (partition ou fichier sur disque dans lesquels on transfère temporairement une partie de la mémoire Ram en cas de saturation) est proscrite. En effet le système de fichiers est généralement en mémoire flash qui ne supporte pas les écritures intensives. En outre cela diminue la prédictibilité des temps d’accès. Je désactive habituellement cette option afin de ne pas risque d’activer malencontreusement le swap ultérieurement.
  • System V IPC et Posix Message Queues : ces mécanismes de communication entre processus (files de messages, sémaphores, mémoire partagée) sont souvent employés dans les systèmes industriels, sauf si le code applicatif est constitué seulement de threads regroupé dans un unique processus. Généralement j’active ces deux options.
  • BSD Process accounting, Export task/process statistics, et Auditing Support : si ces options d’instrumentation peuvent être utiles pendant la phase de mise au point du code, elles n’ont plus lieu d’être activées sur un système en production. Je conseille de les désactiver (ce qui peut nécessiter d’intervenir dans les menus Security options et Virtualization que nous verrons dans d’autres articles).
  • Sous-menu RCU Subsystem : L’option Enable RCU priority boosting permet de limiter les inversions de priorité liées aux mécanismes de synchronisation Read-Copy-Update. Ceci peut être utile – bien que d’un effet limité – dans un système temps-réel à condition de mettre une valeur élevée (99 par exemple) pour Real-time priority to boost RCU readers to.
  • Kernel .config support et Enable access to .config through /proc/config.gz : ces deux options sont à mon avis très utiles. Elles permettent d’embarquer dans l’image du noyau compilé sa propre configuration (réalisée avec make menuconfig). La mise au point d’un système embarqué ou temps-réel peut nécessiter des dizaines de compilations et tests successifs en modifiant peu à peu les options décrites ici. Il n’est pas simple de garder une trace exacte de toutes les modifications apportées et la discipline nécessaire pour sauvegarder et documenter chaque fois le fichier .config vient souvent à manquer après des heures de frustrations et d’échecs successifs. Le fait que chaque noyau compilé puisse restituer sa propre configuration nous évite de gérer les versions et permet facilement de comparer les options utilisées.
  • Sous-menu Control Group support : la plupart des options de ce sous-menu peuvent être utiles pour gérer finement l’ordonnancement des tâches et la répartition des ressources (CPU, mémoire, entrées-sorties du sous-système Block, etc.). Ces paramètres peuvent servir à gérer des tâches en temps-partagé mais également en temps-réel (notamment Group CPU scheduler –> Group scheduling for SCHED_RR/FIFO).
  • J’ai évoqué l’option Automatic process group scheduling dans un article précédent. Elle active le regroupement automatique des processus rattachés au même terminal pour leur donner une part de CPU équivalente aux autres groupes en ordonnancement temps-partagé.
  • Sur de nombreux systèmes embarqués l’option Initial RAM filesystem and RAM disk sera nécessaire car elle permettra de démarrer avec un système de fichiers monté en mémoire Ram (quitte à accéder ensuite à des partitions différentes sur mémoire flash). On choisira en principe le même type de compression (LZMA, XZ, LZO…) que celui de l’option Kernel compression mode vue plus haut, en préférant un volume réduit ou une décompression plus rapide suivant les cas.
  • L’option Optimize for size sera activée sur les systèmes embarqués pour demander au compilateur de réduire la taille de l’image du noyau ; de même on désactivera Enable full-sized data structures for core qui se trouve un peu plus bas
  • Le sous-menu Configure standard kernel features (expert users) n’est parfois accessible que si l’option Embedded System (plus bas) est activée – ce qu’il faut faire. On peut désactiver toutes les options du sous-menu sur la plupart des systèmes embarqués. Le noyau deviendra totalement silencieux (pas de messages de trace avec printk(), pas d’avertissement avec WARN(), pas d’indication d’erreur interne avec BUG()) et ne nous fournira plus d’éléments de diagnostic. En revanche sa taille sera réduite et le boot sensiblement plus rapide.
  • J’active systématiquement Enable futex support pour bénéficier des mutex rapides (verrouillage sans passage par le noyau s’il n’y a pas de contention, pour plus de détails voir cet article précédent).
  • Toutes les options Profiling support, Kprobes, Optimize trace point call sites, et GCOV-based kernel profiling devront être désactivées sur le noyau final. Elles peuvent être trés utiles durant la mise au point du système, mais ne feraient que charger inutilement l’image cible.

Les options qui n’ont pas été mentionnées seront activées ou non en fonction de l’utilisation de votre système. Si l’appel-système signalfd() par exemple n’est jamais employé, l’option Enable signalfd() system call n’a pas vraiment d’intérêt. Néanmoins on ne gagnera pas grand-chose à la désactiver.

 

Voici un premier tableau récapitulatif des options à activer (Y) ou désactiver (N) en fonction des optimisations recherchées. Les options non mentionnées ici n’ont pas d’influence directe sur les aspects embarqués (taille mémoire et vitesse de boot) ou les performances temps-réel.

Option Optimisation
pour
embarqué
Optimisation
pour
temps-réel
Kernel compression mode LZMA / LZO
Support for paging of anonymous memory N N
BSD Process accounting N N
Open by fhandle syscalls N
Export statistics through netlink N
Auditing support N N
RCU Subsystem -> Enable tracing for RCU N N
RCU Subsystem -> Enable RCU priority boosting Y
Kernel .config support Y
Enable access to .config through /proc/config.gz Y
Control Group Support -> Example debug cgroup… N N
Control Group Support -> Freezer cgroup subsystem Y
Control Group Support -> Device controller… Y
Control Group Support -> Cpuset support Y
Control Group Support -> Group CPU -> …SCHED_RR/FIFO Y
Control Group Support -> Block IO controller Y Y
Control Group Support -> Enable Block IO controller debugging N N
Namespace support N
Automatic process group scheduling Y
Initial RAM filesystem and RAM disk Y
Support initial RAM disk cmopressed using LZMA Y
Support initial RAM disk cmopressed using XZ Y
Support initial RAM disk cmopressed using LZO Y
Optimize for size Y
Configure standard kernel features -> Enable 16-bit UID… N
Configure standard kernel features -> Sysctl syscall support N
Configure standard kernel features -> Enable support for printk N
Configure standard kernel features -> BUG() support N
Configure standard kernel features -> Enable Elf core dumps N
Enable full-sized data structures for core N
Enable futex support Y
Enable AIO support Y
Embedded system Y
Kernel performance… -> Kernel performance counters N
Kernel performance… -> Debug; use vmalloc to… N N
Enable VM event counters for /proc/vmstat N N
Profiling support N N
Kprobes N N
Optimize trace point call sites N N
GCOV-based kernel profiling -> Enable gcov-based … N N

Nous examinerons les autres menus dans les prochains articles…

Tous les commentaires, rermarques, corrections, sont les bienvenus…

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

Linux | Publié par cpb
fév 12 2012

(english translation here)

Nous avons commencé la mise au point d’une bibliothèque dynamique sous Linux dans les deux articles précédents : dans le premier nous avons vu comment compiler la bibliothèque et gérer les numéros de versions à l’aide de liens symboliques, dans le second nous avons effectué du suivi d’appel et du débogage pas-à-pas. Nous allons désormais nous intéresser à la vérification de la couverture de la bibliothèque.

La couverture de code est une mesure indiquant le pourcentage de lignes de code qui ont été effectivement parcourues pendant une exécution d’un programme. On élargit peu à peu le jeu de tests afin d’obtenir une couverture de 100% (et s’assurer que le code a été intégralement vérifié).

 L’outil par excellence sous Linux est gcov, qui instrumente le code source et nous fournit des statistiques détaillées après exécution. Il est facile de l’utiliser pour vérifier la couverture d’un fichier source compilé directement dans un exécutable ; nous allons l’employer pour le code d’une bibliothèque ce qui nécessite quelques suppléments d’attention.

 Compilation

Reprenons les mêmes fichiers et répertoires (regroupés dans cette archive) que pour les articles précédents. Un répertoire  principal nommé « factorielle » contient quatre sous-répertoires : src, include et lib où se trouvent respectivement les fichiers sources, les fichiers d’en-tête et les fichiers compilés de la bibliothèque. Le quatrième sous-répertoire « test » contient les fichiers source et exécutable d’un programme utilisant notre bibliothèque libfact.

[~]$ cd factorielle/
[factorielle]$ ls
include  lib  src  test
[factorielle]$ ls include/
fact.h
[factorielle]$ ls lib/
libfact.so  libfact.so.2  libfact.so.2.0
[factorielle]$ ls src/
fact.c
[factorielle]$ ls test/
calcule-factorielle.c
[factorielle]$

NB: la bibliothèque était déjà compilée ci-dessus, mais nous allons la régénérer.

La première étape consiste à compiler le code de la bibliothèque. Nous allons procéder comme dans le premier article, mais cette fois en ajoutant l’option --coverage de gcc. Cette option a deux rôles différents .

  • Lors de la phase de compilation elle a la même signification que -fprofile-arcs et -ftest-coverage (que l’on utilisaient avec les versions précédentes de gcc) : la première ajoute dans le code exécutable des informations d’instrumentation (compteurs de passage), la seconde crée une table de correspondance entre ces éléments et les lignes de code source (un fichier nommé à partir du fichier source avec l’extension .gcno)
  • Pendant la phase d’édition des liens, elle est équivalente à -lgcov (qui était ajoutée automatiquement par -ftest-coverage) qui incorpore les points d’entrée nécessaire pour l’emploi ultérieur de gcov.

Voici la compilation de notre bibliothèque.

[factorielle]$ gcc -c --coverage -fPIC -I include/ -o ./src/fact.o ./src/fact.c 
[factorielle]$ ls src/
fact.c  fact.gcno  fact.o
[factorielle]$

Nous voyons qu’avec l’option --coverage la compilation a généré, outre le fichier objet fact.o, un fichier fact.gcno contenant les relations entre les blocs de code et les numéros de lignes. Continuons.

[factorielle]$ gcc -shared -I include/ -Wl,-soname,libfact.so.2 -o lib/libfact.so.2.0 ./src/fact.o --coverage 
[factorielle]$ ls -l lib/
total 20
lrwxrwxrwx 1 cpb cpb    12 2012-02-11 13:39 libfact.so -> libfact.so.2
lrwxrwxrwx 1 cpb cpb    14 2012-02-11 13:39 libfact.so.2 -> libfact.so.2.0
-rwxrwxr-x 1 cpb cpb 16866 2012-02-11 17:10 libfact.so.2.0
[factorielle]$

Nous avons re-créé la bibliothèque libfact.so.2.0. Les liens symboliques permettent de gérer les numéros de version majeurs et mineurs, comme nous l’avons vu dans le premier article. Compilons à présent un fichier exécutable, sans l’option --coverage (ou conservons le fichier exécutable des articles précédents).

[factorielle]$ gcc -I include/ -L lib/ -o test/calcule-factorielle test/calcule-factorielle.c -lfact
[factorielle]$ ls test/
calcule-factorielle  calcule-factorielle.c
[factorielle]$

Exécution

L’exécution du programme se déroule tout à fait normalement (bien qu’en pratique il soit légèrement ralenti). Il faut, bien sûr, penser à renseigner la variable d’environnement LD_LIBRARY_PATH pour indiquer où l’éditeur de lien dynamique trouvera la bibliothèque nécessaire au fonctionnement de l’application.

[factorielle]$ export LD_LIBRARY_PATH=lib/
[factorielle]$ test/calcule-factorielle 4 5 6
4! = 24
5! = 120
6! = 720
[factorielle]$ ls src/
fact.c  fact.gcda  fact.gcno  fact.o
[factorielle]$

Un nouveau fichier fact.gcda est apparu, qui contient les statistiques d’exécution des blocs de fact.gcno (et des transitions entre blocs).

Exploitation des résultats

Pour obtenir des informations sur la couverture du code d’un fichier source, nous invoquons gcov en indiquant le nom du fichier source. Les données sont en effet valable indépendamment pour chaque fichier source de l’application (ou de la bibliothèque).

Nous allons utiliser l’option -o de gcov pour préciser le nom du répertoire où se trouvent les fichiers .c, .gcno et .gcda.

[factorielle]$ gcov -o src/ fact.c
File './src/fact.c'
Lignes exécutées: 87.50% de 8
./src/fact.c:creating 'fact.c.gcov'

[factorielle]$

gcov nous indique que nous n’avons exécuté que 87.5% des huit lignes de code de notre fonction. Comment savoir ce qui s’est passé ? Nous voyons que gcov a aussi créé un fichier fact.c.gcov dans lequel il reprend notre code source, numérote les lignes, ajoute un en-tête et une colonne de statistiques en début de ligne.

[factorielle]$ ls
fact.c.gcov  include  lib  src  test
[factorielle]$ cat fact.c.gcov 
        -:    0:Source:./src/fact.c
        -:    0:Graph:src/fact.gcno
        -:    0:Data:src/fact.gcda
        -:    0:Runs:1
        -:    0:Programs:1
        -:    1:#include
        -:    2:
        3:    3:int factorielle(long int n, long long int * result)
        -:    4:{
        3:    5:	* result = 1;
        3:    6:	if (n < 0)
    #####:    7:		return -1;
        -:    8:	do {
       12:    9:		(*result) = (*result) * n;
       12:   10:		n = n - 1;
       12:   11:	} while (n > 1);
        3:   12:	return 0;
        -:   13:}
        -:   14:
[factorielle]$

L’en-tête décrit les fichiers concernés et le nombre d’exécutions (ici une seule pour le moment). La colonne de gauche indique le nombre de passages sur chaque ligne. Les lignes contenant un tiret « - » ne correspondent à aucun code compilé. Nous voyons que les lignes 3, 5, 6 et 12 ont été parcourues à trois reprises (une invocation pour chaque argument de la ligne de commande), et les lignes 9, 10, et 11 ont exécutées 12 fois (les itérations pour calculer les factorielles).

Si nous réitérons l’opération, les compteurs se cumulent.

$ test/calcule-factorielle 3 8
3! = 6
8! = 40320
[factorielle]$ gcov -o src/ fact.c
File './src/fact.c'
Lignes exécutées: 87.50% de 8
./src/fact.c:creating 'fact.c.gcov'

[factorielle]$ test/calcule-factorielle 3 8
3! = 6
8! = 40320
[factorielle]$ cat fact.c.gcov 
        -:    0:Source:./src/fact.c
        -:    0:Graph:src/fact.gcno
        -:    0:Data:src/fact.gcda
        -:    0:Runs:2
        -:    0:Programs:1
        -:    1:#include
        -:    2:
        5:    3:int factorielle(long int n, long long int * result)
        -:    4:{
        5:    5:	* result = 1;
        5:    6:	if (n < 0)
    #####:    7:		return -1;
        -:    8:	do {
       21:    9:		(*result) = (*result) * n;
       21:   10:		n = n - 1;
       21:   11:	} while (n > 1);
        5:   12:	return 0;
        -:   13:}
        -:   14:
[factorielle]$

Et la ligne 7 ? Pourquoi ces « ##### » ?

Contrairement aux tableurs courants, ce symbole ne signifie pas que le nombre est trop grand pour tenir dans la colonne, mais que la ligne (qui correspond bien à du code compilé) n’a jamais été exécutée. Deux intérêts à cette notation :

  • attirer l’oeil lors du parcours du listing plutôt qu’un simple « 0 » ;
  • permettre une recherche automatisée des lignes non exécutées à l’aide de grep.

Voyons :

[factorielle]$ gcov -o src/ fact.c
File './src/fact.c'
Lignes exécutées: 87.50% de 8
./src/fact.c:creating 'fact.c.gcov'

[factorielle]$ grep "#####" fact.c.gcov
    #####:    7:		return -1;
[factorielle]$

Le numéro de ligne (7) étant affiché ainsi que son contenu, cela permet un aperçu rapide du code non parcouru.

Correction

Nous avons détecté un problème de test de notre bibliothèque, puisqu’une branche n’a jamais été exécutée. Ceci peut être dû à plusieurs raisons.

  • Un jeu de test incomplet. C’est le cas ici et nous allons y remédier facilement ci-dessous.
  • Du code ancien qui n’est plus jamais appelé (code mort). Il est important de le faire disparaître car il perturbe la maintenance du programme.
  • Des lignes de code qui servent à gérer des cas d’erreur rares, difficiles à tester. Nous y reviendrons dans le prochain paragraphe.

Ici la solution est simple, la ligne non testée correspond à une invocation de la fonction avec un argument négatif. C’est facile à produire.

[factorielle]$ test/calcule-factorielle -1
-1! n'existe pas
[factorielle]$ gcov -o src/ fact.c
File './src/fact.c'
Lignes exécutées: 100.00% de 8
./src/fact.c:creating 'fact.c.gcov'

[factorielle]$

A présent, gcov nous indique que toutes les lignes de notre programme ont été couvertes par notre jeu de test. Cela réduit la probabilité de bug restant.

Traitement d’erreur

Une grosse difficulté pour assurer une couverture de code à 100% lors des tests d’un logiciel est de valider les comportements en cas d’erreur système.

Prenons le cas d’un appel-système classique : malloc(). On lui demande de nous allouer une zone mémoire d’une certaine taille (précisée en octets) et il nous renvoie un pointeur. Toutes les documentations précisent qu’en cas de manque de mémoire, malloc() renvoie un pointeur NULL. (Bien qu’en pratique sous Linux ce soit particulièrement difficile à produire, nous en reparlerons dans un futur article).
Aussi, le programmeur consciencieux écrira-t-il quelque chose comme.

    char * buffer;
    buffer = malloc(TAILLE_BUFFER);
    if (buffer == NULL) {
        signaler_erreur("Manque de memoire");
        enregistrer_code_d_erreur(-ENOMEM);
        return -1;
    }
    // ...

Malheureusement les lignes de la portion entre accolades sont difficiles à tester car on ne peut pas « forcer » malloc() à échouer ; les circonstances reposent sur trop de paramètres externes à l’application pour être réellement reproductible.

Dans le cas précis de malloc(), la GlibC nous offre des points d’entrées que l’on peut utiliser pour remplacer la fonction – voir malloc_hook(3) – mais ça n’est pas souvent le cas avec les appels-système.

Il existe néanmoins plusieurs solutions. L’une d’elle, que j’ai utilisée plusieurs fois, consiste à employer une couche logicielle minimale qui reproduit les appels-système dont nous avons besoin en simulant un échec si certains critères sont remplis. Par exemple la routine suivante reproduit malloc() mais échoue au bout d’un nombre d’invocations contenu dans la variable d’environnement MALLOC_FAIL.

src/my_malloc.c:

#include <stdio.h>
#include <stdlib.h>

void * my_malloc(size_t length)
{
	char * string;
	char buffer[32];
	int count;
	string = getenv ("MALLOC_FAIL");
	if (string != NULL) {
		if (sscanf(string, "%d", & count)  == 1) {
			count --;
			if (count == 0)
				return NULL;
			snprintf(buffer, 80, "%d", count);
			setenv("MALLOC_FAIL", buffer, 1 );
		}
	}
	return malloc(length);
}

On peut la déclarer dans un fichier d’en-tête ainsi:

include/my_malloc.h:

#ifndef MY_MALLOC_H
#define MY_MALLOC_H

#ifndef NDEBUG
        extern void * my_malloc(size_t);
#else
#define my_malloc(L) malloc(L)
#endif

#endif

De cette manière, suivant la présence ou non de la constante NDEBUG, qui représente traditionnellement la compilation en version « production » pour la bibliothèque C, notre routine sera compilée comme le malloc() habituel ou avec notre gestion de la variable d’environnement.

Compilons une bibliothèque dynamique avec notre couche d’abstraction minimale.

[factorielle]$ gcc -c -fPIC -Wall -I include/ -o src/my_malloc.o src/my_malloc.c 
[factorielle]$ gcc -shared -Wl,-soname,libmytest.so.1 -o lib/libmytest.so.1.0 src/my_malloc.o
[factorielle]$ ldconfig -n lib/
[factorielle]$ ln -sf libmytest.so.1 lib/libmytest.so
[factorielle]$ ls -l lib/libmy*
lrwxrwxrwx 1 cpb cpb   14 2012-02-12 04:12 lib/libmytest.so -> libmytest.so.1
lrwxrwxrwx 1 cpb cpb   16 2012-02-12 04:11 lib/libmytest.so.1 -> libmytest.so.1.0
-rwxrwxr-x 1 cpb cpb 7014 2012-02-12 04:11 lib/libmytest.so.1.0
[factorielle]$

Créons un petit executable qui utilise notre bibliothèque de test en bouclant autour du my_malloc()

test/test-mymalloc.c:

#include <stdio.h>
#include <stdlib.h>
#include <my_malloc.h>

int main(void)
{
        int i = 1;
        while(1) {
                fprintf(stderr, "i = %2d...", i);
                if (my_malloc(10) == NULL) {
                        fprintf(stderr, "echec !\n");
                        break;
                }
                fprintf(stderr, "ok\n");
                i ++;
        }
        return 0;
}

Compilation…

[factorielle]$ gcc -I include/ -L lib/ -Wall -o test/test-mymalloc test/test-mymalloc.c -lmytest
[factorielle]$

Et premier test, sans définir la variable d’environnement.

[factorielle]$ unset MALLOC_FAIL
[factorielle]$ ./test/test-mymalloc 
i =  1...ok
i =  2...ok
i =  3...ok
i =  4...ok
i =  5...ok
[...]
i = 21928...ok
i = 21929...ok
i = 21930...ok
i = 21931...ok
i = 21932...
  (Contrôle-C)
[factorielle]$

Bien entendu, notre programme ne s’arrête pas. Ou plutôt il s’arrêtera au bout d’un long moment après avoir épuisé ses 3Go d’espace d’adressage (sur une machine 32 bits).

Ré-essayons en forçant un échec au quatrième appel de malloc().

[factorielle]$ export MALLOC_FAIL=4
[factorielle]$ ./test/test-mymalloc
i =  1...ok
i =  2...ok
i =  3...ok
i =  4...echec !
[factorielle]$

Naturellement, ce principe consistant à faire échouer les appels aux fonctions système sous le contrôle d’une variable d’environnement – ou d’autres paramètres (variable globale, fichier, zone de mémoire partagée, etc.) – peut s’appliquer autant au contenu d’une bibliothèque qu’à celui d’un exécutable lorsqu’on a besoin d’assurer une couverture de code de 100% sur l’ensemble du jeu de tests d’une application.

Conclusion

Nous avons observé dans cette petite série d’articles, comment créer, déboguer et tester une bibliothèque dynamique. Je vous encourage à faire vos propres essais, en vous reportant à la documentation de gcc, gdb, gcov, mais également d’autres outils complémentaires comme gprof, ldconfig, valgrind, etc.

Tous les commentaires, remarques, corrections, etc. sont les bienvenus.

Development of a dynamic library (3/3)

Linux | Publié par cpb
fév 12 2012

(version originale en français ici)

In the two previous posts, we started the development of a dynamic library on Linux: the first one saw us building the library and managing version numbers using symbolic links, in the second one we traced library calls and did step-by-step debugging. Now we are interested in checking the coverage of the library.

Code coverage is a measure indicating the percentage of lines of code that were actually covered during a program execution. We can gradually expand the set of tests to obtain 100% coverage (and ensure that the code has been fully verified).

The best known tool under Linux for code coverage is gcov, which instruments the source code and provide us detailed statistics after execution. It is easy to use it to verify coverage of a source file compiled into an executable. We will use it for library code which requires some more attention.

Compilation

Let’s start with the same files and directories (grouped in this archive) as in the previous articles. A directory named « factorial » contains four subdirectories: src, include and lib respectively where the source files, header files and compiled files of the library are. The fourth sub-directory « test » contains the source and executable files of a program using our library libfact.

[~]$ cd factorial/
[factorial]$ ls
include  lib  src  test
[factorial]$ ls include/
fact.h
[factorial]$ ls lib/
libfact.so  libfact.so.2  libfact.so.2.0
[factorial]$ ls src/
fact.c
[factorial]$ ls test/
factorial.c
[factorial]$

NB: The library was already compiled in the above example, but we will rebuild it.

At first we will compile the library code, as in the first article, but adding the --coverage option of gcc. This option has two different roles:

  • During compilation, it has the same meaning as -fprofile-arcs and -ftest-coverage options (which were used with the previous versions of gcc): the first one added instrumentation data to the executable code (counters), the second one created a table of correspondence between instructions blocks and lines of source code (in a file named after the source file with the extension .gcno)
  • During linking, it is equivalent to -lgcov (which was added automatically by -ftest-coverage) that incorporates the necessary entry points for the subsequent use of gcov.

Here’s the compilation of our library.

[factorial]$ gcc -c --coverage -fPIC -I include/ -o ./src/fact.o ./src/fact.c 
[factorial]$ ls src/
fact.c  fact.gcno  fact.o
[factorial]$

We see that with the --coverage option, the compilation generated, in addition to the fact.o object file, a fact.gcno file, containing the relationships between the blocks of code and the line numbers. We continue.

[factorial]$ gcc -shared -I include/ -Wl,-soname,libfact.so.2 -o lib/libfact.so.2.0 ./src/fact.o --coverage 
[factorial]$ ls -l lib/
total 20
lrwxrwxrwx 1 cpb cpb    12 2012-02-11 13:39 libfact.so -> libfact.so.2
lrwxrwxrwx 1 cpb cpb    14 2012-02-11 13:39 libfact.so.2 -> libfact.so.2.0
-rwxrwxr-x 1 cpb cpb 16866 2012-02-11 17:10 libfact.so.2.0
[factorial]$

We have rebuilt the libfact.so.2.0 library. Symbolic links are used to manage the major and minor version numbers, as we saw in the first article. Now compile an executable file, without --coverage option (or use the executable file of previous articles).

[factorial]$ gcc -I include/ -L lib/ -o test/factorial test/factorial.c -lfact
[factorial]$ ls test/
factorial  factorial.c
[factorial]$

Execution

Program execution takes place quite normally (although in practice it is slightly slower). We must, of course, set the environment variable LD_LIBRARY_PATH to specify where the dynamic linker will find the library needed to run the application.

[factorial]$ export LD_LIBRARY_PATH=lib/
[factorial]$ test/factorial 4 5 6
4! = 24
5! = 120
6! = 720
[factorial]$ ls src/
fact.c  fact.gcda  fact.gcno  fact.o
[factorial]$

A new file named fact.gcda appeared, containing the execution statistics for fact.gcno blocks of code (and block transitions).

Results

For information on the code coverage of a source file, we invoke gcov indicating the source file name. The results are computed independently for each source file of the application (or library).

We will use the -o option of gcov to specify the directory name for the .c, .gcno and .gcda files.

[factorial]$ gcov -o src/ fact.c
File 'src/fact.c'
Lines executed:87.50% of 8
src/fact.c:creating 'fact.c.gcov'

[factorielle]$

gcov tells us that we have only performed 87.5% of the eight lines of code in our function. What happened? We see that gcov also created a file named « fact.c.gcov » in which he puts a copy of our source code, numbering the lines, adding a header and a column of statistics at the beggining of the line.

[factorial]$ ls
fact.c.gcov  include  lib  src  test
[factorial]$ cat fact.c.gcov 
        -:    0:Source:src/fact.c
        -:    0:Graph:src/fact.gcno
        -:    0:Data:src/fact.gcda
        -:    0:Runs:1
        -:    0:Programs:1
        -:    1:#include
        -:    2:
        3:    3:int factorial(long int n, long long int * result)
        -:    4:{
        3:    5:    * result = 1;
        3:    6:    if (n < 0)
    #####:    7:        return -1;
        -:    8:    do {
       12:    9:        (*result) = (*result) * n;
       12:   10:        n = n - 1;
       12:   11:    } while (n > 1);
        3:   12:    return 0;
        -:   13:}
        -:   14:
[factorial]$

The header describes the files involved and the number of executions (only one here). The left column shows the number of passes on each line. Lines containing a dash « - » do not match any compiled code. We see that lines 3, 5, 6 and 12 were scanned three times (one invocation for each argument on the command line), and that lines 9, 10 and 11 were executed 12 times (iterations to calculate factorial).

If we repeat the operation, the counters are cumulated.

[factorial]$ test/factorial 3 8
3! = 6
8! = 40320
[factorial]$ gcov -o src/ fact.c
File 'src/fact.c'
Lines executed:87.50% of 8
src/fact.c:creating 'fact.c.gcov'

[factorial]$ cat fact.c.gcov 
        -:    0:Source:src/fact.c
        -:    0:Graph:src/fact.gcno
        -:    0:Data:src/fact.gcda
        -:    0:Runs:2
        -:    0:Programs:1
        -:    1:#include
        -:    2:
        5:    3:int factorial(long int n, long long int * result)
        -:    4:{
        5:    5:    * result = 1;
        5:    6:    if (n < 0)
    #####:    7:        return -1;
        -:    8:    do {
       21:    9:        (*result) = (*result) * n;
       21:   10:        n = n - 1;
       21:   11:    } while (n > 1);
        5:   12:    return 0;
        -:   13:}
        -:   14:
[factorial]$

But, what about line 7? Why these « #####« ?

Unlike spreadsheet programs, this symbol does not mean that the number is too large to fit in the column, but that the line (which corresponds to compiled code) was never executed. Two advantages with this notation:

  • it attracts the eye better than a single « 0″ would do,
  • it allows us to do an automated search of unexecuted lines using grep.

Let’s see:

[factorial]$ gcov -o src fact.c
File 'src/fact.c'
Lines executed:87.50% of 8
src/fact.c:creating 'fact.c.gcov'

[factorial]$ grep '#####' fact.c.gcov 
    #####:    7:        return -1;
[factorial]$

The line number (7) being printed with the content, we can have a quick overview of non executed code.

Correction

We have detected a problem with our test library, since a branch was never executed. This may be due to different reasons.

  • An incomplete tests set. This is the case here and we will fix it easily below.
  • Legacy code that is never invoked again (dead code). It is important to make it disappear because it disrupts the maintenance of the program.
  • Lines of code used to handle rare error cases, difficult to test. We will deal with this problem in the next paragraph.

Here the solution is simple: the uncovered line corresponds to an invocation of the function with a negative argument. It is easy to test.

[factorial]$ ./test/factorial -1
-1! doesn't exist
[factorial]$ gcov -o src fact.c
File 'src/fact.c'
Lines executed:100.00% of 8
src/fact.c:creating 'fact.c.gcov'

[factorial]$

Now gcov tells us that all the lines of our program have been covered by our test set. This reduces the probability of remaining bug.

Error handling

A major difficulty to achieve 100% code coverage during software testing is to validate the behavior in case of system error.

Take a look at a well-known system call: malloc(). We asked him to allocate a memory area of ​​a certain size (given in bytes) and he returns a pointer. All documentation tell you that in case of lack of memory, malloc() returns a NULL pointer. (Although this case is particularly difficult to produce with Linux, we will discuss this in a future article). Also, the conscientious programmer will write something like.

    char * buffer;
    buffer = malloc(BUFFER_SIZE);
    if (buffer == NULL) {
        display_error("Insufficient memory");
        register_error_code(-ENOMEM);
        return -1;
    }
    // ...

Unfortunately the lines in between braces are difficult to test because we can not « force » malloc() to fail. The failure circumstances are based on too many parameters external to the application to be reproducible.

In the specific case of malloc(), the Glibc library provides us entry points that can be used to replace the function – see malloc_hook(3) – but it is not possible for other system calls.

However there are several solutions. One of them, I have used several times, is to use a software layer that replicates the minimum system calls we need and simulates a failure if certain criteria are met. For example the following routine reproduces malloc() but fails after a number of invocations contained in the environment variable MALLOC_FAIL.

src/my_malloc.c:

#include <stdio.h>
#include <stdlib.h>

void * my_malloc(size_t length)
{
	char * string;
	char buffer[32];
	int count;
	string = getenv ("MALLOC_FAIL");
	if (string != NULL) {
		if (sscanf(string, "%d", & count)  == 1) {
			count --;
			if (count == 0)
				return NULL;
			snprintf(buffer, 80, "%d", count);
			setenv("MALLOC_FAIL", buffer, 1 );
		}
	}
	return malloc(length);
}

We can declare it in a header file as follows:

include/my_malloc.h:

#ifndef MY_MALLOC_H
#define MY_MALLOC_H

#ifndef NDEBUG
        extern void * my_malloc(size_t);
#else
#define my_malloc(L) malloc(L)
#endif

#endif

In this way, depending on the presence or absence of the NDEBUG constant, which traditionally represents for the C library the production version of the code, our routine will be compiled as usual malloc() or with our management of the environment variable.

Compile a dynamic library with our minimal abstraction layer minimum.

[factorial]$ gcc -c -fPIC -Wall -I include/ -o src/my_malloc.o src/my_malloc.c 
[factorial]$ gcc -shared -Wl,-soname,libmytest.so.1 -o lib/libmytest.so.1.0 src/my_malloc.o
[factorial]$ ldconfig -n lib/
[factorial]$ ln -sf libmytest.so.1 lib/libmytest.so
[factorial]$ ls -l lib/libmy*
lrwxrwxrwx 1 cpb cpb   14 2012-02-12 04:12 lib/libmytest.so -> libmytest.so.1
lrwxrwxrwx 1 cpb cpb   16 2012-02-12 04:11 lib/libmytest.so.1 -> libmytest.so.1.0
-rwxrwxr-x 1 cpb cpb 7014 2012-02-12 04:11 lib/libmytest.so.1.0
[factorial]$

Let’s write a small program that uses our test library by looping around my_malloc().

test/test-mymalloc.c:

#include <stdio.h>
#include <stdlib.h>
#include <my_malloc.h>

int main(void)
{
        int i = 1;
        while(1) {
                fprintf(stderr, "i = %2d...", i);
                if (my_malloc(10) == NULL) {
                        fprintf(stderr, "failure!\n");
                        break;
                }
                fprintf(stderr, "ok\n");
                i ++;
        }
        return 0;
}

Compilation…

[factorial]$ gcc -I include/ -L lib/ -Wall -o test/test-mymalloc test/test-mymalloc.c -lmytest
[factorial]$

First test, without the environment variable.

[factorial]$ unset MALLOC_FAIL
[factorial]$ ./test/test-mymalloc 
i =  1...ok
i =  2...ok
i =  3...ok
i =  4...ok
i =  5...ok
[...]
i = 21928...ok
i = 21929...ok
i = 21930...ok
i = 21931...ok
i = 21932...
  (Control-C)
[factorial]$

Of course, our program does not stop. Or rather it will stop after a long runtime when exhausting its 3GB address space (on a 32-bits machine).

Try again by forcing a failure in the fourth malloc() call.

[factorial]$ export MALLOC_FAIL=4
[factorial]$ ./test/test-mymalloc
i =  1...ok
i =  2...ok
i =  3...ok
i =  4...failure!
[factorial]$

Of course, this principle of forcing system call failures under the control of an environment variable – or other parameters (global variable, file, shared memory area, etc.) – can be applied equally to a library code when you need 100% code coverage over the entire set of tests for an application.

Conclusion

We observed in this small series of articles, how to create, debug and test a dynamic library. I encourage you to do your own tests, referring to the documentation of gcc, gdb, gcov, but also other complementary tools such as gprof, ldconfig, valgrind, etc.

All comments, remarks, corrections, etc.. are welcome.

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

Linux | 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! = %lld\n", 4, ...4! = 24
)     = 8
__isoc99_sscanf(0xbf897461, 0x8048796, 0xbf896578, 24, 0) = 1
factorielle(5, 0xbf896570, 0xbf896578, 24, 0)    = 0
fprintf(0xb7896500, "%ld! = %lld\n", 5, ...5! = 120
)     = 9
__isoc99_sscanf(0xbf897463, 0x8048796, 0xbf896578, 120, 0) = 1
factorielle(6, 0xbf896570, 0xbf896578, 120, 0)   = 0
fprintf(0xb7896500, "%ld! = %lld\n", 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! = %lld\n", 4, ...)                                            = 8
__isoc99_sscanf(0xbf93a461, 0x8048796, 0xbf938448, 24, 0)                               = 1
factorielle(5, 0xbf938440, 0xbf938448, 24, 0)                                           = 0
fprintf(0xb771d500, "%ld! = %lld\n", 5, ...)                                            = 9
__isoc99_sscanf(0xbf93a463, 0x8048796, 0xbf938448, 120, 0)                              = 1
factorielle(6, 0xbf938440, 0xbf938448, 120, 0)                                          = 0
fprintf(0xb771d500, "%ld! = %lld\n", 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! = %lld\n", 4, ...)                                       = 8
__isoc99_sscanf(0xbf8b545e, 0x8048796, 0xbf8b36b8, 24, 0)                          = 1
factorielle(5, 0xbf8b36b0)                                                         = 0
fprintf(0xb7704500, "%ld! = %lld\n", 5, ...)                                       = 9
__isoc99_sscanf(0xbf8b5460, 0x8048796, 0xbf8b36b8, 120, 0)                         = 1
factorielle(6, 0xbf8b36b0)                                                         = 0
fprintf(0xb7704500, "%ld! = %lld\n", 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! = %lld\n", 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.

Development of a dynamic library (2/3)

Linux | Publié par cpb
fév 04 2012

(Version originale en français)

In the previous article we saw how to build a dynamic library and properly manage its major and minor version numbers for easy maintenance, both for the developer (of the library but also of the applications that use it) and for the administrator of the system where it is installed. We will now consider how to debug our library and applications that call it.

Calls tracking

The first tool to know is ltrace. It sends to the error output of the process a log of all function calls to dynamic libraries. We put ourselves back in the same situation as for the previous article with the following directories:

  • factorial/src contains the source code of the library
  • factorial/include where the header files of the library are
  • factorial/lib containing the compiled files of the library
  • factorial/test where we find source codes and executable files of the applications using the library

Here is an overview of the content of our directory:

[~] cd factorial
[factorial]$ ls
include  lib  src  test
[factorial]$ ls include/
fact.h
[factorial]$ 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
[factorial]$ ls src/
fact.c  fact.o
[factorial]$ ls test/
factorial  factorial.c
[factorial]$

Details to create executable files and symbolic links were in the previous article. Here is an example of the test program.

[factorial]$ export LD_LIBRARY_PATH=lib/
[factorial]$ ./test/factorial 7
7! = 5040
[factorial]$

Let’s use ltrace to see library function calls.

[factorial]$ ltrace ./test/factorial 4 5 6
__libc_start_main(0x80485b4, 4, 0xbf896624, 0x80486b0, 0x8048720
__isoc99_sscanf(0xbf89745f, 0x8048796, 0xbf896578, 0x80486d1, 0x8048500) = 1
factorial(4, 0xbf896570, 0xbf896578, 0x80486d1, 0x8048500) = 0
fprintf(0xb7896500, "%ld! = %lld\n", 4, ...4! = 24
)     = 8
__isoc99_sscanf(0xbf897461, 0x8048796, 0xbf896578, 24, 0) = 1
factorial(5, 0xbf896570, 0xbf896578, 24, 0)    = 0
fprintf(0xb7896500, "%ld! = %lld\n", 5, ...5! = 120
)     = 9
__isoc99_sscanf(0xbf897463, 0x8048796, 0xbf896578, 120, 0) = 1
factorial(6, 0xbf896570, 0xbf896578, 120, 0)   = 0
fprintf(0xb7896500, "%ld! = %lld\n", 6, ...6! = 720
)     = 9
+++ exited (status 0) +++
[factorial]$

First of all there is a mix between the standard output display and the error output content, making the reports difficult to read. We will send error output to a file:

[factorial]$ ltrace ./test/factorial 4 5 6 2>traces.txt
4! = 24
5! = 120
6! = 720
[factorial]$

If our process uses its error output, we can ask ltrace to put its messages directly into a file, using the -o option.

[factorial]$ ltrace -o traces.txt ./test/factorial 4 5 6 
4! = 24
5! = 120
6! = 720
[factorial]$

Let’s see the content of our trace file.

[factorial]$ cat traces.txt 
__libc_start_main(0x80485b4, 4, 0xbf9384f4, 0x80486b0, 0x8048720 <unfinished ...>
_isoc99_sscanf(0xbf93a45f, 0x8048796, 0xbf938448, 0x80486d1, 0x8048500)                 = 1
factorial(4, 0xbf938440, 0xbf938448, 0x80486d1, 0x8048500)                            = 0
fprintf(0xb771d500, "%ld! = %lld\n", 4, ...)                                            = 8
__isoc99_sscanf(0xbf93a461, 0x8048796, 0xbf938448, 24, 0)                               = 1
factorial(5, 0xbf938440, 0xbf938448, 24, 0)                                           = 0
fprintf(0xb771d500, "%ld! = %lld\n", 5, ...)                                            = 9
__isoc99_sscanf(0xbf93a463, 0x8048796, 0xbf938448, 120, 0)                              = 1
factorial(6, 0xbf938440, 0xbf938448, 120, 0)                                          = 0
fprintf(0xb771d500, "%ld! = %lld\n", 6, ...)                                            = 9
+++ exited (status 0) +++
[factorial]$

Indeed we see the call to our factorial() function, but there is something strange: ltrace prints five parameters for our function, but there are in fact only two parameters (see fact.h).
In fact, ltrace relies on a configuration file named /etc/ltrace.conf that contains the number and type of arguments of the functions of the system dynamic libraries. If it does not find the function in this file, it displays five arguments by default.

/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);

Of course our function does not appear in this file. We could change it (if the library was installed in a location accessible to all users), but I propose instead to create an additional file .ltrace.conf that we put in our home directory (where ltrace look for additional configuration files).

[factorial]$ cat ~/.ltrace.conf 
int factorial(long,addr);
[factorial]$ ltrace -o traces.txt ./test/factorial 4 5 6
4! = 24
5! = 120
6! = 720
[factorial]$ cat traces.txt 
__libc_start_main(0x80485b4, 4, 0xbf8b3764, 0x80486b0, 0x8048720
__isoc99_sscanf(0xbf8b545c, 0x8048796, 0xbf8b36b8, 0x80486d1, 0x8048500)           = 1
factorial(4, 0xbf8b36b0)                                                         = 0
fprintf(0xb7704500, "%ld! = %lld\n", 4, ...)                                       = 8
__isoc99_sscanf(0xbf8b545e, 0x8048796, 0xbf8b36b8, 24, 0)                          = 1
factorial(5, 0xbf8b36b0)                                                         = 0
fprintf(0xb7704500, "%ld! = %lld\n", 5, ...)                                       = 9
__isoc99_sscanf(0xbf8b5460, 0x8048796, 0xbf8b36b8, 120, 0)                         = 1
factorial(6, 0xbf8b36b0)                                                         = 0
fprintf(0xb7704500, "%ld! = %lld\n", 6, ...)                                       = 9
+++ exited (status 0) +++
[factorial]$

We may also use the -F option to specify which ltrace.conf file to use.

This time the result is perfect, we see our function calls, with the value to compute, the address of the result to complete and the return status (0 means « all right »).

Using ltrace, and its cousin command strace for system calls, is very usefull when developping applications and library. Non-intrusive (no special compiler option) and no requirement for source code make it usable even on programs deliveres in binary form only. I remember having used it successfully a few years ago on an application that needed a configuration file, but the documentation referred to an invalid directory. So I started ltrace on the application without providing the configuration file. Of course the application failed to start, but I could find in the logs (filtered with grep) the calls to the fopen() library function, and see the sucessive attempts before failure (something like ~/.APPLICATION/, /usr/lib/APPLICATION, /etc/APPLICATION …)

Debugging with Gdb

In most cases during application development, we consider library calls as invocations of elementary functions on whose contents we do not question. However, dynamic libraries may also require development and step-by-step analysis.

So, let’s try using gdb with the executable file we produced in the previons article.

[factorial]$ gdb ./test/factorial 
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/factorial/test/factorial...(no debugging symbols found)...done.
(gdb)

The debugger has found our executable and loaded it into memory. However, he can’t find any symbol table needed for debugging. Let’s just try to put a breakpoint at the start of main function.

(gdb) break main
Breakpoint 1 at 0x80485b9
(gdb)

It works. Run the program with a value argument.

(gdb) run 5
Starting program: /home/cpb/factorial/test/factorial 5
Breakpoint 1, 0x080485b9 in main ()
(gdb)

We stopped in the beginning of main(). Try to move one step forward.

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

Indeed, the debugger has no notion of line of code, it goes in one run until the end of main(). We can only allow the process to complete and exit the debugger.

(gdb) cont
Continuing.
[Inferior 1 (process 15847) exited normally]
Undefined command: "exit".  Try "help".
(gdb) quit
[factorial]$

Now, we compile our code with the -g option, in order to insert in the executable file a table of associations between memory addresses and lines of code. Then repeat the experience.

[factorial]$ gcc -I include/ -L lib/ -o test/factorial test/factorial.c -l fact -g
[factorial]$ gdb ./test/factorial 
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/factorial/test/factorial...done.
(gdb) break main
Breakpoint 1 at 0x80485bf: file test/factorial.c, line 11.
(gdb) run 5
Starting program: /home/cpb/factorial/test/factorial 5

Breakpoint 1, main (argc=2, argv=0xbffff274) at test/factorial.c:11
11		if (argc < 2) {
(gdb)

This time, we note that gdb displays correctly the line 11. Continue to move forward instruction-by-instruction with the step command.

(gdb) step
15		for (i = 1; i < argc; i ++)
(gdb) step
16			if (sscanf(argv[i], "%ld", & n) == 1) {
(gdb) step
17				if (factorial(n, & f) == 0)
(gdb) step
18                                  fprintf(stdout, "%ld! = %lld\n", n, f);
(gdb)

Unfortunately gdb does not have the sources of the library, so we can not enter the factorial() function, no more than we could in sscanf() or fprintf().

(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
[factorial]$

Debugging the library

We have to compile the library with the -g option.

[factorial]$ gcc -I include/ -o src/fact.o -c src/fact.c -g
[factorial]$ gcc -shared -I include/ -Wl,-soname,libfact.so.2 -o lib/libfact.so.2.0 src/fact.o 
[factorial]$ gcc -I include/ -L lib/ -o test/factorial test/factorial.c -l fact -g

And start debugging…

[factorial]$ gdb ./test/factorial 
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/factorial/test/factorial...done.
(gdb) break main
Breakpoint 1 at 0x80485bf: file test/factorial.c, line 11.
(gdb) run 5
Starting program: /home/cpb/factorial/test/factorial 5
(gdb) break main
Breakpoint 1, main (argc=2, argv=0xbffff274) at test/factorial.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 (factorial(n, & f) == 0)
(gdb) step
factorial (n=5, result=0xbffff1c0) at src/fact.c:5
5		* result = 1;
(gdb)

Now we really are into our factorial() function,  let’s continue step-by-step a few instructions more…

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

We can also look at the state of the variables. The gdb printing of the results is a bit surprising at first because it prefixes the evaluated terms by a $ character followed by a serial number. This allows to easily write graphical front-ends (like ddd, xxgdb, Eclipse, etc..) that retrieve the returned values.

(gdb) print *result
$1 = 20
(gdb) print n
$2 = 4
(gdb) cont
Continuing.
5! = 120
[Inferior 1 (process 30206) exited normally]
(gdb) quit
[factorial]$

Source files locatoin

The source files location is stored into the executable file when compiling with the -g option. However they can be moved or library can be compiled on another machine that those used for debugging. We must therefore have a way to tell gdb where to find the source files of the library. Do a test by moving the sources from the library into a totally different directory.

[factorial]$ mkdir -p ~/tmp/sources
[factorial]$ mv src/* ~/tmp/sources/
[factorial]$ gdb ./test/factorial 
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/factorial/test/factorial...done.
(gdb) break factorial
Breakpoint 1 at 0x80484f0
(gdb) run 5
Starting program: /home/cpb/factorial/test/factorial 5

Breakpoint 1, factorial (n=5, result=0xbffff1c0) at src/fact.c:5
5	src/fact.c: No such file or directory.
	in src/fact.c
(gdb) quit
A debugging session is active.

	Inferior 1 [process 31536] will be killed.

Quit anyway? (y or n) y
[factorial]$

Of course the debugging failed. We will now try the directory command at the gdb prompt.

[factorial]$ gdb ./test/factorial 
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/factorial/test/factorial...done.
(gdb) directory ~/tmp/sources/
Source directories searched: /home/cpb/tmp/sources:$cdir:$cwd
(gdb) break factorial
Breakpoint 1 at 0x80484f0
(gdb) run 5
Starting program: /home/cpb/factorial/test/factorial 5

Breakpoint 1, factorial (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
[factorial]$

It works perfectly.

Conclusion

We have seen two important steps in the development of a library: function calls tracking and step-by-step debugging. We will examine the coverage tests in the next article.