Page suivante - Page précédente - Table des matières

4. Portage et compilation

4.1 Symboles définis automatiquement

Vous pouvez trouver quels symboles votre version de gcc définit automatiquement en le lançant avec l'option -v. Par exemple cela donne ça chez moi :

$ echo 'main(){printf("Bonjour !\n");}' | gcc -E -v -
Reading specs from /usr/lib/gcc-lib/i486-box-linux/2.7.2/specs
gcc version 2.7.2
 /usr/lib/gcc-lib/i486-box-linux/2.7.2/cpp -lang-c -v -undef
-D__GNUC__=2 -D__GNUC_MINOR__=7 -D__ELF__ -Dunix -Di386 -Dlinux
-D__ELF__ -D__unix__ -D__i386__ -D__linux__ -D__unix -D__i386
-D__linux -Asystem(unix) -Asystem(posix) -Acpu(i386)
-Amachine(i386) -D__i486__ -
Si vous écrivez du code qui utilise des spécificités Linux, il est souhaitable d'implémenter le code non portable de la manière suivante
#ifdef __linux__
/* ... code linux ... */
#endif /* linux */

Utilisez __linux__ pour cela, et pas linux. Bien que cette macro soit définie, ce n'est pas une spécification POSIX.

4.2 Options de compilation

La documentation des options de compilation se trouve dans les pages info de gcc (sous Emacs, utilisez C-h i puis sélectionnez l'option `gcc'). Votre distribution peut ne pas avoir installé la documentation ou bien vous pouvez en avoir une ancienne. Dans ce cas, la meilleure chose à faire est de récupérer les sources de gcc depuis ftp://prep.ai.mit.edu/pub/gnu ou l'un des ses nombreux miroirs dont ftp://ftp.ibp.fr/pub/gnu.

La page de manuel gcc (gcc.1) est en principe, complètement dépassée. Cela vous met en garde si vous désirez la consulter.

Options de compilation

gcc peut réaliser un certain nombre d'optimisations sur le code généré en ajoutant l'option -On à la ligne de commandes, où n est un chiffre. La valeur de n, et son effet exact, dépend de la version de gcc, mais s'échelonne normalement entre 0 (aucune optimisation) et 2 (un certain nombre) ou 3 (toutes les optimisations possibles).

En interne, gcc interprète les options telles que -f et -m. Vous pouvez voir exactement ce qu'effectue le niveau spécifié dans l'option -O en lançant gcc avec l'option -v et l'option (non documentée) -Q. Par exemple, l'option -O2, effectue les opérations suivantes sur ma machine :

enabled: -fdefer-pop -fcse-follow-jumps -fcse-skip-blocks
-fexpensive-optimizations
 -fthread-jumps -fpeephole -fforce-mem -ffunction-cse -finline
 -fcaller-saves -fpcc-struct-return -frerun-cse-after-loop
 -fcommon -fgnu-linker -m80387 -mhard-float -mno-soft-float
 -mno-386 -m486 -mieee-fp -mfp-ret-in-387

Utiliser un niveau d'optimisation supérieur à celui que le compilateur supporte (par exemple -O6) aura le même effet qu'utiliser le plus haut niveau géré. Distribuer du code où la compilation est configurée de cette manière est une très mauvaise idée -- si d'autres optimisations sont incorporées dans de versions futures, vous (ou d'autres utilisateurs) pouvez vous apercevoir que cela ne compile plus, ou bien que le code généré ne fait pas les actions désirées.

Les utilisateurs de gcc 2.7.0 à 2.7.2 devraient noter qu'il y a un bogue dans l'option -O2. Plus précisément, la strength reduction ne fonctionne pas. Un patch a été implémenté pour résoudre ce problème, mais vous devez alors recompiler gcc. Sinon, vous devrez toujours compiler avec l'option -fno-strength-reduce.

Spécification du processeur

Il existe d'autres options -m qui ne sont pas positionnées lors de l'utilisation de -O mais qui sont néanmoins utiles dans certains cas. C'est le cas pour les options -m386 et -m486, qui indiquent à gcc de générer un code plus ou moins optimisé pour l'un ou l'autre type de processeur. Le code continuera à fonctionner sur les deux processeurs. Bien que le code pour 486 soit plus important, il ne ralentit pas l'exécution du programme sur 386.

Il n'existe pas actuellement de -mpentium ou -m586. Linus a suggéré l'utilisation des options -m486 -malign-loops=2 -malign-jumps=2 -malign-functions=2, pour exploiter les optimisations du 486 tout en perdant de la place due aux problèmes d'alignements (dont le Pentium n'a que faire). Michael Meissner (de Cygnus) nous dit :

