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

Linux | Publié par cpb
fév 04 2012

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.

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

Linux | Publié par cpb
jan 28 2012

(English translation here)

Lors d’une récente session de formation, une conversation avec un participant m’a poussé à vérifier les options nécessaires pour effectuer du débogage et des tests en couverture sur une bibliothèque partagée.

Une bibliothèque dynamique (fichier libXXXX.soso pour Shared Object) est chargée dans la mémoire du processus au moment de son démarrage. Le fichier exécutable et la bibliothèque sont indépendants avant le lancement de l’application, et peuvent être maintenus séparément.

J’ai réalisé que certains points étaient loin d’être évidents, par exemple la gestion des numéros de version ou l’activation des tests en couverture. Voici un petit récapitulatif des étapes de mise au point d’une bibliothèque dynamique. Le premier article est consacré à la compilation, la gestion des versions et la création des liens symboliques nécessaires. Le second s’intéressera au débogage et au suivi pas-à-pas du code de la bibliothèque depuis une application. Le troisième décrira comment effectuer des tests en couverture sur le contenu de la bibliothèque.

Compilation et installation de la bibliothèque

Compilation du code de la bibliothèque

Commençons par créer une petite bibliothèque dynamique, avec une fonction relativement simple : l’implémentation de la fonction mathématique « factorielle ».

Je crée un répertoire de travail factorielle regroupant tous les fichiers concernant cette bibliothèque. Nous y créons trois sous-répertoires :

  • src/ qui contiendra le code source de la bibliothèque,
  • lib/ où seront regoupés les fichiers binaires et les liens symboliques décrits plus bas,
  • include/ dans lequel les fichiers d’en-tête de la bibliothèque seront stockés.
[~]$ mkdir factorielle
[~]$ mkdir factorielle/src
[~]$ mkdir factorielle/include
[~]$ mkdir factorielle/lib
[~]$ cd factorielle
[factorielle]$

Créons un fichier src/fact.c implémentant notre fonction.

Et si vous pensez avoir trouvé un bug dans le code ci-dessous, ayez la gentillesse de lire l’article en entier avant de m’envoyer un mail de moquerie ;-)

#include <fact.h>

long long int factorielle(long int n)
{
        long long int f = 1;
        do {
                f = f * n;
                n = n - 1;
        } while (n > 1);
        return f;
}

Ce fichier commence par inclure son propre fichier d’en-tête, ce qui permet de s’assurer à la compilation de la concordance du prototype et de l’implémentation.

Le fichier include/fact.h contient les lignes suivantes.

#ifndef LIB_FACT_H
#define LIB_FACT_H
    long long int factorielle(long int n);
#endif

Lors de la compilation de ce fichier nous fournirons sur la ligne de commande de gcc les options:

  • -c pour arrêter gcc après la phase de compilation et obtenir ainsi un fichier objet (pas d’édition des liens).
  • -I include/ qui indique à gcc de rechercher les fichiers d’en-tête .h dans le répertoire include/ en supplément des répertoires usuels (/usr/include…).
  • -fPIC pour demander la génération d’un code relogeable (Position Independant Code) comme c’est nécessaire pour la création de bibliothèques partagées même si cette option n’est pas  indispensable sur certaines architectures (x86 32 bits par exemple).

Voici un exemple de compilation.

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

Cette compilation a généré un fichier objet fact.o que nous utiliserons ci-après.

On notera que durant la phase de test et de mise au point, il ne faut utiliser aucune option d’optimisation, sinon le compilateur risque de modifier le code exécutable créé (en regroupant des blocs de code par exemple) et il n’y aura plus de correspondance exacte avec le fichier source.

Génération de la bibliothèque

La bibliothèque proprement dite est obtenue en invoquant gcc avec l’option -shared. Nous allons lui demander d’enregistrer la bibliothèque dans le fichier libfact.so.1.0. Les numéros 1 et 0 correspondent respectivement aux numéros majeur et mineur de version de la bibliothèque.

Il est d’usage de considérer qu’un changement de numéro majeur représente une rupture de la compatibilité binaire de la bibliothèque et nécessite une recompilation des applications, alors qu’une variation du numéro mineur signifie des corrections ou des améliorations internes n’influant pas sur l’interface de programmation.

Nous allons indiquer à gcc d’enregistrer dans l’en-tête de la bibliothèque son nom officiel incluant le numéro majeur de version avec l’option -Wl. Celle-ci transmet au linker la chaîne de caractères qui la suit après avoir remplacé les virgules par des espaces. C’est donc l’option -soname libfact.so.1 qui est passée.

Il est conseillé de répêter les options passées lors de la compilation précédente, comme -fPIC.

[factorielle]$ gcc -fPIC -shared -Wl,-soname,libfact.so.1 -o lib/libfact.so.1.0 src/fact.o
[factorielle]$ ls lib/
libfact.so.1.0
[factorielle]$

Nous disposons donc d’un fichier libfact.so.1.0 dont l’en-tête contient le nom libfact.so.1

Création des liens symboliques

Lorsque nous compilerons une application, nous préciserons à gcc de la lier avec la bibliothèque fact. Celui-ci recherchera un fichier libfact.so. Et non pas libfact.so.1.0. Aussi va-t-il falloir créer un lien symbolique pour indiquer le chemin vers le fichier. Ce lien est créé manuellement avec la commande ln.

[factorielle]$ cd lib/
[lib]$ ln -sf libfact.so.1.0 libfact.so
[lib]$ ls -l lib*
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:04 libfact.so -> libfact.so.1.0
-rwxrwxr-x 1 cpb cpb 6659 2012-01-27 10:02 libfact.so.1.0
[lib]$

Lors de la compilation, gcc enregistrera dans le fichier exécutable généré le nom de la bibliothèque qu’il a utilisé. Il s’agit du nom « officiel » qu’il trouve dans la section SONAME que nous avons renseignée avec l’argument -Wl,-soname précédement.

A l’exécution, le chargeur recherche donc la bibliothèque dont le numéro majeur correspond à celui utilisé lors de la compilation. Il va donc falloir qu’il trouve un fichier libfact.so.1, ou plutôt un lien libfact.so.1 qui pointe vers libfact.so.1.0.

La création du premier lien symbolique était nécessaire pour pouvoir compiler une application avec la bibliothèque, le second lien est indispensable pour pouvoir exécuter un programme lié avec elle. Ce lien est donc utilisé beaucoup plus fréquemment que le précédent. Pour simplifier la vie de l’administrateur, une commande nommée ldconfig va l’aider à créer automatiquement les liens dont son système a besoin pour que les utilisateurs puissent exécuter les applications. Elle parcourt les répertoires-système contenant des bibliothèques (/lib, /usr/lib, /usr/local/lib, etc. plus tous ceux indiqués dans /etc/ld.so.conf) et crée sur chaque fichier de bibliothèque un lien avec le nom contenu dans sa section SONAME. Nous allons en voir un exemple en forçant ldconfig à explorer uniquement notre répertoire grâce à son option -n.

