Tutoriel Linux : compiler le langage C

Le langage C est incontournable en environnement Linux, et pas seulement parce que le noyau Linux lui-même est programmé à 99% en C.

En effet, tous les binaires exécutables présents sur un système Linux, quel que soit le langage dans lequel ils sont développés, utilisent la librairie C, qui est le point de passage obligé pour s’interfacer avec les appels systèmes offerts par le noyau. Beaucoup de ces langages sont en réalité traduits en C avant d’être compilés (ce qui est assez cocasse), d’autres comme Python sont interprétés par un interpréteur écrit en C.

Comme nous l’avons déjà vu, les appels systèmes réellement offerts par le noyau (les vrais) sont différents des appels système offerts par la librairie C (souvent appelée libc pour faire court). Les (faux) appels système de la libc sont les seuls à être standardisés par la norme POSIX, qui définit les appels systèmes à implémenter dans un système Unix. La libc est parfaitement conforme (et offre même des appels système en plus) à la norme POSIX. Ne laissez donc jamais un incompétent vous affirmer que Linux n’est pas un Unix.

Le langage C est un langage merveilleux, très simple à apprendre (c’est ce qui a fait son succès), avec lequel on peut absolument TOUT programmer (même des applications web). C’est LE langage par excellence, celui qui, quand on le maîtrise, fait qu’on n’a plus besoin de s’intéresser aux autres langages de programmation.

Tout simple qu’il soit, il n’est pas question dans cet article d’apprendre la programmation C. Le lecteur se référera pour cela à un livre dédié comme celui-ci :

Ou mieux, le Kernighan et Ritchie original, écrit par les deux créateurs du langage C, que tout afficionado de Linux se doit d’avoir dans sa bibliothèque :

La compilation du langage C

Sous Linux le compilateur C gratuit fourni en standard par le GNU est gcc, pour GNU C compiler.

Nous allns voir ce qui se passe avec notre programme simple prog.c qui est un simple et classique « Hello world » dont voici le code source :

#include <stdio.h>

int main()
{
printf("Hello world\n");
}

Ce programme se compile avec la commande :

gcc ./prog.c -o prog

L’exécutable produit est prog. On pourrait très bien le nommer prog.exe comme sous les environnements Microsoft, mais c’est inutile sous Linux. Le système sait parfaitement tout seul si un fichier est exécutable ou non.

Comme on peut le voir sur la séquence ci-dessus, le programme compile et fonctionne. La commande file, qui donne la nature d’un fichier donné, révèle que prog est un exécutable ELF (Executable and Linkable Format) 64 bits, compilé pour micro-processeur x86-64.

Le format de binaire ELF est devenu au fil des années le format standard de tous les systèmes Unix.

Notre binaire prog étant compilé pour tourner sur micro-processeur x86-64, il ne pourra pas fonctionner sur une machine ayant un micro-processeur différent (comme un Raspberry Pi par exemple, qui est équipé d’un micro-processeur ARM). Pour le faire foncionner sur une telle machine, il faudra le re-compiler avec un compilateur C adapté (comme gcc sur Raspberry Pi par exemple, ou avec un cross-compilateur c’est à dire un compilateur tournant sur une architecture A mais capable de compiler pour une architecture B).

La commande file révèle également que notre binaire n’est pas strippé, ce qui signifie qu’il embarque en son sein toutes les informations de structure qui vont permettre de l’ouvrir de manière conviviale dans un debugger (comme gdb sous Linux) si besoin.

La commande strip retire ces informations, et l’on peut constater qu’une fois ces informations retirées, si la taille du fichier diminue de 15952 à 14464 octets, l’exécutable continue de fonctionner parfaitement.

Le fichier est maintenant vu par le système comme strippé :

La raison qui fait que notre binaire ne peut tourner sur une autre architecture que x86-64 est que le seul et unique langage reconnu par un micro-processeur est le langage machine, qui repose sur le jeu d’instructions connu par le micro-processeur et qui lui est propre. Le langage machine est une suite d’instructions en binaire ou en hexadécimal, parfaitement inadaptée à la manipulation humaine.

L’équivalent du langage machine lisible par un humain est l’assembleur. C’est la représentation textuelle des instructions que le micro-processeur doit exécuter. L’assembleur n’est pas du binaire mais du texte. C’est un langage de programmation de très bas niveau, encore plus bas que le C bien sûr, qui a été intensivement utilisé à l’époque des débuts de l’informatique mais plus maintenant.

Le noyau Linux utilise lui-même quelques instructions en assembleur, pour des cas très spécifiques d’instructions particulières à chaque micro-processeur. Le code assembleur dans le noyau Linux doit avoisiner les 0.01%.

Pour en revenir à notre programme en C, vous avez compris que le compilateur traduit en langage machine le code source en C dans le binaire généré. Voir ce langage machine n’a aucun intérêt puisque c’est du binaire, mais par contre on peut facilement voir le code assembleur généré, en utilisant la commande :

gcc -S ./prog.c

Cette commande crée le fichier prog.s qui est le code source en assembleur de la traduction du code source en C :

	.file	"prog.c"
	.text
	.section	.rodata
