1 - Compilation et programmation

1.1 - Préparation du fichier source

1.1.1 - L'éditeur vi (mode console)

Bien que peu convivial, l’éditeur de texte standard d’Unix est bien adapté à la rédaction de sources en C. Il est souhaitable néanmoins de le configurer correctement au moyen de la variable d’environnement EXINIT et du fichier de configuration .exrc qui est propre au catalogue de travail que l’on a choisi.

Dans le fichier .profile, ajouter ces deux lignes:

EXINIT="set exrc"

export EXINIT

Dans le fichier .exrc, écrire la ligne:

set redraw autoindent number showmatch showmode

1.1.2 - L'éditeur nedit ou xedit (sous X)

1.2 - La compilation sous unix

Outils associés

Le langage C est un langage compilé. Il y a:

Compiler, c'est:

Depuis un source a.c:


Les options du compilateur sont:

Les avantages de la compilation dynamique sont:

- gain de place disque : les librairies ne sont pas dupliquées

- les erreurs se corrigent par remplacement simple des librairies sans recompilation

- on peut modifier des fonctions par remplacement de la librairie. Exemple: gethostbyname(3)

Les inconvénients sont:

- non-portabilité: un programme n'est plus autonome, et il dépend des librairies dynamiques (ldd) et de leurs emplacements.

 

Exemple:

 

# en un coup

gcc -o myprog myprog.c

# en 2 coups

gcc -c myprog.c

gcc -o myprog myprog.o

# avec la librairie math en dynamique

gcc -o myprog myprog.c -lm

# avec la librairie math en statique

gcc -o myprog myprog.c -static -lm

La compilation séparée

Un gros programme peut être découpé en fichiers indépendants.

Exemple: Soit un fichier principal, une première partie, et une seconde partie, la compilation sera:

 

gcc -c main.c

gcc -c part1.c

gcc -c part2.c

gcc -o main main.o part1.o part2.o

1.3 - Constitution d’une bibliothèque

1.3.1 - Statique

La commande ar permet de réaliser le compactage de plusieurs fichiers ordinaires en un seul fichier d’archive avec la possibilité de mise à jour, d’ajout...

La commande ar gère les droits d’accès au différents fichiers archivés.

On peut donc utiliser ar pour gérer une bibliothèque qui n’est autre qu’une collection de fichiers «point o» obtenus par compilation sans édition de liens. Pour être utilisable, cette bibliothèque doit comporter une table des matières et une table des symboles.

 

Affichage du contenu de la bibliothèque standard du langage C:

ar -tv /lib/libc.a

Création d’une bibliothèque référencée par libamoi.a dans le catalogue courant, à partir de tous les fichiers modules objets de ce catalogue:

ar -r libamoi.a *.o

Ajout du module sous.o à cette bibliothèque:

ar -r libamoi.a sous.o

Mise à jour de la table des symboles par exemple après un déplacement de la bibliothèque:

ar -s libamoi.a

1.3.2 - Dynamique

Création d’une librairie nommée libamoi.so dans le catalogue courant, à partir de tous les fichiers modules objets de ce catalogue :

cc –o libamoi.so –shared *.o

1.4 - Make

1.4.1 - Qu'est-ce que make ?

La commande make est utilisée pour la compilation de gros programmes (pas nécessairement en C) dont les sources sont réparties en plusieurs fichiers.

Elle détermine quelles sont les parties qui doivent être recompilées lorsque des modifications ont étés effectuées et appelle les commandes nécessaires à leur recompilation. La commande make interprète pour cela un fichier, le makefile, qui décrit les relations entre fichiers sources et contient les commandes nécessaires à la compilation. Une fois écrit le makefile, il suffit d'invoquer make pour exécuter toutes les recompilations nécessaires. Toutefois, on peut utiliser make avec des arguments pour contrôler les fichiers à compiler ou pour exécuter des tâches annexes.

1.4.2 - Le makefile

Le makefile est un ensemble de règles qui décrivent les relations entre les fichiers sources et les commandes nécessaires à la compilation. Il contient aussi des règles permettant d'exécuter certaines actions utiles comme par exemple nettoyer le répertoire, ou imprimer les sources.

1.4.3 - Les règle

Une règle rule a la forme suivante :

 

cible ...  : dependance ...

      action

      ...

      ...

 