[lib]$ ldconfig -n .
[lib]$ ls -l lib*
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:04 libfact.so -> libfact.so.1.0
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:05 libfact.so.1 -> libfact.so.1.0
-rwxrwxr-x 1 cpb cpb 6659 2012-01-27 10:02 libfact.so.1.0
[lib]$

Les liens présents permettront donc de compiler une application nécessitant notre bibliothèque (par le biais de libfact.so) puis de l’exécuter en s’assurant que la version majeure soit la bonne (grâce à libfact.so.1).

Utilisation de la bibliothèque

Compilation d’une application

Écrivons un petit programme qui utilise notre bibliothèque. Le fichier factorielle.c va invoquer notre fonction factorielle() sur tous les nombres fournis sur sa ligne de commande.

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

int main (int argc, char * argv[])
{
        long int n;
        int i;
        if (argc < 2) {
                fprintf(stderr, "usage: %s valeurs...\n", argv[0]);
                exit(EXIT_FAILURE);
        }
        for (i = 1; i < argc; i ++)
                if (sscanf(argv[i], "%ld", & n) == 1)
                        fprintf(stdout, "%ld! = %lld\n", n, factorielle(n));
        return EXIT_SUCCESS;
}

Ce programme se trouve dans le répertoire factorielle/test/ que nous créons pour l’occasion. Il inclut le fichier d’en-tête <fact.h>. Il faudra donc que le compilateur puisse le trouver. Pour cela deux solutions :

  • placer le fichier d’en-tête dans /usr/include, /usr/local/include ou tout autre répertoire que gcc consulte – ceci doit être réservé aux fichiers cruciaux pour plusieurs applications utiles pour l’ensemble du système,
  • laisser le fichier dans un répertoire spécifique à notre bibliothèque et indiquer à gcc où le trouver.

C’est naturellement la seconde option que je vais utiliser.

En outre, nous ajouterons en fin de ligne l’argument -lfact qui demande au linker de réaliser l’édition des liens avec la bibliothèque libfact.so. Comme pour le fichier d’en-tête il faudra préciser à gcc où il pourra trouver le fichier libfact.so que nous avons créé plus haut sous forme de lien symbolique. C’est le rôle de l’option -L.

[factorielle]$ gcc -I ./include/ -L ./lib/ -o ./test/factorielle ./test/factorielle.c -lfact
[factorielle]$ 
[factorielle]$ ls -l test/
total 12
-rwxrwxr-x 1 cpb cpb 7359 2012-01-27 10:41 factorielle
-rw-r--r-- 1 cpb cpb  382 2012-01-25 18:29 factorielle.c
[factorielle]$

Exécution de l’application

Si nous testons directement notre programme, son exécution échoue.

$ ./test/factorielle 4 5 6
./test/factorielle: error while loading shared libraries: libfact.so.1: cannot open shared object file: No such file or directory
$

En effet, l’éditeur de liens dynamique qui doit démarrer le processus ne sait pas où trouver la bibliothèque. On remarque au passage qu’il recherche bien le fichier libfact.so.1 (avec le numéro majeur comme extension). Si notre application est suffisamment importante pour être employée régulièrement par différents utilisateurs, il est légitime de placer les fichiers de bibliothèque dans /usr/local/lib où le linker les trouvera. Toutefois si l’application est en phase de mise au point ou réservée à un emploi rare, on préférera laisser les bibliothèques dans un répertoire personnel. Dans ce cas, il faudra remplir (éventuellement dans un script de lancement) la variable d’environnement LD_LIBRARY_PATH pour ajouter le chemin d’accès à ces fichiers.

[factorielle]$ export LD_LIBRARY_PATH=./lib/ 
[factorielle]$ ./test/factorielle 4 5 6
4! = 24
5! = 120
6! = 720
[factorielle]$

Bien sûr, le contenu de la variable LD_LIBRARY_PATH peut être renseigné avec un chemin absolu plutôt que relatif si on souhaite lancer le programme exécutable depuis un emplacement quelconque.

Bibliothèque dynamique version 1.0

Bibliothèque dynamique version 1.0

Maintenance de la bibliothèque

Modification de version mineure

Notre bibliothèque semble fonctionner, nous pouvons commençer à nous livrer à des tests intensifs :

[factorielle]$ ./test/factorielle 3
3! = 6
[factorielle]$

Très bien !

[factorielle]$ ./test/factorielle 2
2! = 2
[factorielle]$

Parfait !

[factorielle]$ ./test/factorielle 1
1! = 1
[factorielle]$

Aucun souci.

[factorielle]$ ./test/factorielle 0
0! = 0
[factorielle]$

Aïe !

Et oui, par convention, il est posé que 0! = 1 (vous pouvez vérifier sur Wikipédia si vous le souhaitez). Notre programme est donc défectueux. La correction est relativement simple, il suffit de remplacer la boucle

        do {
                f = f * n;
                n = n - 1;
        } while (n > 1);

par

        while (n > 1) {
                f = f * n;
                n = n - 1;
        }

C’est ce que j’ai fait dans le fichier fact-2.c. En principe je devrais garder le même nom de fichier source et le remplacer simplement pour la nouvelle version de la bibliothèque. Je veux ici conserver la version précédente simplement à titre pédagogique.

Je vais le compiler, puis générer une nouvelle version de bibliothèque en incrémentant le numéro mineur. L’interface de la fonction n’étant pas modifiée, les fichiers exécutables qui en dépendent doivent continuer à fonctionner normalement.

[factorielle]$ gcc -c -I include/ -fPIC -Wall -o src/fact.o src/fact-2.c
[factorielle]$ gcc -fPIC -shared -Wl,-soname,libfact.so.1 -o lib/libfact.so.1.1 src/fact.o

Notre bibliothèque a été re-générée dans un nouveau nom de fichier, aussi faut-il relancer la commande ldconfig.

[factorielle]$ ldconfig -n lib/
[factorielle]$ ls -l lib/
total 16
lrwxrwxrwx 1 cpb cpb   12 2012-01-27 10:04 libfact.so -> libfact.so.1
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:11 libfact.so.1 -> libfact.so.1.1
-rwxrwxr-x 1 cpb cpb 6659 2012-01-27 10:02 libfact.so.1.0
-rwxrwxr-x 1 cpb cpb 6661 2012-01-27 10:10 libfact.so.1.1
[factorielle]$
[factorielle]$ ./test/factorielle 0 1 2
0! = 1
1! = 1
2! = 2
[factorielle]$

Notre programme fonctionne correctement pour 0!, et l’ancienne version de la bibliothèque n’étant plus utilisée, il est possible de l’effacer.

[factorielle]$ rm -f lib/libfact.so.1.0 
[factorielle]$
Bibliothèque dynamique version 1.1

Bibliothèque dynamique version 1.1

Modification de version majeure

Après quelques essais, nous arrivons face à un nouveau problème avec notre bibliothèque.

[factorielle]$ ./test/factorielle -3
-3! = 1
[factorielle]$

Notre fonction renvoie une valeur lorsqu’on lui passe un nombre négatif. La véritable factorielle mathématique n’est définie que sur l’ensemble des entiers naturels, pas pour les entiers relatifs négatifs. Notre fonction devrait donc signaler l’erreur d’argument et non pas renvoyer une valeur, cohérente il est vrai mais trompeuse.

Nous choisissons de modifier l’interface de notre routine, qui va prendre en argument un pointeur sur un entier long long dans lequel elle stockera le résultat, et renverra une valeur de réussite (zéro) ou d’échec (-1). Cette modification d’interface va impliquer une adaptation et une recompilation des applications utilisant la bibliothèque, aussi devrons-nous changer de version majeure.

La nouvelle fonction fact-3.c est définie comme suit.

int factorielle(long int n, long long int * result)
{
        * result = 1;
        if (n < 0)
                return -1;
         do {
                 (*result) = (*result) * n;
                 n = n - 1;
         } while (n > 1);
        return 0;
}

Bien sur, on modifie le fichier d’en-tête en (fact-3.h):

#ifndef LIB_FACT_H
#define LIB_FACT_H
        int factorielle(long int n, long long int * result);
#endif

Compilons notre bibliothèque comme précédemment.

[factorielle]$ gcc -c -I include/ -fPIC -Wall -o src/fact.o src/fact-3.c
[factorielle]$ gcc -fPIC -shared -Wl,-soname,libfact.so.2 -o lib/libfact.so.2.0 src/fact.o
[factorielle]$ ldconfig -n lib/
[factorielle]$ ls -l lib/
total 16
lrwxrwxrwx 1 cpb cpb   12 2012-01-27 10:04 libfact.so -> libfact.so.1
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:11 libfact.so.1 -> libfact.so.1.1
-rwxrwxr-x 1 cpb cpb 6661 2012-01-27 10:10 libfact.so.1.1
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:26 libfact.so.2 -> libfact.so.2.0
-rwxrwxr-x 1 cpb cpb 6661 2012-01-27 10:26 libfact.so.2.0
[factorielle]$ cd lib/
[lib]$ ln -sf libfact.so.2 libfact.so
[lib]$ ls -l
total 16
lrwxrwxrwx 1 cpb cpb   12 2012-01-27 10:27 libfact.so -> libfact.so.2
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:11 libfact.so.1 -> libfact.so.1.1
-rwxrwxr-x 1 cpb cpb 6661 2012-01-27 10:10 libfact.so.1.1
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:26 libfact.so.2 -> libfact.so.2.0
-rwxrwxr-x 1 cpb cpb 6661 2012-01-27 10:26 libfact.so.2.0
[lib]$ cd ..
[factorielle]$

Les liens sont en place pour compiler une nouvelle version du programme de test (factorielle-2.c).

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

int main (int argc, char * argv[])
{
        long int n;
        long long int f;
        int i;
        if (argc < 2) {
                fprintf(stderr, "usage: %s valeurs...\n", argv[0]);
                exit(EXIT_FAILURE);
        }
        for (i = 1; i < argc; i ++)
                if (sscanf(argv[i], "%ld", & n) == 1) {
                        if (factorielle(n, & f) == 0)
                                fprintf(stdout, "%ld! = %lld\n", n, f);
                        else
                                fprintf(stdout, "%ld! n'existe pas\n", n);
                }
        return EXIT_SUCCESS;
}

Compilons et essayons-le :

[factorielle]$ gcc -I ./include/ -L ./lib/ -o ./test/factorielle-2 ./test/factorielle-2.c -lfact
[factorielle]$ ./test/factorielle-2 3 0 -3
3! = 6
0! = 0
-3! n'existe pas
[factorielle]$

Cette fois notre programme se comporte correctement. On peut noter que la présence de l’ancienne version majeure permet à notre précédent exécutable de continuer à fonctionner.

[factorielle]$ ./test/factorielle 3 0 -3
3! = 6
0! = 1
-3! = 1
[factorielle]$
Bibliothèque dynamique version 2.0

Bibliothèque dynamique version 2.0

Conclusion

La gestion des numéros majeurs et mineurs de version pour les bibliothèques dynamiques offre les avantages suivants :

  • Les modifications internes uniquement, représentées par des évolutions du numéro mineur, permettent aux exécutables déjà compilés de fonctionner directement avec la nouvelle version de la bibliothèque et de bénéficier – sans recompilation – des améliorations.
  • Les transformations de l’interface externe de la bibliothèque impliquent une recompilation (éventuellement après adaptation) pour pouvoir fonctionner.

Plusieurs versions majeures de la bibliothèque peuvent cohabiter simultanément permettant un fonctionnement correct de différentes générations d’une application. Toutefois les nouvelles compilations utiliseront la version majeure pointée par le lien symbolique contenant uniquement le nom de la bibliothèque (libfact.so)

Nous verrons dans le prochain article comment déboguer le code de la bibliothèque dynamique en effectuant un suivi pas-à-pas de l’exécution et en examinant le contenu de ses variables.

Development of a dynamic library (1/3)

Linux | Publié par cpb
jan 28 2012

(Version originale en français)

During a recent training session, a conversation with a participant gave me the idea to check the options needed to perform debugging and coverage tests on a shared library.

A dynamic library (libXXXX.so file – « so » standing for Shared Object) is loaded into memory when the process starts. The executable file and the library file are independent before launching the application, and can be maintained separately.

I realized that some points were far from obvious, such as managing version numbers or activating coverage tests. Here is a list of the steps needed for the development of a dynamic library. The first article is devoted to compilation, version control and symbolic links. The second will focus on debugging and step-by-step tracing of the library code. The third will describe how to perform coverage tests on the library.

Compiling and installing the library

Compiling library code

Let’s start by creating a small dynamic library, with a simple function: the implementation of the mathematical « factorial ».

I create a working directory named factorial including all the files. Then we make three sub-directories:

  • src/ containing the source code of the library,
  • lib/ where stand binary files and symbolic links described below,
  • include/ storing the header files of the library.
[~]$ mkdir factorial
[~]$ mkdir factorial/src
[~]$ mkdir factorial/include
[~]$ mkdir factorial/lib
[~]$ cd factorial
[factorial]$

