Compiler du C/C++ de manière vraiment statique et faire de la cross-compilation facilement

Quand on développe en C/C++, on compile par défaut des exécutables qui sont liés dynamiquement aux bibliothèques qu’ils utilisent, à commencer par la bibliothèque standard du C (la libc), mais aussi d’autres bibliothèques nécessaires. Et quand on compile, c’est naturellement pour l’architecture (le couple processeur/système d’exploitation) sur laquelle on est.

On va voir qu’il est aujourd’hui très facile de dépasser tout ça.

Compilation statique

Prenons le plus simple des programmes :

#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("Nombre d'arguments : %d\n", argc);
    return (0);
}Langage du code : C++ (cpp)

Quand on le compile simplement (gcc main.c), si on regarde ensuite ses dépendances (ldd a.out), on verra le résultat suivant :

linux-vdso.so.1
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6
/lib64/ld-linux-x86-64.so.2

Trois dépendances sont listées. La première concerne le vDSO (virtual dynamic shared object), qui est utilisé pour mettre à disposition certaines fonctions du kernel dans l’espace utilisateur. La deuxième correspond la bibliothèque standard du C. La troisième est justement le linkeur dynamique nécessaire pour accéder aux deux premières.

Mais parfois, on souhaite fournir un exécutable réellement statique, qui n’ait pas la moindre dépendance. L’un des moyens pour cela est de linker statiquement avec les bibliothèques nécessaires… sauf que cela pose problème pour la glibc.

Linker statiquement la glibc est un problème technique, car elle est disponible uniquement sous forme statique dans les environnements de développement, à moins de commencer par la recompiler − mais c’est un peu lourdingue.
Et linker statiquement la glibc est aussi un problème de licence, car vous ne pouvez pas forcément fournir votre code sous une licence compatible avec la LGPL.

Toutefois, la glibc n’est que l’implémentation de la bibliothèque standard du C fournie par GNU. Elle est utilisée par la plupart des distributions Linux, mais il est possible de la remplacer par une autre implémentation, comme la uClibc qui a été conçue pour les systèmes embarqués (et qui est aussi sous licence LGPL). Il existe aussi la bibliothèque musl, qui est sous licence MIT, et avec laquelle il est donc possible de linker statiquement n’importe quel programme, quelle qu’en soit la licence.

Le truc, c’est qu’il n’est pas si simple que ça de configurer gcc ou LLVM pour qu’ils utilisent musl à la place de la glibc.

C’est là que le langage de programmation Zig vient à la rescousse. C’est un langage qui veut être un concurrent moderne du C, au même titre que le Rust ou le Go. Mais ce qui nous intéresse ici, c’est qu’il est possible d’utiliser la chaîne de compilation de Zig comme un remplaçant direct de gcc/LLVM.
C’est d’autant plus intéressant que Zig est extrêmement simple à installer : il n’y a qu’un fichier à télécharger, puis il suffit d’utiliser zig cc à la place de gcc dans toutes les commandes de compilation. Dans vos fichiers Makefile, il suffit souvent de remplacer CC = gcc par CC = zig cc et le tour est joué.

Pour compiler un programme complètement statique, on va donc indiquer qu’il faut utiliser la bibliothèque musl, en ajoutant l’option -target x86_64-linux-musl :

zig cc -target x86_64-linux-musl main.c

En regardant les dépendances de l’exécutable généré (ldd a.out), on obtient maintenant :

n'est pas un exécutable dynamique

C’est réussi. Retenez bien qu’on n’a pas eu besoin d’installer la bibliothèque musl. L’exécutable zig contient tout ce qui est nécessaire.

Cross-compilation

Maintenant qu’on a utilisé Zig pour compiler statiquement, on va pouvoir l’utiliser tout aussi facilement pour compiler notre programme pour d’autres architectures.

Pour connaître la liste des architectures supportées, il faut taper :

zig targets

La section utile est celle intitulée « libc ». Sur un système Linux classique, la liste est quand même assez longue, avec 60 combinaisons processeur/OS/libc.
On peut remarquer que sont supportés les processeurs x86 (Intel/AMD) en 32 et 64 bits, ARM (32/64 bits), RISC-V (64 bits), MIPS (32/64 bits), et plusieurs autres plus anciens (Sparc, PowerPC) ou exotiques (C-Sky, S/390), de même que le WASM (32/64 bits).
Au niveau des systèmes d’exploitation, c’est principalement du Linux (avec glibc ou musl), mais certains processeurs sont compatibles avec Windows (x86 en 32/64 bits, ARM en 32/64 bits) et macOS (x86 64 bits, ARM 64 bits).

Pour cross-compiler vers Windows x86 64 bits :

zig cc -target x86_64-windows-gnu main.c

Pour cross-compiler vers macOS ARM 64 bits :

zig cc -target aarch64-macos-none main.c

Pour cross-compiler vers Linux RISC-V avec compilation statique :

zig cc -target riscv64-linux-musl main.c

Là encore, l’exécutable zig contient tout ce qui est nécessaire pour compiler sur toutes ces architectures. C’est vraiment bluffant.

Je regrette que Zig ne supporte pas − pour le moment − la cross-compilation vers les systèmes BSD (OpenBSD, FreeBSD, NetBSD, DragonFly BSD). Mais comme ils proposent tous la possibilité d’exécuter des programmes Linux, ce n’est pas si grave.

Laisser un commentaire

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

Notifiez-moi des commentaires à venir via email. Vous pouvez aussi vous abonner sans commenter.