« Mon avis est que l'option -mno-strength-reduce permet d'obtenir un code plus rapide sur un x86 (nota : je ne parle pas du bogue strength reduction, qui est un autre problème). Cela s'explique en raison du peu de registres dont disposent ces processeurs (et la méthode de GCC qui consiste à grouper les registres dans l'ordre inverse au lieu d'utiliser d'autres registres n'arrange rien). La strength reduction consiste en fait à rajouter des registres pour remplacer les multiplications par des additions. Je suspecte également -fcaller-saves de ne pas arranger la situation. »

Une autre idée est que -fomit-frame-pointer n'est pas obligatoirement une bonne idée. D'un côté, cela peut signifier qu'un autre registre est disponible pour une allocation. D'un autre côté, vue la manière dont les processeurs x86 codent leur jeu d'instruction, cela peut signifier que la pile des adresses relatives prend plus de place que les adresses de fenêtres relatives, ce qui signifie en clair que moins de cache est disponible pour l'exécution du processus. Il faut préciser que l'option -fomit-frame-pointer, signifie que le compilateur doit constamment ajuster le pointeur de pile après les appels, alors qu'avec une fenêtre, il peut laisser plusieurs appels dans la pile.

Le mot final sur le sujet provient de Linus :

Remarquez que si vous voulez des performances maximales, ne me croyez pas : testez ! Il existe tellement d'options de gcc, et il est possible que cela ne soit une réelle optimisation que pour vous.

Internal compiler error: cc1 got fatal signal 11

Signal 11 correspond au signal SIGSEGV, ou bien segmentation violation. Normalement, cela signifie que le programme s'est mélangé les pointeurs et a essayé d'écrire là où il n'en a pas le droit. Donc, cela pourrait être un bug de gcc.

Toutefois, gcc est un logiciel assez testé et assez remarquable de ce côté. Il utilise un grand nombre de structures de données complexes, et un nombre impressionnant de pointeurs. En résumé, c'est le plus pointilleux des testeurs de mémoire existants. Si vous n'arrivez pas à reproduire le bogue --- si cela ne s'arrête pas au même endroit lorsque vous retentez la compilation --- c'est plutôt un problème avec votre machine (processeur, mémoire, carte mère ou bien cache). N'annoncez pas la découverte d'un nouveau bogue si votre ordinateur traverse tous les tests du BIOS, ou s'il fonctionne correctement sous Windows ou autre : ces tests ne valent rien. Il en va de même si le noyau s'arrête lors du `make zImage' ! `make zImage' doit compiler plus de 200 fichiers, et il en faut bien moins pour arriver à faire échouer une compilation.

Si vous arrivez à reproduire le bogue et (mieux encore) à écrire un petit programme qui permet de mettre en évidence cette erreur, alors vous pouvez envoyer le code soit à la FSF, soit dans la liste linux-gcc. Consultez la documentation de gcc pour plus de détails concernant les informations nécessaires.

4.3 Portabilité

Cette phrase a été dite un jour : si quelque chose n'a pas été porté vers Linux alors ce n'est pas important de l'avoir :-).

Plus sérieusement, en général seules quelques modifications mineures sont nécessaires car Linux répond à 100% aux spécifications POSIX. Il est généralement sympathique d'envoyer à l'auteur du programme les modifications effectuées pour que le programme fonctionne sur Linux, pour que lors d'une future version, un `make' suffise pour générer l'exécutable.

Spécificités BSD (notamment bsd_ioctl, daemon et<sgtty.h>)

Vous pouvez compiler votre programme avec l'option -I/usr/include/bsd et faire l'édition de liens avec -lbsd (en ajoutant -I/usr/include/bsd à la ligne CFLAGS et -lbsd à la ligne LDFLAGS dans votre fichier Makefile). Il est également nécessaire de ne pas ajouter -D__USE_BSD_SIGNAL si vous voulez que les signaux BSD fonctionnent car vous les avez inclus automatiquement avec la ligne -I/usr/include/bsd et en incluant le fichier d'en-tête <signal.h>.

Signaux manquants (SIGBUS, SIGEMT, SIGIOT, SIGTRAP, SIGSYS, etc.)

Linux respecte les spécifications POSIX. Ces signaux n'en font pas partie (cf. ISO/IEC 9945-1:1990 - IEEE Std 1003.1-1990, paragraphe B.3.3.1.1) :

« Les signaux SIGBUS, SIGEMT, SIGIOT, SIGTRAP, et SIGSYS ont été omis de la norme POSIX.1 car leur comportement est dépendant de l'implémentation et donc ne peut être répertorié d'une manière satisfaisante. Certaines implémentations peuvent fournir ces signaux mais doivent documenter leur effet »

La manière la plus élégante de régler ce problème est de redéfinir ces signaux à SIGUNUSED. La manière normale de procéder est d'entourer le code avec les #ifdef appropriés :

#ifdef SIGSYS
/* ... code utilisant les signaux non posix  .... */
#endif

Code K & R

GCC est un compilateur ANSI, or il existe beaucoup de code qui ne soit pas ANSI.

Il n'y a pas grand chose à faire, sauf rajouter l'option -traditional lors de la compilation. Il effectue certaines vérifications supplémentaires. Consultez les pages info gcc.

Notez que l'option -traditional a pour unique effet de changer la forme du langage accepté par gcc. Par exemple, elle active l'option -fwritable-strings, qui déplace toutes les chaînes de caractères vers l'espace de données (depuis l'espace de texte, où elle ne peuvent pas être modifiées). Ceci augmente la taille de la mémoire occupée par le programme.

Les symboles du préprocesseur produisent un conflit avecles prototypes du code

Un des problèmes fréquents se produit lorsque certaines fonctions standards sont définies comme macros dans les fichiers d'en-tête de Linux et le préprocesseur refusera de traiter des prototypes identiques. Par exemple, cela peut arriver avec atoi() et atol().

sprintf()

Parfois, soyez prudent lorsque vous effectuez un portage à partir des sources de programmes fonctionnant sous SunOs, surtout avec la fonction sprintf(string, fmt, ...) car elle renvoie un pointeur sur la chaîne de caractères alors que Linux (suivant la norme ANSI) retourne le nombre de caractères recopiés dans la chaîne de caractères.

fcntl et ses copains. Où se trouve la définition de FD_* et compagnie ?

Dans <sys/time.h>. Si vous utilisez fcntl vous voudrez probablement inclure <unistd.h> également, pour avoir le prototype de la fonction.

D'une manière générale, la page de manuel pour une fonction donne la liste des fichiers d'en-tête à inclure.

Le timeout de select(). Les programmescommencent dans un état d'attente active

A une certaine époque, le paramètre timeout de la fonction select() était utilisé en lecture seule. C'est pourquoi la page de manuel comporte une mise en garde :

select() devrait retourner normalement le temps écoulé depuis le timeout initial, s'il s'est déclenché, en modifiant la valeur pointée par le paramètre time. Cela sera peut-être implémenté dans les versions ultérieures du système. Donc, il n'est pas vraiment prudent de supposer que les données pointées ne seront pas modifiées lors de l'appel à select().

Mais tout arrive avec le temps ! Lors d'un retour de select(), l'argument timeout recevra le temps écoulé depuis la dernière réception de données. Si aucune donnée n'est arrivée, la valeur sera nulle, et les futurs appels à cette fonction utilisant le même timeout auront pour résultat un retour immédiat.

Pour résoudre le problème, il suffit de mettre la valeur timeout dans la structure à chaque appel de select(). Le code initial était

 struct timeval timeout;
 timeout.tv_sec = 1;
 timeout.tv_usec = 0;
 while (some_condition)
 select(n,readfds,writefds,exceptfds,&timeout);
et doit devenir :
 struct timeval timeout;
 while (some_condition)
 {
 timeout.tv_sec = 1;
 timeout.tv_usec = 0;
 select(n,readfds,writefds,exceptfds,&timeout);
 }

Certaines versions de Mosaic étaient connues à une certaine époque pour avoir ce problème.

La vitesse de rotation du globe terrestre était inversement proportionnelle à la vitesse de transfert des données !

Appels systèmes interrompus

Symptomes :

Lorsqu'un processus est arrêté avec un Ctrl-Z et relancé - ou bien lorsqu'un autre signal est déclenché dans une situation différente : par exemple avec un Ctrl-C, la terminaison d'un processus, etc, on dit qu'il y a « interruption d'un appel système » , ou bien « write : erreur inconnue » ou des trucs de ce genre.

Problèmes :

Les systèmes POSIX vérifient les signaux plus souvent que d'autres Unix plus anciens. Linux peux lancer les gestionnaires de signaux :

  • d'une manière asynchrone (sur un top d'horloge)
  • lors d'un retour de n'importe quel appel système
  • pendant l'exécution des appels systèmes suivants : select(), pause(), connect(), accept(), read() sur des terminaux, des sockets, des pipes ou des fichiers situés dans /proc, write() sur des terminaux, des sockets, des pipes ou des imprimantes, open() sur des FIFOs, des lignes PTYs ou séries, ioctl() sur des terminaux, fcntl() avec la commande F_SETLKW, wait4(), syslog(), et toute opération d'ordre TCP ou NFS.