Let’s create a file named src/fact.c containing our function.

And if you think you have found a bug in the code below, be kind and read the entire article before sending me a mocking mail ;-)

#include <fact.h>

long long int factorial(long int n)
{
        long long int f = 1;
        do {
                f = f * n;
                n = n - 1;
        } while (n > 1);
        return f;
}

This file includes its own header, which ensures consistency of the prototype and implementation.

The file include/fact.h contains the following lines.

#ifndef LIB_FACT_H
#define LIB_FACT_H
    long long int factorial(long int n);
#endif

When compiling this file we will provide the following options on the gcc command line:

  • -c to tell gcc to stop his job after the compilation phase and thus providing an object file (not linking).
  • -I include/ telling gcc to look for .h header files in the include/ directory in addition to the usual directories (/usr/include…).
  • -fPIC to request the generation of a relocatable code (PIC stands for Position Independent Code). It is necessary for the creation of shared libraries even if this option has no effect on some architectures (x86 32-bit for example).

Here is an example of compilation:

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

The generated fact.o object file will be used below.

Note that during the development and testing phase, we do not use any optimization option, otherwise the compiler may change the executable code created and there won’t be an exact matching with the source file.

Generation of the library

The library itself is created by invoking gcc with the -shared option. We ask him to save the library in the libfact.so.1.0 file. The numbers 1 and 0 correspond respectively to the major and minor numbers of the library version.

It is customary to consider that a major number change represents a break in binary compatibility of the library and requires recompilation of applications, while a variation of the minor number represents only internal corrections or improvements that do not interfere with the programming interface.

We will tell gcc with the -Wl option to record in the heade rof the library its official name of the library including the major version number. It passes to the linker the string that follows the -Wl option after replacing commas by spaces. Thus the linker gets the -soname libfact.so.1 string.

It is recommanded to repeat the options passed in the previous compilation, such as -fPIC.

[factorial]$ gcc -fPIC -shared -Wl,-soname,libfact.so.1 -o lib/libfact.so.1.0 src/fact.o
[factorial]$ ls lib/
libfact.so.1.0
[factorial]$

So we get the libfact.so.1.0 file, whose header contains the libfact.so.1 name.

Creating symbolic links

When we compile an application, we tell gcc to link it with the fact library. He looks for a file named libfact.so, not libfact.so.1.0. So we have to create a symbolic link named libfact.so pointing to the real library file. This link is created manually using the ln command.

[factorial]$ cd lib/
[lib]$ ln -sf libfact.so.1.0 libfact.so
[lib]$ ls -l lib*
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:04 libfact.so -> libfact.so.1.0
-rwxrwxr-x 1 cpb cpb 6659 2012-01-27 10:02 libfact.so.1.0
[lib]$

During compilation gcc records the name of the library he used into the executable. This is the « official » name found in the SONAME section we filled previously with the -Wl,-soname option.

At runtime, the loader searches the library which major number matches those used during compilation. So he has to find a file named libfact.so.1, or rather a symbolic link named libfact.so.1 pointing to libfact.so.1.0.

The creation of the first symbolic link was needed to compile an application with the library, the second link is essential to run a program associated with it. This link is used much more frequently than the first one. To make the life of the administrator easier, a command named ldconfig will help to automatically create the links needed to allow users to run applications. It searches the directories containing system libraries (/lib, /usr/lib, /usr/local/lib, etc… and all given in /etc/ld.so.conf) and creates links on each library file with the name contained in the section. Let’s see an example where I force ldconfig through its -n option to explore only our lib/ directory.

[lib]$ ldconfig -n .
[lib]$ ls -l lib*
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:04 libfact.so -> libfact.so.1.0
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:05 libfact.so.1 -> libfact.so.1.0
-rwxrwxr-x 1 cpb cpb 6659 2012-01-27 10:02 libfact.so.1.0
[lib]$

The links will allow us to compile an application that requires our library (through libfact.so) and then run it by making sure the major version is the right one (with libfact.so.1).

Using the library

Compiling an application

Let’s write a small program that uses our library. The factorial.c file will call our factorial() function for all the numbers found on the command line.

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

int main (int argc, char * argv[])
{
        long int n;
        int i;
        if (argc < 2) {
                fprintf(stderr, "usage: %s value...\n", argv[0]);
                exit(EXIT_FAILURE);
        }
        for (i = 1; i < argc; i ++)
                if (sscanf(argv[i], "%ld", & n) == 1)
                        fprintf(stdout, "%ld! = %lld\n", n, factorial(n));
        return EXIT_SUCCESS;
}

This file is located in the factorial/test/ directory that we create now. It includes the <fact.h> header file. So the compiler has to find this header file. Two solutions:

  • Put the header file in /usr/include, /usr/local/include or any other directory where gcc searches. This should be reserved for critical files, needed by several applications and useful for the entire system.
  • Keep the file in an application specific directory and tell gcc where to find it.

I will obviously choose the second one.

In addition, we will put the -lfact option at the end of the command line, asking the linker to perform the linking with the libfact.so library. As for the header file, gcc must be told where he can find the libfact.so file we created previously as symbolic link. It is the role of the -L option.

[factorial]$ gcc -I ./include/ -L ./lib/ -o ./test/factorial ./test/factorial.c -lfact
[factorial]$ 
[factorial]$ ls -l test/
total 12
-rwxrwxr-x 1 cpb cpb 7359 2012-01-27 10:41 factorial
-rw-r--r-- 1 cpb cpb  382 2012-01-25 18:29 factorial.c
[factorielle]$

Running the application

If we run our program directly, the execution fails.

$ ./test/factorial 4 5 6
./test/factorial: error while loading shared libraries: libfact.so.1: cannot open shared object file: No such file or directory
$

Indeed, the dynamic linker which should start the process does not know where to find the library. We can see that it searches for the libfact.so.1 file (with the major number as extension). If our application is important enough to be used regularly by different users, it is legitimate to place library files in /usr/local/lib where the loader will find them. However if the application is currently under development or reserved for personnal use, it is preferable to leave the library in a sub-directory of our home directory. In this case, we must be fill (possibly in a startup script) the environment variable LD_LIBRARY_PATH to add the path to the library file.

[factorial]$ export LD_LIBRARY_PATH=./lib/ 
[factorial]$ ./test/factorial 4 5 6
4! = 24
5! = 120
6! = 720
[factorial]$

Of course, the LD_LIBRARY_PATH variable can be given an absolute path rather than a relative one if you want to lauch the application from any location of the filesystem tree.

Dynamic library v. 1.0

Dynamic library v. 1.0

Maintaining the library

Minor version update

Our library seems to works, let’s engage intensive testings:

[factorial]$ ./test/factorial 3
3! = 6
[factorial]$

Very good!

[factorial]$ ./test/factorial 2
2! = 2
[factorial]$

Perfect!

[factorial]$ ./test/factorial 1
1! = 1
[factorial]$

No problem.

[factorial]$ ./test/factorial 0
0! = 0
[factorial]$

Ouch!

By convention, it is hypothesized that 0! = 1 (you can check on Wikipedia if you wish). Our program is defective. The correction is fairly simple, just replace the loop

        do {
                f = f * n;
                n = n - 1;
        } while (n > 1);

by

        while (n > 1) {
                f = f * n;
                n = n - 1;
        }

That’s what I did in fact-2.c file. Theoretically I should keep the same source file and replace it in the new version of the library. I wanted to keep the previous version here for demonstration purposes so did I rename the file.

I will compile and generate a new library version incrementing the minor number. The interface of the factorial() function is not changed, executable files that depend on the library will continue to operate normally.

[factorial]$ gcc -c -I include/ -fPIC -Wall -o src/fact.o src/fact-2.c
[factorial]$ gcc -fPIC -shared -Wl,-soname,libfact.so.1 -o lib/libfact.so.1.1 src/fact.o

Our library has been re-created with a new file name, so it’s necessary to rerun the ldconfig command.

[factorial]$ ldconfig -n lib/
[factorial]$ ls -l lib/
total 16
lrwxrwxrwx 1 cpb cpb   12 2012-01-27 10:04 libfact.so -> libfact.so.1
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:11 libfact.so.1 -> libfact.so.1.1
-rwxrwxr-x 1 cpb cpb 6659 2012-01-27 10:02 libfact.so.1.0
-rwxrwxr-x 1 cpb cpb 6661 2012-01-27 10:10 libfact.so.1.1
[factorial]$
[factorial]$ ./test/factorial 0 1 2
0! = 1
1! = 1
2! = 2
[factorial]$

Our code works correctly for 0!. The previous version of the library is no more used, so we can delete it

[factorial]$ rm -f lib/libfact.so.1.0 
[factorial]$
Dynamic library v. 1.1

Dynamic library v. 1.1

Major version update

After a few tests, we are facing a new problem with our library.

[factorial]$ ./test/factorial -3
-3! = 1
[factorial]$

Our function returns a value when given a negative number. in mathematics, the factorial is only defined for natural numbers, not for negative integers. The function should report the argument error and not return a value (coherent but misleading).

We choose to modify the interface of our routine, which will take as argument a pointer to a long long integer where it will store the result and return a success (zero) or failure (-1) value. This change will involve an interface adaptation and recompilation of the applications using the library. So must we change the major version.

The new function in fact-3.c is implemented as follow:

int factorial(long int n, long long int * result)
{
        * result = 1;
        if (n < 0)
                return -1;
         do {
                 (*result) = (*result) * n;
                 n = n - 1;
         } while (n > 1);
        return 0;
}

Of course, we change the header file (fact-3.h):

#ifndef LIB_FACT_H
#define LIB_FACT_H
        int factorial(long int n, long long int * result);
#endif

We compile our library as we did before:

[factorial]$ gcc -c -I include/ -fPIC -Wall -o src/fact.o src/fact-3.c
[factorial]$ gcc -fPIC -shared -Wl,-soname,libfact.so.2 -o lib/libfact.so.2.0 src/fact.o
[factorial]$ ldconfig -n lib/
[factorial]$ ls -l lib/
total 16
lrwxrwxrwx 1 cpb cpb   12 2012-01-27 10:04 libfact.so -> libfact.so.1
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:11 libfact.so.1 -> libfact.so.1.1
-rwxrwxr-x 1 cpb cpb 6661 2012-01-27 10:10 libfact.so.1.1
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:26 libfact.so.2 -> libfact.so.2.0
-rwxrwxr-x 1 cpb cpb 6661 2012-01-27 10:26 libfact.so.2.0
[factorial]$ cd lib/
[lib]$ ln -sf libfact.so.2 libfact.so
[lib]$ ls -l
total 16
lrwxrwxrwx 1 cpb cpb   12 2012-01-27 10:27 libfact.so -> libfact.so.2
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:11 libfact.so.1 -> libfact.so.1.1
-rwxrwxr-x 1 cpb cpb 6661 2012-01-27 10:10 libfact.so.1.1
lrwxrwxrwx 1 cpb cpb   14 2012-01-27 10:26 libfact.so.2 -> libfact.so.2.0
-rwxrwxr-x 1 cpb cpb 6661 2012-01-27 10:26 libfact.so.2.0
[lib]$ cd ..
[factorial]$

The links are in place to compile a new version of the test program (factorial-2.c).

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

int main (int argc, char * argv[])
{
        long int n;
        long long int f;
        int i;
        if (argc < 2) {
                fprintf(stderr, "usage: %s value...\n", argv[0]);
                exit(EXIT_FAILURE);
        }
        for (i = 1; i < argc; i ++)
                if (sscanf(argv[i], "%ld", & n) == 1) {
                        if (factoriel(n, & f) == 0)
                                fprintf(stdout, "%ld! = %lld\n", n, f);
                        else
                                fprintf(stdout, "%ld! doesn't exist\n", n);
                }
        return EXIT_SUCCESS;
}

Let us compile and try it:

[factorial]$ gcc -I ./include/ -L ./lib/ -o ./test/factorial-2 ./test/factorial-2.c -lfact
[factorial]$ ./test/factorial-2 3 0 -3
3! = 6
0! = 0
-3! doesn't exist
[factorial]$

This time our application behaves correctly. We can remark that the presence of the former major release enables our previous executable to continue operating.

[factorial]$ ./test/factorial 3 0 -3
3! = 6
0! = 1
-3! = 1
[factorial]$
Dynamic library v. 2.0

Dynamic library v. 2.0

Conclusion

The management of major and minor numbers of dynamic library versions offers the following advantages:

  • The internal only modifications, represented by changes in the minor number, allow already compiled executables to run directly with the new version of the library and enjoy – without recompilation – the latest improvements.
  • Changes in the external interface of the library require a recompilation (possibly after adaptation) of the application.

Several major versions of the same library can coexist simultaneously for proper operation of different generations of an application. However, the compiler uses the new major version pointed to by the main symbolic link of the library (libfact.so)

We will see in the next article how to debug the code in the dynamic library, tracing it with step-by-step execution and examining the contents of its variables.

Parallélisation de compilations

Linux, Microprocesseur | Publié par cpb
jan 14 2012

(English translation here)

Il m’arrive très fréquemment de compiler des noyaux Linux, souvent durant des sessions de formation ou des prestations d’ingénierie (principalement dans le domaine de l’embarqué ou le développement de drivers), et parfois à titre expérimental ou par simple curiosité pour rédiger des articles ou mon prochain livre.

