Patcher GCC pour compiler des Actually Portable Executables
(ahgamut.github.io)- Pour compiler davantage de logiciels C existants avec Cosmopolitan Libc, il ne suffisait pas de tromper le système de build : il a fallu un patch GCC d’environ 2 000 lignes afin que le compilateur transforme automatiquement le code problématique
- L’obstacle venait du fait que des valeurs système comme
SIGTERMetEINVALne sont pas des constantes connues à la compilation, ce qui entre en conflit avec les règles du C pour les labelscasedeswitchet l’initialisation destatic/const struct - Des tentatives ont été faites avec
sed, des expressions régulières Python, un plugin GCC et l’interception de macros du préprocesseur, mais l’erreur sur les labelscasesurvenant pendant le parsing, il était difficile de l’éviter avec un simple plugin - Le patch final s’active avec le flag
-fportcosmo: juste avant que GCC n’émette une erreur, il transforme lesswitchenif/goto, et complète les initialisations de structures en remplissant les valeurs à l’exécution - Des limites subsistent, comme les structures anonymes, les
enum, certainsconst intou les index de tableau dans les expressions d’initialisation, mais cela ouvre la voie à la compilation de logiciels commebash,curl,git,ninjaougccsans modifier leur code source
Objectif du patch GCC
- Un patch d’environ 2 000 lignes pour
gcca été écrit afin de faciliter la création d’Actually Portable Executables avec Cosmopolitan Libc - Après application du patch, les logiciels suivants peuvent être compilés avec des systèmes de build
./configureoucmakebashcurlgitninjagcclui-même
- Les exécutables produits devraient pouvoir s’exécuter aussi sous Linux, FreeBSD, MacOS, OpenBSD, NetBSD et Windows
- Avec la réserve que l’exécution sous Windows n’a pas été testée directement
- Les binaires générés avec GitHub Actions sont disponibles dans les releases de superconfigure
Les points de blocage du portage vers Cosmopolitan Libc
- Après que Cosmopolitan Libc a attiré l’attention en 2021 avec le serveur web
redbean, une question importante a été de savoir dans quelle mesure les logiciels C existants fonctionnaient correctement sur cette libc - Lors du portage de runtimes de langages comme Lua, Wren, Janet ou
quickjs, le même schéma s’est répété - Utiliser des constantes système comme
SIGTERMouEINVALdans les labelscased’une instructionswitchentre en conflit avec les règles de compilation du C - La norme C exige que les labels
casesoient des constantes connues à la compilation- Dans Cosmopolitan Libc, les macros du préprocesseur C liées aux interfaces système devaient être symboliques
- Résultat : du code courant comme
switch(errno) { case EINVAL: ... }pouvait ne pas compiler
Transformer les switch en if et goto
- Si des labels comme
case EINVALoucase ENOSYSdansswitch(errno)ne sont pas des constantes connues à la compilation, GCC émet l’erreurcase label does not reduce to an integer constant - Le patch transforme automatiquement l’instruction
switchselon la structure suivante- Ajout d’une branche de type
if (errno == EINVAL) goto caselabel_EINVAL;pour chaque valeur decase - Ajout d’un
gotovers le labeldefault - Les blocs
cased’origine sont restructurés avec des labels et desgoto endofthis_switch
- Ajout d’une branche de type
- Cette approche n’est pas la plus élégante, mais elle a permis de traiter les cas rencontrés dans plusieurs bases de code
- Comme il fallait aussi gérer des
switchmêlantifdefet fallthrough, une transformation basée surgotoétait l’option la plus simple
L’initialisation des struct subit la même contrainte
- Le problème des constantes connues à la compilation se pose aussi dans les initialisations de
staticou deconst struct - Si
EINVALn’est pas une constante connue à la compilation, certaines des initialisations suivantes ne sont pas valides en C- Initialisation d’une
structlocale ordinaire dans une fonction : possible const structstatic structconst structglobalestatic structglobale
- Initialisation d’une
- Un exemple réel se trouve dans le module
faulthandlerde CPython - La solution consiste à initialiser la structure avec des valeurs factices, puis à ajouter du code qui renseigne les bonnes valeurs avant utilisation
- Dans une fonction, en ajoutant une instruction
if - Pour les initialisations globales ou statiques, en utilisant
__attribute__((constructor))
- Dans une fonction, en ajoutant une instruction
Limites des substitutions de texte et des plugins GCC
- Au départ, l’automatisation de la transformation a été tentée avec un script shell
sed, puis avec un script Python et des expressions régulières afin de gérer aussi les fallthrough - Mais à cause du préprocesseur C et des
ifdef, il était difficile de faire fonctionner correctement les substitutions de texte dans tous les cas- Parce qu’il est difficile de savoir à l’avance quels
ifdefseront actifs lors de la compilation
- Parce qu’il est difficile de savoir à l’avance quels
- La tentative suivante a consisté à utiliser un plugin GCC pour une transformation de l’AST
- Un plugin GCC peut être chargé avec
-fplugin=your-plugin.so - Il peut accéder à l’AST lors d’événements comme
PLUGIN_PRE_GENERICIZE,PLUGIN_FINISH_PARSE_FUNCTIONouPLUGIN_FINISH_DECL debug_treepermet d’afficher l’AST, etwalk_tree_without_duplicatesde le parcourir
- Un plugin GCC peut être chargé avec
- Le problème était que la validation des labels
casese produit pendant le parsing de GCC- Le plugin accède à l’AST une fois le parsing terminé
- Un label invalide
case SIGTERM:ne reste pas dans l’AST - Comme GCC a déjà émis une erreur, il est difficile de le corriger depuis le plugin
Expérimentation autour de l’interception des macros du préprocesseur
- Comme il était difficile d’empêcher les erreurs avant le parsing avec le seul plugin, une méthode interagissant avec le préprocesseur C a été essayée
- Via la structure
cpp_readerdes headers de plugins GCC, il était possible d’enregistrer des callbacks lors du#define, du#undefou de l’utilisation d’une macro - En exploitant la structure des macros de valeurs système de Cosmopolitan Libc, des constantes temporaires ont été insérées
- Exemple :
extern const int SIGTERM; - Exemple :
#define SIGTERM SYMBOLIC(SIGTERM) - Pour le plugin, des valeurs temporaires comme
static const int __tmpcosmo_SIGTERM = ...étaient définies
- Exemple :
- Le plugin interceptait l’utilisation de
SYMBOLIC(SIGTERM)pour la remplacer par une valeur temporaire, puis, à l’étapePLUGIN_PRE_GENERICIZE, remplaçait à nouveau l’AST par le bonVAR_DECL - Ce hack de macros a réussi à contourner les erreurs de
switchet d’initialisation destruct - Cette approche a permis de compiler au moins CPython 3.11
Pourquoi corriger GCC lui-même plutôt qu’utiliser un plugin
- Après discussion avec Justine Tunney, une fois confirmé que le problème pouvait être résolu, une approche plus simple consistant à modifier directement la base de code de GCC a été proposée
- Les hacks à base de plugin et de macros imposaient beaucoup de travail inutile, et la gestion des cas limites et des crashs était complexe
- Par exemple, même dans une situation où seul
case ENOSYSdevait être transformé, la substitution de macros obligeait le plugin à gérer aussi des usages valides commeENOSYS + 1ouprintf("ENOSYS = %d", ENOSYS) - Avec l’approche par patch GCC, la fonctionnalité s’active via le flag
-fportcosmo, sans-fpluginséparé - Juste avant que GCC n’émette l’erreur
case is not constant, il vérifie siflag_portcosmoest activé et, si c’est le cas, effectue les remplacements nécessaires - Le code de transformation d’AST de l’ancien plugin a été déplacé dans GCC pour s’exécuter avant l’appel aux autres callbacks de plugins
- Ce changement a permis de supprimer les hacks liés aux macros
Builds réels et extension du patch
- Au 5 juin 2023, le GCC patché passait tous les anciens cas de test du plugin, et un nouveau binaire a été ajouté au monorepo de Cosmopolitan Libc
- En compilant davantage de code, des corrections et améliorations ont suivi
- Le build de
luaest devenu plus simple, et après avoir trouvé où l’erreurcase constantse produisait dansg++, il est aussi devenu possible de compilerninja - De nouveaux problèmes sont apparus lors de la tentative d’ajouter
ncursesà Python 3.11- Dans
ncurses, certains codes contenaient des expressions binaires dans les valeurs d’initialisation de structures, commeDATA2(PARODD | PARENB, PARODD) - Le patch existant gérait les initialisations avec constantes simples, mais pas les expressions comme
PARODD | PARENB
- Dans
- Le patch a ensuite été étendu pour que les labels
casedu C et les éléments d’initialisation de structures puissent être des expressions arbitraires- Selon la norme C, les labels
casedoivent toujours être des constantes connues à la compilation - Le compilateur patché émet un avertissement pour ces usages, mais poursuit la compilation
- Le C++ est exclu du remplacement d’expressions arbitraires, car cela ne correspond pas au traitement de
constexprparg++11
- Selon la norme C, les labels
- Après ce changement,
ncursesa compilé sans problème
Contraintes restantes et conclusion pratique
- Le patch n’est pas parfait
- Il ne gère pas certaines structures anonymes
- Il ne gère pas certains cas d’
enumet deconst int - Il ne gère pas les cas où
SIGTERMest utilisé comme index de tableau dans une expression d’initialisation - Il peut rester de rares crashs du compilateur avec des initialisations
staticétranges ou lorsqueSIGTERMest placé dans unstatic const int8_t
- Malgré cela, de nombreux contre-exemples évidents ont été éliminés, et beaucoup de logiciels populaires se compilent de manière fluide
- Des environnements de test plus stricts pourraient révéler d’autres points à améliorer
- Il reste possible de corriger manuellement les instructions
switchet les expressions d’initialisation destruct, mais dans beaucoup de cas le compilateur peut s’en charger automatiquement - Si un logiciel C peut être compilé statiquement, en particulier s’il compile aussi avec
musl, il est probable qu’il puisse aussi être compilé avec Cosmopolitan Libc
3 commentaires
Neo adopte désormais un format qui encadre les messages dans des blocs de code ? Le besoin de faire défiler horizontalement et la couleur de texte un peu trop dans les tons rouges sont un peu regrettables...
Ah, le fait que cela soit entouré d’un bloc de code est un bug, snif.
Commentaires sur Hacker News
C’est moi qui ai écrit cet article, mais le titre devrait être « Patching GCC to build Actually Portable Executables », car il fait référence à Cosmopolitan Libc et au format Actually Portable Executable de jart
Grâce à mon patch GCC, il est désormais possible de compiler des logiciels comme vim, emacs, ninja, bash, git et gcc avec Cosmopolitan Libc en passant par les systèmes de build habituels de type autotools/cmake. Les exécutables générés devraient pouvoir fonctionner sur Linux, FreeBSD, MacOS, OpenBSD, NetBSD et Windows. Je n’ai toutefois pas encore testé Windows
La liste des logiciels que j’ai compilés avec cette méthode est ici : https://github.com/ahgamut/superconfigure
Le script superconfigure n’est qu’un wrapper autour du script configure classique utilisé pour compiler des logiciels, auquel il ajoute des flags comme
--enable-staticSi vous voulez essayer de compiler GCC avec Cosmopolitan Libc, vous pouvez utiliser ce dépôt : https://github.com/ahgamut/musl-cross-make/tree/gccbuild
J’espère que tu seras mieux traité sur HN
Par exemple, je me demande s’il y a eu assez de tests pour démontrer l’absence de régressions par rapport à une version glibc
Il y a une issue liée à ce sujet dans le dépôt Go : https://github.com/golang/go/issues/51900
Dans l’ensemble, c’est assez impressionnant, mais j’aimerais comprendre la théorie derrière. Si l’on crée une nouvelle cible de système d’exploitation, il semblerait logique de définir de nouvelles constantes
Au lieu de transmettre telles quelles les constantes propres à chaque système d’exploitation sous-jacent, on pourrait les convertir pour qu’elles aient partout les mêmes valeurs. Ainsi, le code qui utilise ces constantes comme indices de tableau ou dans des instructions case continuerait à fonctionner correctement
Cela pourrait introduire plus de surcharge que l’approche décrite dans l’article, mais ça semblerait bien plus simple, y compris pour les personnes qui veulent compiler de nouveaux programmes avec Cosmopolitan libc
On peut voir la liste des constantes ici : https://github.com/jart/cosmopolitan/blob/master/libc/sysv/c...
À chaque utilisation de l’une de ces constantes, il faudrait embarquer dans le binaire un grand nombre de constantes sous forme de grosse table de correspondance, puis passer par cette table chaque fois que le programme doit les vérifier. Ce ne serait peut-être pas extrêmement lent, mais ce serait clairement perceptible
L’objectif était de faciliter le portage sans modifier excessivement le code source de la libc ou du logiciel à porter, tout en produisant des binaires aux performances comparables, voire meilleures. Avec ces contraintes, ce patch GCC semblait être la meilleure manière de simplifier le processus
Si je tombe sur suffisamment de bases de code qui utilisent
SIGHUPpour initialiser des indices de tableau, j’essaierai peut-être l’approche proposée pour mesurer le compromis. Sinon, n’hésite pas à l’essayer toi-même et à me dire si un ensemble distinct de constantes donne de meilleurs résultatsSi « pas mal de temps a été consacré à éliminer les contre-exemples évidents et beaucoup de logiciels populaires se compilent proprement », alors publier une liste des logiciels compilés avec succès serait utile. Il y en a peut-être déjà une
Le script superconfigure n’est qu’un wrapper autour du script configure classique utilisé pour compiler des logiciels, auquel il ajoute des flags comme
--enable-staticJe me demande s’il existe un usage pratique des APE. Y a-t-il réellement des cas où l’on distribue un seul exécutable censé fonctionner sur plusieurs systèmes d’exploitation ?
lnav lui-même n’est pas compilé en APE, mais l’agent embarqué à l’intérieur l’est. Quand un utilisateur veut lire les logs de cet hôte, cet agent lui est envoyé à distance
Cela évite d’avoir à détecter le type d’OS séparément ou d’ajouter une étape supplémentaire pour inclure plusieurs versions de l’exécutable. Il y a un court billet à ce sujet ici : https://lnav.org/2021/05/03/tailing-remote-files.html
Les programmes compilés statiquement sont à la base de tout, mais au niveau applicatif ils restent minoritaires
APE pose une grande question : « si un langage AOT bas niveau comme le C peut être compilé une fois et exécuté partout, qui a encore besoin d’une machine virtuelle ? »
En plus, la cross-compilation est pénible. Les petits projets open source qui n’ont pas de ferme de build avec tous les BSD finissent souvent par ne pas distribuer de binaires pour ces plateformes
Même kubectl propose des binaires pour Linux, mais pas pour BSD. À moins de compter OSX comme un BSD. Donc, pour ce genre de projet, cela semble être un gain facile
Ils sont trop gros pour être compilés à la volée sur la machine de l’utilisateur. Ils embarquent plusieurs grosses dépendances tierces en C++, et la compilation prend entre 2 et 20 minutes selon le matériel
Il faut que cela fonctionne sur macOS x86-64+ARM, Linux x86-64 et Windows x86-64. ARM Linux serait un plus
Pour l’instant, j’étudie WASM, mais cela nécessite un runtime WASI installé
Pour ceux qui sont perdus : « Actually Portable Executables » https://justine.lol/ape.html
Remplacer
switchparifest assez grossier. On pourrait probablement corriger ça en mappant vers un ensemble de constantes neutres vis-à-vis de la plateforme et en déréférençant les symboles runtime via des fonctionsPar exemple, avec quelque chose comme
switch(CosmoMappErrno(EINVAL))etcase COSMO_EINVAL:, on éviterait de transformer le code en amas de gotoBien sûr, si tu veux dire que le compilateur pourrait reconnaître ce genre de switch et, au lieu de le réécrire en arbre de if, remplacer les labels par des constantes spéciales propres à cosmo pour chaque valeur, puis envelopper l’entrée du switch dans un appel de fonction qui mappe la valeur de la plateforme runtime actuelle vers cette constante cosmo dédiée, avec passage inchangé des autres valeurs, c’est différent
Ce serait peut-être en réalité une transformation du compilateur plus simple. Mais cela ajoute la complexité de devoir reconnaître quel ensemble de constantes est utilisé et d’appliquer l’appel de mapping approprié à l’entrée du switch. Il faut aussi créer ces mappings côté bibliothèque, donc cela demande de la complexité
Comme il existe déjà une implémentation qui fonctionne avec une autre méthode de transformation, je ne sais pas si l’auteur aura envie de mettre cela en œuvre
Enfin, cette approche par mapping introduit un risque de mauvais mapping, avec aussi des subtilités comme la nécessité d’inverser l’entrée avant le mapping pour du code qui utilise des valeurs errno négatives
Si j’aime le tableau if-else-goto, c’est parce qu’il s’imbriquait parfaitement avec d’autres parties de GCC. Si tu regardes mon patch[1], tu verras qu’il n’a fallu modifier qu’une toute petite partie du code existant de GCC pour ajouter cette fonctionnalité
[1]: https://github.com/ahgamut/gcc/tree/portcosmo-11.2
Je ne comprends pas pourquoi on est si surpris par le code qui utilise
SIGxxxcomme index. Les numéros de signal sont bien définis, généralement continus, et réputés commencer à 1En plus, cette erreur est apparue en même temps qu’une erreur du compilateur dans mon patch qui m’a pris longtemps à déboguer, donc ça m’est resté comme une surprise marquante
Je me demande si vous considérez vraiment ce pattern comme courant. J’aimerais aussi savoir dans quelles codebases on le retrouve, en dehors de busybox. S’il y a beaucoup d’exemples, je pourrais passer du temps à mettre à jour le patch pour gérer ce pattern
Si errno est un problème à ce point, je ne vois pas bien quel espoir il reste pour faire tourner la bibliothèque standard C++ et std::error_code
Là, il n’y a aucun moyen d’utiliser des macros magiques
std::error_coden’est pas une constante, donc il ne devrait pas y avoir le même problème