Sur d'autres systèmes d'exploitation, il est possible que vous ayez à inclure dans cette catégorie les appels systèmes suivants : creat(), close(), getmsg(), putmsg(), msgrcv(), msgsnd(), recv(), send(), wait(), waitpid(), wait3(), tcdrain(), sigpause(), semop().

Si un signal (que le programme désire traiter) est lancé pendant l'exécution d'un appel système, le gestionnaire est lancé. Lorsque le gestionnaire du signal se termine, l'appel système détecte qu'il a été interrompu et se termine avec la valeur -1 et errno = EINTR. Le programme n'est pas forcément au courant de ce qui s'est passé et donc s'arrête.

Vous pouvez choisir deux solutions pour résoudre ce problème.

(1)Dans tout gestionnaire de signaux que vous mettez en place, ajoutez l'option SA_RESTART au niveau de sigaction. Par exemple, modifiez

 signal (signal_id, mon_gestionnaire_de_signaux);
en
 signal (signal_id, mon_gestionnaire_de_signaux);
 {
 struct sigaction sa;
 sigaction (signal_id, (struct sigaction *)0, &sa);
#ifdef SA_RESTART
 sa.sa_flags |= SA_RESTART;
#endif
#ifdef SA_INTERRUPT
 sa.sa_flags &= ~ SA_INTERRUPT;
#endif
 sigaction (signal_id, &sa, (struct sigaction *)0);
 }

Notez que lors de certains appels systèmes vous devrez souvent regarder si errno n'a pas été positionnée à EINTR par vous même comme avec read(), write(), ioctl(), select(), pause() et connect().

(2) A la recherche de EINTR :

Voici deux exemples avec read() et ioctl(),

Voici le code original utilisant read()

int result;
while (len> 0)
{
 result = read(fd,buffer,len);
 if (result < 0)
 break;
 buffer += result;
 len -= result;
}
et le nouveau code

int result;
while (len> 0)
{
 result = read(fd,buffer,len);
 if (result < 0)
 {
 if (errno != EINTR)
 break;
 }
 else
 {
 buffer += result;
 len -= result;
 }
}
Voici un code utilisant ioctl()

int result;
result = ioctl(fd,cmd,addr);
et cela devient
int result;
do
{
 result = ioctl(fd,cmd,addr);
}
while ((result == -1) && (errno == EINTR));

Il faut remarquer que dans certaines versions d'Unix de type BSD on a l'habitude de relancer l'appel système. Pour récupérer les interruptions d'appels systèmes, vous devez utiliser les options SV_INTERRUPT ou SA_INTERRUPT.

Les chaînes et leurs accès en écritures (ou les programmes qui provoquent des« segmentation fault » d'une manière aléatoire)

GCC a une vue optimiste en ce qui concerne ses utilisateurs, en croyant qu'ils respectent le fait qu'une chaîne dite constante l'est réellement. Donc, il les range dans la zone texte(code) du programme, où elles peuvent être chargées puis déchargées à partir de l'image binaire de l'exécutable située sur disque (ce qui évite d'occuper de l'espace disque). Donc, toute tentative d'écriture dans cette chaîne provoque un « segmentation fault ».

Cela peut poser certains problèmes avec d'anciens codes, par exemple ceux qui utilisent la fonction mktemp() avec une chaîne constante comme argument. mktemp() essaye d'écrire dans la chaîne passée en argument.

Pour résoudre ce problème,

  1. compilez avec l'option -fwritable-strings pour indiquer à gcc de mettre les chaînes constantes dans l'espace de données
  2. réécrire les différentes parties du code pour allouer une chaîne non constante puis effectuer un strcpy des données dedans avant d'effectuer l'appel.

Pourquoi l'appel à execl() échoue ?

Tout simplement parce que vous l'utilisez mal. Le premier argument d'execl est le programme que vous désirez exécuter. Le second et ainsi de suite sont en fait le éléments du tableau argv que vous appelez. Souvenez-vous que argv[0] est traditionnellement fixé même si un programme est lancé sans argument. Vous devriez donc écrire :

execl("/bin/ls","ls",NULL);
et pas
execl("/bin/ls", NULL);

Lancer le programme sans argument est considéré comme étant une demande d'affichage des bibliothèques dynamiques associées au programme, si vous utilisez le format a.out. ELF fonctionne d'une manière différente.

(Si vous désirez ces informations, il existe des outils plus simples; consultez la section sur le chargement dynamique, ou la page de manuel de ldd).


Page suivante - Page précédente - Table des matières