La durée de compilation varie beaucoup en fonction de la quantité de code (de drivers, systèmes de fichiers, protocoles, etc.) et de la puissance de la machine hôte. Sur un PC de milieu de gamme, la compilation d’un kernel ajusté pour un système embarqué dure environ trois minutes. Sur une machine d’entrée de gamme (ou un peu ancienne), la compilation d’un noyau générique pour PC (disposant donc de centaines de drivers sous forme de modules) peut durer une heure.

Pour tirer parti du parallélisme proposé par les processeurs actuels (systèmes multiprocesseurs, multicoeurs ou avec hyper-threading), la commande make nous permet de lancer simultanément plusieurs jobs. Ainsi

$ make -j 4

s’arrangera pour qu’il y ait toujours quatre jobs de compilation actifs.

J’ai longtemps répété que « si vous avez N processeurs (ou coeurs, ou CPU virtuels) disponibles, vous gagnerez du temps de compilation en lançant 2N jobs en parallèle« . Ceci repose sur l’idée que pour chaque processeur nous avons un job qui effectue de la compilation (en consommant du temps CPU) et tandis qu’un autre job peut terminer de sauvegarder les résultats de la compilation précédente ou charger le fichier source du traitement suivant. Mais… est-ce vrai ?

Script de test

Pour en avoir le coeur net, j’ai écrit le petit script suivant, qui télécharge au besoin les sources d’un noyau et les décompresse, puis réalise plusieurs compilations en démarrant un nombre variable de jobs.

Par exemple si on lance

$ ./test-make-j.sh 3 5 8

Il effectue trois compilations complètes : l’une avec trois tâches en parallèle, la suivante avec cinq jobs et la dernière avec huit, les résultats étant cumulés dans un fichier de texte. Le script est le suivant.

test-make-j.sh 
#! /bin/sh

KERNEL_VERSION="linux-3.2"
KERNEL_URL_PATH="www.kernel.org/pub/linux/kernel/v3.0/"
RESULT_FILE="compilation-timing.txt"

if [ "$#" -eq 0 ]
then
  echo "usage: $@ jobs_number..." >& 2
  exit 0
fi

if [ ! -d "${KERNEL_VERSION}" ]
then
  if [ ! -f "${KERNEL_VERSION}.tar.bz2" ]
  then
    wget "${KERNEL_URL_PATH}/${KERNEL_VERSION}.tar.bz2"
    if [ $? -ne 0 ] || [ ! -f "${KERNEL_VERSION}.tar.bz2" ]
    then
      echo "unable to obtain ${KERNEL_VERSION} archive" >&2
      exit 1
    fi
  fi
  tar xjf "${KERNEL_VERSION}.tar.bz2"
  if [ $? -ne 0 ]
  then
    echo "Error while uncompressing kernel archive" >&2
    exit 1
  fi
fi

cd "${KERNEL_VERSION}"

echo "# Timings of ${KERNEL_VERSION} compilations" >> "${RESULT_FILE}"
nb_cpu=$(grep "^processor" /proc/cpuinfo | wc -l)

echo "# Processors: ${nb_cpu}" >> "${RESULT_FILE}"
affinity=$(taskset -p $$ | sed -e 's/^.*://') >> "${RESULT_FILE}"

echo "# Affinity mask: ${affinity}" >> "${RESULT_FILE}"
for nb in "$@"
do
  echo "# Compiling with $nb simultaneous jobs" >> "${RESULT_FILE}"
  make mrproper
  make i386_defconfig
  sync
  sleep 10 # Let's all calm down
  start=$(date "+%s")
  make -j $nb
  sync
  end=$(date "+%s")
  # This script will fail during february 2038 ;-)
  echo "$nb     $((end - start))" >> "${RESULT_FILE}"
done

Résultats

Voici les résultats d’une exécution sur un processeur Intel Q6600 Quad-Core (fichier Intel-Q6600-1.txt)

# Timings of linux-3.2 compilations
# Processors: 4
# Affinity mask:  f
# Compiling with 1 simultaneous jobs
1     675
# Compiling with 2 simultaneous jobs
2     346
# Compiling with 3 simultaneous jobs
3     241
# Compiling with 4 simultaneous jobs
4     197
# Compiling with 5 simultaneous jobs
5     198
# Compiling with 6 simultaneous jobs
6     194
# Compiling with 7 simultaneous jobs
7     195
# Compiling with 8 simultaneous jobs
8     196
# Compiling with 9 simultaneous jobs
9     197
# Compiling with 10 simultaneous jobs
10     198
# Compiling with 11 simultaneous jobs
11     198
# Compiling with 12 simultaneous jobs
12     198
# Compiling with 13 simultaneous jobs
13     200
# Compiling with 14 simultaneous jobs
14     201
# Compiling with 15 simultaneous jobs
15     201
# Compiling with 16 simultaneous jobs
16     200

Observons-les graphiquement avec cette petite ligne de commande pour Gnuplot. Horizontalement, nous voyons le nombre de jobs simultanés et verticalement le temps de compilation en secondes.

$ echo "set terminal png size 640,480 ; set output './Intel-Q6600-1.png'; plot 'Intel-Q6600-1.txt' with linespoints" | gnuplot

 

Compilations parallèles sur quatre CPU

Compilations parallèles sur quatre CPU

Visiblement, les meilleurs résultats sont atteints (à quelques fluctuations près) dès make -j 4. Essayons de confirmer ceci. Avant de lancer le script, nous le limitons sur deux processeurs avec la commande suivante qui fixe les jobs lancés à partir du shell courant sur les processeurs 2 et 3.

$ taskset -pc 2-3 $$

Voici le résultat (fichier Intel-QL6600-2.txt).

# Timings of linux-3.2 compilations
# Processors: 4
# Affinity mask:  c
# Compiling with 1 simultaneous jobs
1     684
# Compiling with 2 simultaneous jobs
2     360
# Compiling with 3 simultaneous jobs
3     362
# Compiling with 4 simultaneous jobs
4     366
# Compiling with 8 simultaneous jobs
8     370
# Compiling with 16 simultaneous jobs
16     376
# Compiling with 32 simultaneous jobs
32     377
# Compiling with 64 simultaneous jobs
64     378
Compilations parallèles sur deux CPU

Compilations parallèles sur deux CPU

Cette fois, il est visible que le minimum de temps est obtenu avec make -j 2. Si nous répétons l’expérience sur un seul CPU, on obtient les valeurs suivantes (fichier Intel-Q6600-3.txt).