Une cible target est en général le nom d'un fichier à générer. Ce peut être aussi le nom d'une action à exécuter. Il y a en général une seule cible par règle.

Une dépendance dependency est un fichier utilisé pour générer la cible. Plus généralement les commandes correspondant à une cible sont exécutées lorsque au moins une des dépendances de la cible a été modifiée depuis le dernier appel de make. Une cible a en général plusieurs dépendances.

Une action command est une ligne de commande Unix qui sera exécutée par make. Attention, dans la syntaxe du makefile, une action est toujours précédée d'une tabulation.

1.4.4 - Ecrire un makefile

Voilà, par exemple, un makefile :

 

mystrings:   main.o list.o

        gcc -Wall -ansi -g -o mystrings main.o list.o

main.o: main.c table.h

        gcc -Wall -ansi -g -c main.c

list.o: list.c table.h

        gcc -Wall -ansi -g -c list.c

clean:

        rm mystrings main.o list.o

 

Pour créer l’exécutable mystrings, on tapera make, pour supprimer les fichiers objets et exécutables du répertoire, on tapera make clean.

1.4.5 - Comment make interprète-t-il un makefile ?

Par défaut, make commence par la première cible, mystrings dans notre exemple.

Avant d’exécuter la commande associée, il doit mettre à jour les fichiers qui apparaissent dans ses dépendances (ici main.o et list.o) puisque ceux-ci apparaissent aussi comme des cibles.

 

Ainsi il interprète successivement

1.4.6 - Exécuter un makefile

La commande make interprète par défaut le fichier makefile ou Makefile si le précédent n'existe pas. On peut donner un nom différent au makefile et utiliser l'option -f.

Si aucun argument n'est spécifié, make interprète en premier lieu la première cible. N'importe qu'elle autre cible peut être donnée comme argument, auquel cas la règle correspondante est interprétée.

Avec certaines options, make peut être utilisé pour exécuter d'autres tâches que celles associées aux règles, par exemple avec l'option -n, il imprime les commandes qu'il devrait exécuter, mais ne les exécute pas, et avec l'option -q, il n’exécute rien et n'imprime rien, mais retourne 0 ou 1 selon que les cibles sont à jour ou non.

1.4.7 - Les variables

1.4.7.1 - Des variables dans le makefile

Dans notre exemple, nous avons écrit trois fois la même commande de compilation, et trois fois les noms des fichiers objets. De telles répétitions sont fastidieuses (dans les gros makefile) et sources d'erreurs.

On peut éviter cela en utilisant des variables macros qui permettent à une chaîne de caractères d'être définie une seule fois puis utilisée en plusieurs endroits.

Ainsi le makefile est équivalent à celui de notre exemple :

 

objects = main.o list.o

cccom = gcc -Wall -ansi -g

mystrings: $(objects)

        $(cccom) -o mystrings $(objects)

main.o: main.c table.h

        $(cccom) -c main.c

list.o: list.c table.h

        $(cccom) -c list.c

clean:

        rm mystrings $(objects)

 

Le nom d'une variable ne doit pas contenir `:', `#', `=', ni d'espaces. Tout autre chaîne de caractère est valide et make fait la différence entre les majuscules et les minuscules.

On récupère la valeur de la variable toto par $(toto) ou par ${toto}.

La substitution d'une variable par sa valeur est purement syntaxique. Elle est effectuée lorsque make lit la variable, sauf si une variable apparaît dans la définition d'une autre variable. Ceci permet d'avoir des définitions récurrentes.

1.4.7.2 - Définitions récurrentes

En utilisant des définitions récurrentes de variables, on peut réécrire notre makefile comme ci-dessous :

objects = main.o list.o

cccom = gcc $(ccopts)

ccopts = -Wall -ansi -g

 

mystrings: $(objects)

        $(cccom) -o mystrings $(objects)

main.o: main.c table.h

        $(cccom) -c main.c

list.o: list.c table.h

        $(cccom) -c list.c

clean:

        rm mystrings $(objects)

1.4.7.3 - Substitutions

On peut aussi effectuer des substitutions à l'intérieur des variables :

objects = main.o list.o

sources = $(objets:.o=.c)

cccom = gcc $(ccopts)

ccopts = -Wall -ansi -g

 

mystrings: $(objects)

        $(cccom) -o mystrings $(objects)

main.o: main.c table.h

        $(cccom) -c main.c

list.o: list.c table.h

        $(cccom) -c list.c

clean:

        rm mystrings $(objects)

print:

        lpr $(sources)

 

Dans cet exemple, la variable source est définie à partir de la variable objects en substituant le suffixe .c au suffixe .o.  L'appel de make print a pour effet d'imprimer les sources.

La commande : @ echo texte a pour effet d’afficher le texte a l’écran pendant la lecture du makefile par make.

1.4.7.4 - Noms de variables calculés

On peut utiliser des variables pour définir des noms de nouvelles variables.

representation = list

objects = main.o list.o tree.o

$(representation)_obj = main.o $(representation).o

sources = $(objets:.o=.c)

cccom = gcc $(ccopts)

ccopts = -Wall -ansi -g

 

mystrings: $(representation)_obj

        $(cccom) -o mystrings $(representation)_obj

main.o: main.c table.h

        $(cccom) -c main.c

list.o: list.c table.h

        $(cccom) -c list.c

tree.o: tree.c table.h

        $(cccom) -c tree.c

clean:

        rm mystrings $(objects)

print:

        lpr $(sources)

Dans ce cas, une variable list_obj est défini et prends la valeur main.o list.o. Ce sont ces objets qui seront liés pour créer l’exécutable mystrings. Cette fonctionnalité est intéressante dans la mesure ou certaine variable peuvent être définies à l'extérieur du makefile.

1.4.7.5 - Les valeurs des variables

On peut définir une variable de différentes manières :

 

 

     make representation=tree

 

     assignera tree à représentation à la place de list et tree.o sera utilisé pour créer l’exécutable.

De plus, make connaît de nombreuses variables qu'il n'est pas nécessaire de définir :

 

$@ : le nom du fichier correspondant à la cible courante

$< : la première dépendance de la cible courante,

$^ : toutes les dépendances de la cible courante,

$? : les dépendances de la cible courante qui sont plus récentes que la cible.

 

Ainsi, si on utilise cc au lieu de gcc, on peut utiliser ce makefile :

 

representation = list

objects = main.o list.o tree.o

$(representation)_obj = main.o $(representation).o

sources = $(objets:.o=.c)

cccom = ${CC} $(ccopts)

ccopts = -Wall -ansi -g

 

mystrings: $(representation)_obj

        $(cccom) -o mystrings $^

main.o: main.c table.h

        $(cccom) -c $<

list.o: list.c table.h

        $(cccom) -c $<

tree.o: tree.c table.h

        $(cccom) -c $<

clean:

        rm mystrings $(objects)

print: .print

.print: $(sources)

        lpr $?

1.4.8 - Les règles

1.4.8.1 - Des règles implicites

Certaines manières de construire des cibles à partir de leurs dépendances sont standards et se retrouvent dans la plupart des makefiles.

make possède un certain nombre de règles prédéfinis qui sont appliquées pour construire une cible, si celle-ci n'a pas de règle associée, ou pour construire un fichier qui n’apparaît que comme une dépendance. Dans ce cas, make choisit la règle à appliquer en fonction du nom et du suffixe de la cible.

Ainsi une cible toto.o sera construite à partir de toto.c en utilisant implicitement la règle :

${CC} -c ${CFLAGS} toto.c

 

si la ligne

 

toto.o : toto.c

 

apparaît seule dans le makefile ou si toto.o apparaît dans les dépendances d'une autre cible et n'est pas une cible.

On peut empêcher make d'utiliser des règles implicites prédéfinis en utilisant l'option -r ou en redéfinissant la cible prédéfini .SUFFIXES dont les dépendances sont les suffixes connus par make.

 

Extraites du catalogue de ces règles, voici les plus courantes :

 

 

Ainsi, en utilisant des règles implicites, notre makefile pourrait s'écrire :

 

representation = list

objects = main.o list.o tree.o

$(representation)_obj = main.o $(representation).o

sources = $(objets:.o=.c)

CFLAGS = -Wall -ansi -g

 

mystrings: $(representation)_obj

main.o: main.c table.h

list.o: list.c table.h

tree.o: tree.c table.h

clean:

        rm mystrings $(objects)

print: .print

