Archive for février 2012

Options de compilation pour Linux industriel (2)

Linux | Publié par cpb
fév 26 2012

Dans le précédent article, nous avons examiné les options du menu General Setup à prendre particulièrement en considération lors de la préparation d’un noyau Linux pour un système embarqué ou temps-réel.

Examinons à présent d’autres options proposées, suivant les architectures dans un menu, dont le nom varie légèrement :

Architecture Menu
arm Kernel Features
avr32 System Type and Features
ia64 Processor Type and Features
m32r Processor Type and Features
m68kr Kernel Features
mips Kernel Type
openrisc Processor Type and Features
parisc Processor Type and Features
powerpc Kernel Options
sh Kernel Features
sparc Kernel Features
tile Tilera-specific Configuration
unicore32 Kernel Features
x86 Processor Type and Features
xtensa Processor Type and Features

 

  • Tickless system : pour les systèmes embarqués sur batterie, on activera généralement cette option qui permet au processeur de s’endormir « profondément » lorsqu’il est inactif. Sur un système temps-réel, au contraire on la désactivera, car l’endormissement du processeur rallonge le temps de réaction aux interruptions.
  • Timer Frequency : C’est la fréquence du tick système qui influe sur la granularité des timers logiciels de basse fréquence. Sur un système embarqué, on emploie généralement une fréquence de 300 Hz. Si le système est tickless, ce paramètre n’a pas d’autre influence que la granularité des jiffies.
  • High Resolution Timer Support : si l’architecture permet de disposer de timers haute résolution (fréquence supérieure à 1kHz) il est toujours intéressant de les utiliser. Cette option est indépendante de la précédente. Activée.
  • Preemption Model : sur les systèmes temps-réel ou interactifs, on sélectionnera l’option la plus préemptible. En particulier si le patch Linux-rt est appliqué, on choisira « Fully Preemptible Kernel » pour obtenir la meilleure réactivité face aux interruptions.
  • High Memory Support : sur les architectures (32 bits) où cette option est proposée, elle permet de gérer des quantités de mémoire élevées au prix d’un léger surcoût de gestion de la MMU. Sur des systèmes embarqués avec une faible quantité de mémoire (moins d’un Go), on choisira en général la valeur la plus faible ou on désactivera l’option.

Il existe beaucoup d’autres options dans ces menus. Elles dépendent généralement du support matériel utilisé, et devront donc être configurée en fonction de la plate-forme cible.

Nous examinerons quelques éléments appartenant aux autres menus de configuration dans le prochain article.

 

 

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 options d’un autre menu dans le prochain article

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.

Version Control by Example

Livres | Publié par cpb
fév 08 2012

J’ai reçu il y a quelques jours un exemplaire gratuit du livre « Version Control by Example » d’Eric Sink.

 

Il s’agit d’un ouvrage très intéressant sur le fonctionnement des logiciels de gestion de version avec une présentation détaillée des différentes générations (gestion localisée, gestion centralisée, gestion distribuée) et des outils correspondant (RCS, CVS, Subversion, Mercurial Git, Veracity…).

 

J’ai appris plusieurs détails sur Git et sur les commandes auxquelles je n’étais pas habitué et je vous le conseille chaudement.

 

Le livre est disponible en version PDF sur le blog de l’auteur et il offre même des exemplaires gratuits sur demande. Naturellement on peut aussi l’acheter chez les revendeurs habituels.

Ainsi que son précédent ouvrage :