# Timings of linux-3.2 compilations
# Processors: 4
# Affinity mask:  8
# Compiling with 1 simultaneous jobs
1     683
# Compiling with 2 simultaneous jobs
2     698
# Compiling with 3 simultaneous jobs
3     708
# Compiling with 4 simultaneous jobs
4     709
# Compiling with 5 simultaneous jobs
5     719
# Compiling with 6 simultaneous jobs
6     719
# Compiling with 7 simultaneous jobs
7     720
# Compiling with 8 simultaneous jobs
8     724

Ce qui se représente sur le graphique suivant.

Compilations parallèles sur un seul CPU

Compilations parallèles sur un seul CPU

Nous pouvons regrouper ces trois courbes sur un même graphique pour mieux visualiser leurs échelles (je n’ai pas prolongé la courbe de la compilation sur un seul CPU, mais on peut imaginer qu’elle se poursuit avec une légère croissance).

Compilations parallèles sur processeur Q6600

Compilations parallèles sur processeur Q6600

Pour en avoir le coeur net, nous pouvons recommencer l’expérience sur un autre processeur avec deux coeurs (AMD QL66). Les résultats sont les suivants (fichier AMD-QL66-1.txt).

# Timings of linux-3.2 compilations
# Processors: 2
# Affinity mask:  3
# Compiling with 1 simultaneous jobs
1     1113
# Compiling with 2 simultaneous jobs
2     844
# Compiling with 3 simultaneous jobs
3     875
# Compiling with 4 simultaneous jobs
4     863
# Compiling with 5 simultaneous jobs
5     840
# Compiling with 6 simultaneous jobs
6     844
# Compiling with 7 simultaneous jobs
7     844
# Compiling with 8 simultaneous jobs
8     851
Compilations parallèles sur deux CPU

Compilations parallèles sur deux CPU

Essayons une dernière expérience, sur la même machine (deux CPU), en désactivant deux éléments :

  • la lecture anticipée des blocs suivants du disque (qui permet d’améliorer les lectures localisées) avec echo 0 > /sys/block/sda/read_ahead_kb
  • l’écriture différée (de 30 secondes environ) des blocs (qui évite les accès répétitifs au disque en cas de modifications successives) avec mount / -o sync,remount.

 

Cette fois les résultats sont très différents (fichier AMD-QL66-2.txt). Les temps sont beaucoup plus longs que précédemment car à chaque écriture sur le disque, le processus attend que les données soient transmises au périphérique pour continuer son travail.

 Timings of linux-3.2 compilations
# Processors: 2
# Affinity mask:  3
# Compiling with 1 simultaneous jobs
1     3487
# Compiling with 2 simultaneous jobs
2     2562
# Compiling with 3 simultaneous jobs
3     2198
# Compiling with 4 simultaneous jobs
4     1963
# Compiling with 5 simultaneous jobs
5     1779
# Compiling with 6 simultaneous jobs
6     1646
# Compiling with 7 simultaneous jobs
7     1636
# Compiling with 8 simultaneous jobs
8     1602
# Compiling with 9 simultaneous jobs
9     1738
# Compiling with 10 simultaneous jobs
10     1577
Compilations parallèles sans optimisation disque

Compilations parallèles (2 CPU) sans optimisation disque

Ici, la courbe est plus proche de celle que j’imaginais à l’origine. Le fait de placer plusieurs jobs par CPU permet de tirer parti des temps d’attente liés au disque pour avancer dans une autre compilation. Regroupons les deux courbes pour bien voir les durées respectives.

Compilation parallèle sur QL66

Compilations parallèles sur QL66

Conclusion

Nous voyons qu’avec la qualité de l’ordonnanceur d’entrées-sorties (IO Scheduler) de Linux, et la gestion optimisée des périphériques blocs, les meilleurs temps de compilation sont obtenus dès que l’on lance un job par processeur.

Je modifierai donc à l’avenir ma recommandation en « Si vous avez N processeurs disponibles, compilez votre noyau avec  make -j N  pour avoir le meilleur temps d’exécution« .

PS : si vous avez l’occasion de faire fonctionner ce script sur des architectures différentes (8 processeurs, 16 processeurs, etc.) je serai très intéressé par vos résultats.

Parallelizing Compilations

Linux, Microprocesseur | Publié par cpb
jan 14 2012

(Version originale en français)

I very frequently compile Linux kernels, often during training sessions or engineering services (mainly in the field of embedded systems or drivers development), sometimes while writing articles or books.

Compilation time varies greatly depending on the amount of code (drivers, filesystems, protocols, etc.) and on the CPU power of the host machine. On a mid-range PC, compiling a kernel adjusted for an embedded system (with very few drivers) lasts about three minutes. On an entry level machine (or a little old one), compiling a generic kernel for PC (with hundreds of drivers as modules) can last an hour.

To take advantage of the parallelism offered by the current processors (multiprocessor, multicore or hyper-threading), the make command allows us to run multiple jobs. So

$ make -j 4

guarantees there is always four compilation jobs active.

I have long reiterated that « if you have N processors (or cores, or virtual CPUs) available, you will save time by starting 2N compilation jobs in parallel« . The idea is that for every proccessor we have a job that performs the compilation (consuming CPU time) while another job is saving the results of the previous compilation and loading the source file of the next task. But wait… is this true?

Test script

I wrote the following script which downloads and unpack the required kernel sources, then makes several compilations using a variable number of jobs. For example, if we start

$ ./test-make-j.sh 3 5 8

It performs three complete compilations: one with three tasks in parallel, one with the five jobs and the last with eight jobs. The results are recorded in a text file. The script follows.

test-make-j.sh 
#! /bin/sh

KERNEL_VERSION="linux-3.2"
KERNEL_URL_PATH="www.kernel.org/pub/linux/kernel/v3.0/"
RESULT_FILE="compilation-timing.txt"

if [ "$#" -eq 0 ]
then
  echo "usage: $@ jobs_number..." >& 2
  exit 0
fi

if [ ! -d "${KERNEL_VERSION}" ]
then
  if [ ! -f "${KERNEL_VERSION}.tar.bz2" ]
  then
    wget "${KERNEL_URL_PATH}/${KERNEL_VERSION}.tar.bz2"
    if [ $? -ne 0 ] || [ ! -f "${KERNEL_VERSION}.tar.bz2" ]
    then
      echo "unable to obtain ${KERNEL_VERSION} archive" >&2
      exit 1
    fi
  fi
  tar xjf "${KERNEL_VERSION}.tar.bz2"
  if [ $? -ne 0 ]
  then
    echo "Error while uncompressing kernel archive" >&2
    exit 1
  fi
fi

cd "${KERNEL_VERSION}"

echo "# Timings of ${KERNEL_VERSION} compilations" >> "${RESULT_FILE}"
nb_cpu=$(grep "^processor" /proc/cpuinfo | wc -l)

