undefined reference to en C : l'erreur de l'édition de liens
Ton code compile mais ld échoue : main mal écrit, fichier .c oublié, -lm manquant, ordre des -l, multiple definition. Chaque cas reproduit avec GCC et nm.
Aucun numéro de ligne, un fichier /tmp/cc08S7Fl.o que tu n'as jamais créé, et une dernière ligne qui parle de collect2 et de ld. Ce message ne ressemble pas aux erreurs de compilation habituelles parce qu'il ne vient pas du compilateur. Il vient de l'étape d'après, et une fois qu'on sait laquelle, la cause se trouve vite.
En bref
undefined reference to `x' est une erreur de l'éditeur de liens (ld) : chaque fichier a compilé, mais au moment d'assembler l'exécutable, aucun d'eux ne fournit le symbole x. Les causes habituelles sont un main absent ou mal orthographié, un fichier .c oublié dans la commande, une fonction déclarée mais jamais définie (ou définie sous un autre nom), et une bibliothèque non liée, comme -lm pour sqrt, ou placée avant les fichiers qui l'utilisent. collect2: error: ld returned 1 exit status n'est que la ligne de conclusion : la cause est dans les lignes au-dessus.
Compilation ou édition de liens : à quelle étape ça casse ?
gcc main.c -o main enchaîne quatre étapes, et seule la dernière a besoin de voir tout le programme d'un coup. Le préprocesseur colle le contenu des #include, le compilateur traduit ton C en assembleur, l'assembleur en fait un fichier objet .o, puis l'éditeur de liens réunit les .o et les bibliothèques en un exécutable.
- main.cton code source
- main.i#include et macros remplacés
- main.sassembleur
- main.ocode machine, appels encore en attente
- mainchaque appel doit trouver sa fonction
Un fichier objet contient le code de tes fonctions, plus la liste des noms qu'il utilise sans les définir. Pour main.o, ce sont les fonctions appelées depuis main. Tant qu'on reste fichier par fichier, ces trous sont permis : un prototype suffit pour compiler un appel à une fonction définie ailleurs. C'est à l'édition de liens que chaque trou doit être bouché par un autre fichier ou une bibliothèque.
On le voit en séparant les étapes, sur un programme en deux fichiers : main.c appelle carre, déclarée dans calcul.h et définie dans calcul.c. gcc -c s'arrête au fichier objet, sans éditer les liens :
$ gcc -Wall -Wextra -c main.c$ lscalcul.c calcul.h main.c main.o$ gcc main.o -o main/usr/bin/ld: main.o: in function `main':main.c:(.text+0xc): undefined reference to `carre'collect2: error: ld returned 1 exit status$ gcc -Wall -Wextra -c calcul.c$ gcc main.o calcul.o -o main && ./main49La compilation de main.c n'a rien signalé : l'appel était correct. L'erreur arrive quand on demande l'exécutable sans calcul.o. Quand tu compiles en une seule commande, GCC fait la même chose avec des fichiers objets temporaires, d'où les noms comme /tmp/ccBIVodg.o. Et collect2 est le programme que GCC lance à la place de ld pour cette dernière étape : il appelle le vrai ld et répète son échec.
undefined reference to `main'
Le programme n'a pas de fonction main, ou elle porte un autre nom. Une lettre suffit :
#include <stdio.h>
int mian(void){ printf("Bonjour\n"); return 0;}$ gcc -Wall -Wextra mian.c -o mian/usr/bin/ld: /lib/aarch64-linux-gnu/crt1.o: in function `__wrap_main':(.text+0x38): undefined reference to `main'collect2: error: ld returned 1 exit statusLe fichier fautif cité est crt1.o, qui ne vient pas de toi : c'est le code de démarrage fourni avec la bibliothèque C, qui prépare le processus puis appelle main. Son nom de fonction et son chemin varient d'un système à l'autre (la sortie ci-dessus vient d'une machine ARM 64 bits), le message undefined reference to `main' reste le même.
L'autre cas courant : tu compiles seul un fichier qui ne contient que des fonctions, sans main, pour vérifier qu'il compile. Ajoute -c et GCC s'arrêtera au .o, sans réclamer de main.
Fichier oublié ou fonction jamais définie
Reprenons l'exemple du début : main.c appelle carre, déclarée dans calcul.h et définie dans calcul.c, mais cette fois tout se compile en une commande.
#include <stdio.h>#include "calcul.h"
int main(void){ printf("%d\n", carre(7)); return 0;}$ gcc -Wall -Wextra main.c -o main/usr/bin/ld: /tmp/cc08S7Fl.o: in function `main':main.c:(.text+0xc): undefined reference to `carre'collect2: error: ld returned 1 exit status$ gcc -Wall -Wextra main.c calcul.c -o main && ./main49Un #include "calcul.h" n'ajoute que des déclarations. Il promet au compilateur que carre existe quelque part, il ne dit pas où. Chaque fichier .c du programme doit apparaître dans la commande, ou dans le Makefile qui l'écrit pour toi (voir écrire son Makefile en C).
Même message si calcul.c est bien là mais que la définition porte un autre nom : care au lieu de carre. Le compilateur ne peut rien voir, chaque fichier pris seul est correct. C'est là qu'un outil qui lit les fichiers objets devient utile.
Lire les symboles avec nm
nm(1) liste les symboles d'un fichier objet. T désigne une fonction définie dans ce fichier, U un symbole utilisé mais défini ailleurs. L'édition de liens réussit quand chaque U trouve un T dans un autre fichier ou une bibliothèque.
$ gcc -c main.c calcul.c$ nm main.o calcul.omain.o: U carre0000000000000000 T main U printf
calcul.o:0000000000000000 T careLe diagnostic est immédiat : main.o attend carre, calcul.o fournit care. printf est aussi en U, mais la bibliothèque C, liée d'office, la fournit.
Pour faire attraper ce genre de coquille par le compilateur, ajoute -Wmissing-prototypes : il signale toute fonction globale définie sans prototype visible, ce qui arrive justement quand le nom de la définition ne correspond pas à celui du .h.
$ gcc -Wall -Wextra -Wmissing-prototypes -c calcul.ccalcul.c:3:5: warning: no previous prototype for 'care' [-Wmissing-prototypes] 3 | int care(int n) | ^~~~undefined reference to `sqrt' : la bibliothèque mathématique
Avec la glibc, les fonctions de <math.h> vivent dans une bibliothèque à part, libm, que GCC ne lie pas par défaut. Le programme suivant lit un nombre et affiche sa racine :
#include <math.h>#include <stdio.h>
int main(void){ double x;
if (scanf("%lf", &x) != 1) return 1; printf("%.3f\n", sqrt(x)); return 0;}$ gcc -Wall -Wextra racine.c -o racine/usr/bin/ld: /tmp/ccu0veMv.o: in function `main':racine.c:(.text+0x34): undefined reference to `sqrt'collect2: error: ld returned 1 exit status$ gcc -Wall -Wextra racine.c -o racine -lm && echo 2 | ./racine1.414-lm demande à l'éditeur de liens de chercher dans libm. La section LIBRARY de chaque page de manuel indique laquelle ajouter : pour sqrt, elle dit Math library (libm, -lm). On peut aussi vérifier avec nm -D, qui liste les symboles exportés par une bibliothèque partagée (le chemin dépend de l'architecture) :
$ nm -D /lib/aarch64-linux-gnu/libm.so.6 | grep -w sqrt0000000000013840 W sqrt@@GLIBC_2.17$ nm -D /lib/aarch64-linux-gnu/libc.so.6 | grep -w sqrtsqrt est dans libm, pas dans libc : la seconde commande ne trouve rien.
Le piège qui fait douter : avec une constante, l'erreur disparaît.
$ gcc -Wall -Wextra racine_const.c -o racine_const && ./racine_const1.414Ici le programme affiche sqrt(2.0). GCC calcule la valeur pendant la compilation, même sans optimisation, et l'appel à sqrt disparaît du fichier objet : plus rien à chercher dans libm. Le jour où l'argument vient d'une saisie, l'erreur revient. Pour un exemple qui reproduit vraiment le problème, la valeur doit être lue à l'exécution, comme dans racine.c.
Pourquoi l'ordre des -l compte
L'éditeur de liens lit la commande de gauche à droite. Quand il passe sur une bibliothèque statique (.a), il n'en extrait que les fonctions réclamées par ce qu'il a déjà lu, et il n'y revient pas. Une bibliothèque placée avant le fichier qui l'utilise ne sert donc à rien :
$ gcc -c calcul.c$ ar rcs libcalcul.a calcul.o$ gcc -L. -lcalcul main.c -o main/usr/bin/ld: /tmp/ccBIVodg.o: in function `main':main.c:(.text+0xc): undefined reference to `carre'collect2: error: ld returned 1 exit status$ gcc main.c -L. -lcalcul -o main && ./main49-L. ajoute le dossier courant aux endroits où chercher, -lcalcul désigne libcalcul.a. Avec une bibliothèque partagée comme libm.so, la Debian utilisée ici accepte les deux ordres. Ce n'est plus vrai quand l'éditeur de liens tourne avec l'option --as-needed, qui ne garde une bibliothèque partagée que si un fichier lu avant elle en a besoin. Tu ne sais pas toujours comment est réglé l'éditeur de liens de la machine qui compilera ton code ; -Wl, permet de passer l'option à la main pour voir la différence :
$ gcc -Wl,--as-needed -lm racine.c -o racine/usr/bin/ld: /tmp/ccPuxzTA.o: in function `main':racine.c:(.text+0x34): undefined reference to `sqrt'collect2: error: ld returned 1 exit status$ gcc -Wl,--as-needed racine.c -lm -o racine && echo 2 | ./racine1.414La règle qui marche partout : les fichiers source et objets d'abord, les -l à la fin de la commande, et si une bibliothèque en appelle une autre, elle vient avant elle : -lA -lB quand A utilise B.
multiple definition of : le problème inverse
multiple definition of `x' signifie que deux fichiers objets définissent le même symbole, et l'éditeur de liens ne sait pas lequel garder. La cause classique est une variable globale définie dans un en-tête :
int compteur = 0;
void incrementer(void);Inclus par main.c et par compteur.c, cet en-tête crée deux variables compteur, une dans chaque fichier objet :
$ gcc -Wall -Wextra main.c compteur.c -o main/usr/bin/ld: /tmp/ccWvalar.o:(.bss+0x0): multiple definition of `compteur'; /tmp/ccSxaBpC.o:(.bss+0x0): first defined herecollect2: error: ld returned 1 exit status$ gcc -c main.c compteur.c$ nm main.o compteur.omain.o:0000000000000000 B compteur U incrementer0000000000000000 T main U printf
compteur.o:0000000000000000 B compteur0000000000000000 T incrementerB veut dire « définie, dans la zone des variables initialisées à zéro ». Deux B compteur : deux définitions. Le même message apparaît sans le = 0, avec un simple int compteur; dans l'en-tête, parce que GCC 10 a changé le comportement par défaut (-fno-common). La correction est la même dans les deux cas : l'en-tête déclare avec extern, un seul fichier .c définit.
int compteur = 0;
void incrementer(void);extern int compteur;
void incrementer(void);Et dans compteur.c, la ligne int compteur = 0; en dehors de toute fonction. nm main.o affiche alors U compteur : main.o utilise la variable, compteur.o la fournit. Pour une fonction, même logique : le prototype dans le .h, le corps dans un seul .c.
header_guard
counter.h déclare un type et trois fonctions, et Grademe l'inclut deux fois dans le même fichier. Protège l'en-tête avec une garde d'inclusion, puis écris les trois définitions dans counter.c : les déclarations dans le .h, le code dans un seul .c, la séparation qui évite les multiple definition.
Le module du parcours C consacré aux en-têtes va plus loin, jusqu'au type opaque dont les champs restent cachés dans un seul .c :
Questions fréquentes
Pourquoi le message ne donne pas de numéro de ligne ?
Parce que l'éditeur de liens travaille sur des fichiers objets, où il n'y a plus de lignes, seulement des positions comme .text+0x34. Compile avec -g et il retrouve le fichier et la ligne de l'appel grâce aux informations de débogage.
J'ai inclus <math.h>, pourquoi faut-il encore -lm ?
L'en-tête déclare sqrt, il ne contient pas son code. Ce code est dans libm, et avec la glibc, GCC ne la lie que si tu écris -lm, à la fin de la commande.
Dans le terminal d'un exercice Grademe, sqrt marche sans -lm. Pourquoi ?
Les exercices tournent sous Alpine, avec la bibliothèque C musl, qui range les fonctions mathématiques dans la bibliothèque C elle-même : nm -D /usr/lib/libc.so | grep -w sqrt y affiche T sqrt. -lm y reste accepté. Garde-le dans tes commandes, il est indispensable dès que tu compiles avec la glibc, sur Ubuntu, Debian ou WSL.
Que veut dire « collect2: error: ld returned 1 exit status » à lui seul ?
Rien de plus que « l'édition de liens a échoué ». ld s'est terminé avec le code 1 et collect2, l'intermédiaire que GCC lance pour cette étape, le signale. La vraie erreur est toujours au-dessus : undefined reference, multiple definition ou cannot find -l....
Sources