.print: $(sources)

        lpr $?

 

et même, si les fichiers .c ne dépendaient pas de table.h, et si l'éxécutable s'appelait main on pourrait simplement écrire :

 

representation = list

objects = main.o list.o tree.o

sources = $(objets:.o=.c)

CFLAGS = -Wall -ansi -g

 

main: $(representation).o

clean:

        rm mystrings $(objects)

print: .print

.print: $(sources)

        lpr $?

1.4.8.2 - Définir des règles implicites

Pour éviter l’ambiguïté et le peu de lisibilité du makefile précédent, et pour assurer la portabilité (les règles implicites prédéfinis peuvent varier d'une version à l'autre de make), on peut redéfinir les règles implicites et on peut aussi en définir d'autres.

Une règle implicite contient le caractère % dans la cible comme motif -pattern- pour remplacer n'importe quel nom : ainsi la règle

 

%.o : %.c

        gcc ${CFLAGS} -c %.c

 

remplacera la règle implicite générant les objets à partir des sources. Une bonne version de notre makefile sera :

 

 

representation = list

objects = main.o list.o tree.o

$(representation)_obj = main.o $(representation).o

sources = $(objets:.o=.c)

CFLAGS = -Wall -ansi -g

 

mystrings: $(representation)_obj

        gcc -g -o mystrings $^

%.o : %.c table.h

        gcc ${CFLAGS} -c $<

main.o: main.c table.h

list.o: list.c table.h

tree.o: tree.c table.h

clean:

        rm mystrings $(objects)

print: .print

.print: $(sources)

        lpr $?

1.5 - L'interface C-Unix

1.5.1 - Les primitives

L'exécution d'une primitive n'est rien d'autre que l'exécution d'une partie de code du noyau. Les primitives permettent de réaliser des opérations "dangereuses" portant en particulier sur:

Un appel système qui échoue renvoie en général la valeur -1 et la variable externe errno est alimentée pour fournir un numéro qui renseigne sur la cause de l'échec.

On testera toujours la réussite d'un appel système !

 

#include <stdio.h>

extern int errno

int main()

{

if (Appel_Système == -1)

fprintf(stderr, "Echec: erreur numéro %d\n",errno);

}

 

Des mnémoniques d'erreur sont définis dans le fichier à inclure <sys/errno.h> et peuvent être utilisés pour affiner la gestion des erreurs

if (Appel_Système == -1) && (errno==enoent) /* fichier inexistant*/

Enfin, il existe une fonction nommée perror qui affiche sur la sortie d'erreur standard un message personnalité suivi d'un message en clair provenant du système.

#include<stdio.h>

void perror(const char *MesgPerso)

1.5.2 - Les fonctions

Différentes bibliothèques de fonction sont disponibles qui permettent de simplifier le travail du développeur tout en minimisant le nombre d'appel système. L'utilisation d'une fonction nécessite une édition de lien avec la librairie qui contient le module objet où la fonction est définie.

1.6 - L'interface shell

La situation classique est celle d'un utilisateur qui lance une commande depuis son shell de connexion. Le programme de l'utilisateur doit pouvoir récupérer des arguments sur la ligne de commande, accéder aux variables d'environnement et renvoyer au shell un code de retour qui renseignera sur le succès de la commande.

1.6.1 - Accès aux arguments

Le passage des arguments entre le shell et la commande est réalisé au moyen de l'entête :

int main(int argn, char *argv[])

1.6.2 - Accès à l'environnement

Le passage des arguments et de l'environnement est réalisé en une seule opération au moyen de l'entête :

int main(int argn, char *argv[], char *arge[])

1.6.3 - Renvoi d'un code de retour

La fonction exit cause l'arrêt du processus en cours, avec fermeture de tous les fichiers ouverts et renvoie au processus père du code de retour passé en argument. Si le processus père est le shell, on rappelle que le code de retour est contenu dans la variable d'environnement $?.

#include<stdlib.h>

extern void exit(int CodeRetour)

 

On s'attachera à respecter le principe Unix suivant:

1.7 - Aide à la mise au point

La fonction system permet de lancer une commande Unix depuis un programme rédigé en C. Cette façon de faire est très lente et ne doit être réservée qu'à la mise au point.

#include<stdlib.h>

extern int system(const char *commande)

 

Ex: system("who");