echo "# Processors: ${nb_cpu}" >> "${RESULT_FILE}"
affinity=$(taskset -p $$ | sed -e 's/^.*://') >> "${RESULT_FILE}"

echo "# Affinity mask: ${affinity}" >> "${RESULT_FILE}"
for nb in "$@"
do
  echo "# Compiling with $nb simultaneous jobs" >> "${RESULT_FILE}"
  make mrproper
  make i386_defconfig
  sync
  sleep 10 # Let's all calm down
  start=$(date "+%s")
  make -j $nb
  sync
  end=$(date "+%s")
  # This script will fail during february 2038 ;-)
  echo "$nb     $((end - start))" >> "${RESULT_FILE}"
done

Results

Here are the results of a run on an Intel Q6600 Quad-Core (file: Intel-Q6600-1.txt)

# Timings of linux-3.2 compilations
# Processors: 4
# Affinity mask:  f
# Compiling with 1 simultaneous jobs
1     675
# Compiling with 2 simultaneous jobs
2     346
# Compiling with 3 simultaneous jobs
3     241
# Compiling with 4 simultaneous jobs
4     197
# Compiling with 5 simultaneous jobs
5     198
# Compiling with 6 simultaneous jobs
6     194
# Compiling with 7 simultaneous jobs
7     195
# Compiling with 8 simultaneous jobs
8     196
# Compiling with 9 simultaneous jobs
9     197
# Compiling with 10 simultaneous jobs
10     198
# Compiling with 11 simultaneous jobs
11     198
# Compiling with 12 simultaneous jobs
12     198
# Compiling with 13 simultaneous jobs
13     200
# Compiling with 14 simultaneous jobs
14     201
# Compiling with 15 simultaneous jobs
15     201
# Compiling with 16 simultaneous jobs
16     200

Let’s see them graphically with this little Gnuplot command line. On the horizontal axis, lies the number of concurrent jobs and on the vertical axis is the compilation time (in seconds).

$ echo "set terminal png size 640,480 ; set output './Intel-Q6600-1.png'; plot 'Intel-Q6600-1.txt' with linespoints" | gnuplot

 

Parallel Compilations on 4 CPU

Parallel Compilations on 4 CPU

Apparently, the best results are achieved (with some fluctuations) with make-j 4. Let’s try to confirm this. Before running again the script, we limit it to two processors with the following command which binds on processors 2 and 3 all the jobs launched from the running shell.

$ taskset -pc 2-3 $$

Here are the results (file: Intel-QL6600-2.txt).

# Timings of linux-3.2 compilations
# Processors: 4
# Affinity mask:  c
# Compiling with 1 simultaneous jobs
1     684
# Compiling with 2 simultaneous jobs
2     360
# Compiling with 3 simultaneous jobs
3     362
# Compiling with 4 simultaneous jobs
4     366
# Compiling with 8 simultaneous jobs
8     370
# Compiling with 16 simultaneous jobs
16     376
# Compiling with 32 simultaneous jobs
32     377
# Compiling with 64 simultaneous jobs
64     378
Parallel Compilations on 2 CPU

Parallel Compilations on 2 CPU

This time, it is clear that the minimum time is achieved with make-j 2. If we repeat the experiment on a single CPU, we obtain the following values (file: Intel-Q6600-3.txt).

# Timings of linux-3.2 compilations
# Processors: 4
# Affinity mask:  8
# Compiling with 1 simultaneous jobs
1     683
# Compiling with 2 simultaneous jobs
2     698
# Compiling with 3 simultaneous jobs
3     708
# Compiling with 4 simultaneous jobs
4     709
# Compiling with 5 simultaneous jobs
5     719
# Compiling with 6 simultaneous jobs
6     719
# Compiling with 7 simultaneous jobs
7     720
# Compiling with 8 simultaneous jobs
8     724

Represented on the graph below:

Parallel Compilations on a single CPU

Parallel Compilations on a single CPU

We can group these three curves on a single graph to better see their scales (I have not extended the curve of the compilation on a single CPU, but we can imagine that it continues with a slight increase).

Parallel Compilations on Q6600

Parallel Compilations on Q6600

To be sure, we can repeat the experiment on a different processor with two cores (AMD QL66). The results are as follows (file: AMD-QL66-1.txt).

# Timings of linux-3.2 compilations
# Processors: 2
# Affinity mask:  3
# Compiling with 1 simultaneous jobs
1     1113
# Compiling with 2 simultaneous jobs
2     844
# Compiling with 3 simultaneous jobs
3     875
# Compiling with 4 simultaneous jobs
4     863
# Compiling with 5 simultaneous jobs
5     840
# Compiling with 6 simultaneous jobs
6     844
# Compiling with 7 simultaneous jobs
7     844
# Compiling with 8 simultaneous jobs
8     851
Parallel Compilations on two CPU

Parallel Compilations on two CPU

Let’s try one last experiment on the same machine (two CPU), by disabling two elements:

  • prefetching of the next blocks of the disk (which can improve localized readings) with echo 0 > /sys/block/sda/read_ahead_kb
  • delayed (about 30 seconds) block writes (avoiding repetitive access to the disk in case of subsequent modification of the same block) with mount / -o sync,remount.

 

This time the results are very different (file: AMD-QL66-2.txt). The times are much longer than before because for each write to disk, the process waits for data to be transmitted to the device to continue his work.

 Timings of linux-3.2 compilations
# Processors: 2
# Affinity mask:  3
# Compiling with 1 simultaneous jobs
1     3487
# Compiling with 2 simultaneous jobs
2     2562
# Compiling with 3 simultaneous jobs
3     2198
# Compiling with 4 simultaneous jobs
4     1963
# Compiling with 5 simultaneous jobs
5     1779
# Compiling with 6 simultaneous jobs
6     1646
# Compiling with 7 simultaneous jobs
7     1636
# Compiling with 8 simultaneous jobs
8     1602
# Compiling with 9 simultaneous jobs
9     1738
# Compiling with 10 simultaneous jobs
10     1577
Parallel Compilations on 2 CPU without disk optimizations

Parallel Compilations on 2 CPU without disk optimizations

Here, the curve is closer to that than I imagined at first. Placing more jobs per CPU can take advantage of the wait times due to the disk access to progress in another compilation. Group the two curves in order to see the respective durations.

Parallel Compilations on QL66

Parallel Compilations on QL66

Conclusion

We see that with the quality of the I/O scheduler of Linux, and the optimized management of block devices, the best compilation time are obtained as soon as we launch one job per processor.

So I will modify my recommendation in the future as « If you have N processors available, compile your kernel with make -j N to get the best execution time. »

PS: If you have the opportunity to run this script on different architectures (8 processors, 16 processors, etc.). I am very interested in your results.