.LC0:
	.string	"Hello world"
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	endbr64
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	leaq	.LC0(%rip), %rax
	movq	%rax, %rdi
	call	puts@PLT
	movl	$0, %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	main, .-main
	.ident	"GCC: (Ubuntu 15.2.0-16ubuntu1) 15.2.0"
	.section	.note.GNU-stack,"",@progbits
	.section	.note.gnu.property,"a"
	.align 8
	.long	1f - 0f
	.long	4f - 1f
	.long	5
0:
	.string	"GNU"
1:
	.align 8
	.long	0xc0000002
	.long	3f - 2f
2:
	.long	0x3
3:
	.align 8
4:

C’est ce code source en assembleur qui va être traduit en langage machine. Comme on peut le voir, même s’il est censé être lisible par un humain, l’assembleur est imbitable.

Compilation dynamique ou statique

La compilation que nous avons réalisée de prog.c est dynamique par défaut, ce qui signifie que le binaire compilé n’embarque pas les libraires dont il a besoin, à commencer par la libc.

La commande ldd sur un binaire généré avec des liens dynamiques vers des librairies permet de voir quelles librairies sont appelées à l’exécution du binaire.

Ci-dessus on voit les 3 librairies nécessaires à l’exécution du binaire. La libc apparait en deuxième place.

Ces librairies sont le strict équivalent des dll sous Windows.

On peut cependant choisir de ne pas laisser le compilateur linker dynamiquement les librairies mais au contraire de générer un binaire linké statiquement. Ce binaire statique embarquera en son sein toutes les fonctions des librairies qu’il utilise. Pour réaliser cela, il suffit de rajouter l’indication -static sur la ligne de commande de compilation.

Regardons les deux méthodes en parallèle :

Compilation avec d’autres librairies en plus de la libc

Modifions légèrement notre programme en C pour qu’il affiche non plus « Hello world » mais une fonction mathématique (ici un cosinus) :

#include <stdio.h>
#include <math.h>

int main()
{
double x;

x=0;
printf("Cosinus de zero : %f\n",cos(x));
}

Comme on peut le voir ci-dessus, on est contraint d’ajouter -lm à la ligne de compilation pour que celle-ci fonctionne. C’est normal puisqu’on utilise une fonction mathématique dans le code (cosinus), il faut linker le binaire à la libm (librairie mathématique). -lm dit donc de linker à libm.

ldd montre bien que le binaire dynamique généré utilise la libm (en plus d’autres librairies, dont l’incontournable libc).

-l est donc le moyen de rajouter des librairies appelées par le binaire généré.

Créer sa propre librairie

libc, libm, vous vous demandez certainement comment ont été créées ces librairies. Le plus simple est de créer un exemple simple. Nous allons créer une librairie appelée libq.c qui contient une seule fonction affiche qui fait un simple printf de ce qu’on lui passe en paramètre.

Voici le code source de libq.c

#include <stdio.h>

void affiche(char *texte)
{
printf("%s\n",texte);
}

nous allons générer la librairie libq.so en tapant :

gcc -shared ./libq.c -o libq.so

Le résultat est bien une librairie (shared object, d’où le so)

Nous allons maintenant utiliser cette librairie libq dans notre programme prog.c, dont voici le code source modifié :

#include <stdio.h>
#include "libq.h"

int main()
{
affiche("Hello world");
}

Ce programme utilise la fonction affiche qui est programmée dans la librairie libq.c. Pour que le compilateur connaisse la syntaxe de cette fonction affiche, il est nécessaire de créer le fichier libq.h avec le contenu suivant, qui déclare la syntaxe d’appel de cette fonction :

void affiche(char *texte);

Voici donc d’où on part :

Compilons maintenant prog.c, de sorte qu’il utilise libq.so

Comme on peut le voir, gcc ./prog.c -o prog ne compile pas car le compilateur ne sait pas où se trouve le code en langage machine de la fonction affiche.

Le premier réflexe est de rajouter -lq comme nous l’avons vu précédemment, mais cela ne marche pas non plus car notre librairie est dans le répertoire courant et non dans /usr/lib, là où sont les autres librairies.

L’ajout de -L./, qui spécifie de chercher la librairie dans le répertoire courant, résout le problème et le binaire linké est bien généré.

Malheureusement la commande ldd montre qu’à l’exécution la librairie ne sera pas trouvé, parce qu’elle n’est pas dans le répertoire standard /usr/lib. D’ailleurs, à l’exécution on n’est pas déçu : error while loading shared libraries.

La création d’un lien /usr/libq.so (ou la copie de libq.so vers /usr/lib) vers notre fichier .so local résout le problème à l’exécution. ldd trouve maintenant la librairie dynamique libq.so et on a bien un « Hello world » à l’exécution du programme, ce qui prouve que notre librairie libq.so est parfaitement fonctionnelle.

Les librairies libc.so, libm.so et beaucoup d’autres n’ont pas été créées autrement par les créateurs du système.

Voilà qui termine notre tour d’horizon sur la mécanique de compilation du langage C et d’exécution des binaires générés sous Linux.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *