1 Punkte von GN⁺ 17 시간 전 | 1 Kommentare | Auf WhatsApp teilen
  • Code, der sich strikt nur an den ISO-C-Standard hält, ist selten; reale C-Codebasen verlassen sich auf nicht standardisierte Erweiterungen, um Funktionen zu ergänzen und Lücken je nach Compiler und Bibliothek zu umgehen
  • Ein nützlicher C-Compiler muss schon System-Header wie <stdio.h> verarbeiten können, aber glibc errichtet durch GNU-Erweiterungen und Annahmen wie __attribute__((packed)) und #include_next Hürden
  • Die Byteswapping-Logik von SDL kann bei vorhandenen ISA-Makros inline assembly wählen, sodass auch Compiler, die weder GCC noch clang sind, GCC-artige Erweiterungen unterstützen müssen
  • Die Behandlung von extern inline in OpenBSD und Gnulib macht die Kompatibilität der inline-Semantik durch Unterschiede zwischen C99 und GCC-Semantik, plattformspezifische Verzweigungen und _FORTIFY_SOURCE-Bedingungen kompliziert
  • Kleine C-Compiler müssen zwischen Upstream-Patches, Downstream-Patches, eigenen Guards und dem Nachahmen von GCC-Kompatibilität wählen; die Ausweitung von Feature-Test-Makros scheint der bessere Weg zu sein

Die erste Hürde durch glibc-Header

  • Um ein nützlicher C-Compiler zu sein, muss man Header der System-C-Bibliothek präprozessieren und parsen können; wer <stdio.h> nicht verarbeiten kann, wird kaum schon ein Hello World kompilieren
  • In GNU/Linux-Umgebungen führt diese Hürde direkt zu glibc
  • glibc prüft in sys/cdefs.h, das fast von allen libc-Headern indirekt eingebunden wird, Compiler-vordefinierte Makros, um zu bestimmen, welche Erweiterungen unterstützt werden
  • Nicht unterstützte Erweiterungen werden behandelt, indem die betreffenden Definitionen entfernt werden, aber auch diese Kompatibilitätslogik kann in der Praxis kaputtgehen
  • struct epoll_event und __attribute__((packed))

    • struct epoll_event in sys/epoll.h unter Linux ist eine packed struct, die GNU-__attribute__((packed)) verwendet
    • Dieses Attribut verändert auf 64-Bit-Systemen das Layout der Struktur; wenn es ignoriert wird, ist die ABI kaputt
    • Es reicht nicht aus, dass ein Compiler __attribute__((packed)) implementiert
    • In sys/cdefs.h gibt es Code, der __attribute__(xyz) zu einem leeren Makro definiert, wenn es sich nicht um GCC, clang oder tcc handelt
    • Dadurch kann das packed-Attribut in glibc-Headern auch bei anderen Compilern entfernt werden, selbst wenn diese es unterstützen
    • Da der epoll-Header Linux-spezifisch ist, lässt sich ein strenger Maßstab der C-Standard-Portabilität hier nur eingeschränkt anlegen
  • limits.h und #include_next

    • Einige C-Header wie stddef.h, stdint.h, limits.h und float.h werden auch in freestanding Implementierungen benötigt und müssen daher vom Compiler bereitgestellt werden
    • POSIX verlangt, dass limits.h zusätzlich zu den Standard-C-Konstanten auch POSIX-spezifische Konstanten definiert; deshalb wird über dem limits.h des Compilers noch ein plattformspezifisches limits.h benötigt
    • Das <limits.h> von glibc definiert, wenn kein GNU C vorliegt, die ANSI-limits.h-Werte direkt und holt in GCC-Umgebungen per #include_next <limits.h> den Compiler-Header
    • Diese Struktur setzt voraus, dass das GCC-exklusive builtin-limits.h bestimmte Makros definiert, und hängt außerdem von der Erweiterung #include_next ab
    • clang muss diese Struktur ebenfalls umgehen

Funktions­erkennung in SDL und das Problem mit inline assembly

  • Die Byteswapping-Funktionen in SDL_endian.h verwenden nach Möglichkeit Compiler-Builtins oder inline assembly und fallen als letztes Mittel auf eine gewöhnliche Implementierung mit Bitoperationen zurück
  • Die Erkennungslogik arbeitet grob in dieser Reihenfolge
    • Wenn GCC oder clang vorliegt und __has_builtin(__builtin_bswapX) verfügbar ist, werden Builtins verwendet
    • Bei MSVC 8.0 oder neuer wird das MSVC-Intrinsic per #pragma verwendet
    • Wenn ISA-spezifische Makros wie __x86_64__ definiert sind, wird inline assembly verwendet
    • Andernfalls wird die allgemeine Implementierung mit Bitoperationen genutzt
  • Definiert ein Compiler, der weder GCC noch clang ist, aus gutem Grund ISA-spezifische vordefinierte Makros, wird diese Reihenfolge problematisch
  • Selbst wenn dieser Compiler ein bswap-Builtin und den Spezialoperator __has_builtin bereitstellt, kann die Logik trotzdem versuchen, GCC-artige inline assembly zu verwenden
  • Dadurch entsteht faktisch die Erwartung, dass auch ein unbekannter Compiler GCC-Stil-inline-assembly unterstützt

OpenBSD libc und die Verwirrung um extern inline

  • Einige Header von OpenBSD enthalten Inline-Funktionsdefinitionen, die der Compiler bei Optimierung optional verwenden kann
  • Diese Funktionen werden über das Makro __only_inline definiert; wenn der Compiler sie nicht tatsächlich inlined, muss stattdessen ein externes Symbol verwendet werden
  • Dafür werden also Inline-Funktionen mit externem Linkage benötigt
  • Unterschiede zwischen C99-inline und GCC-inline-Semantik

    • inline ist in C99 festgelegt, aber das Standardverhalten kollidiert mit dem nicht standardisierten GCC-Verhalten aus der Zeit vor C99
    • Eine Inline-Definition in einem Header muss zusammen mit dem Funktionskörper extern inline verwenden; in diesem Fall wird keine tatsächlich exportierte Funktion emittiert
    • In einer Translation Unit muss die Deklaration nur mit inline versehen sein, um die Funktionsdefinition zu exportieren
    • Die Bedeutung von inline unterscheidet sich auch zwischen C++ und C
    • Diese Unterschiede werden im Artikel von Youtao Guo ausführlich behandelt
  • OpenBSDs __only_inline

    • OpenBSD verlässt sich auf GCC-inline-Semantik
    • Um Unterschiede zwischen GCC-Versionen abzudecken, legt das Makro __only_inline in sys/cdefs.h bei neueren GCC-Versionen per explizitem __attribute__ die ältere gnu89-inline-Semantik fest
    • Bei Nicht-GNU-Compilern wird __only_inline als static-Linkage definiert
    • Dadurch können Funktionen mit widersprüchlichem Linkage deklariert und definiert werden und daran zerbrechen
  • Der Umweg über _ANSI_LIBRARY

    • OpenBSD respektiert das Makro _ANSI_LIBRARY
    • Wenn dieses Makro definiert ist, wird die problematische __only_inline-Definition in Standard-Headern wie signal.h vollständig weggelassen
    • Die optimierte Version erhält man dann zwar nicht, aber zumindest funktioniert der Build
  • Gnulibs Kompatibilitätscode für extern inline

    • Der Kompatibilitätscode von Gnulib für extern inline taucht auch beim Build von Guile und nano auf
    • extern-inline.m4 enthält komplexe bedingte Verzweigungen, um kaputte und merkwürdige Implementierungen dieses C-Eckfalls zu behandeln
    • Die Bedingungen spiegeln Unterschiede zwischen Apple, DragonFly, FreeBSD, GCC, clang, PCC, HP cc, PGI, SunPro C, _FORTIFY_SOURCE, __GNUC_STDC_INLINE__ und __GNUC_GNU_INLINE__ wider

Android bionic und die Annahme von clang

  • bionic ist die libc von Android, und ihre Header setzen sogar stärker auf clang als auf GCC
  • Die bionic-Header verwenden für Nullability-Checks viele clang-exklusive Erweiterungen wie _Nonnull und _Null_unspecified
  • Solche Makros lassen sich per Kommandozeilen-Flag nicht besonders schwer per #define neutralisieren
  • Wenn man über Termux ein Android-Smartphone als native aarch64-Entwicklungsumgebung nutzt, wird dieses Problem in den bionic-Headern sichtbar
  • _Null_unspecified wird auch __BIONIC_COMPLICATED_NULLNESS genannt; die zugehörige Definition steht in bionics sys/cdefs.h

Vor welchen Entscheidungen kleine C-Compiler stehen

  • Code, der sich strikt nur an den ISO-C-Standard hält, ist in der Praxis selten, und viele C-Codebasen hängen von nicht standardisiertem Verhalten und Spracherweiterungen ab
  • Diese Abhängigkeiten entstehen nicht nur für zusätzliche Funktionen, sondern auch beim Umgehen unterschiedlicher Bugs und Lücken je nach Compiler und Bibliothek
  • Codebasen, die mehrere Umgebungen unterstützen wollen, setzen auf Präprozessor-Prüfungen und Guards, aber dieser Ansatz bricht leicht und ist schwer handhabbar
  • Beim Bau eines C-Compilers wie antcc treten solche Kompatibilitätsprobleme immer wieder auf
  • Wenn viele Open-Source-Projekte auch in nicht essenziellen Bereichen von compiler­spezifischen nicht standardisierten Erweiterungen und Verhaltensweisen abhängen, steigt die Last für alternative Compiler
  • Gleichzeitig ist es schwer zu verlangen, dass alle Entwickler ihren C-Code auf mehreren Compilern testen, einschließlich kleinerer und weniger bekannter Compiler
  • C-Portabilität ist schon für sich genommen schwierig genug
  • Aus Sicht von Compiler-Autoren gibt es vier mögliche Wege
    • versuchen, die Inkompatibilitäten Upstream zu patchen
    • bekannt genug werden, damit Entwickler eigene #ifdef-Prüfungen und grundlegende Tests hinzufügen
    • das Problem Downstream behandeln und Patches oder separate Patches verteilen
    • sich als bestimmte GCC-Version ausgeben und die betreffenden Erweiterungen implementieren
  • Upstream-Patches wirken wie ein schwer zu gewinnender Kampf, und Downstream-Patches sind der einfachste Weg
  • Um viele Codebasen mit minimaler Verwirrung für Nutzer und Entwickler zu unterstützen, ist das Nachahmen von GCC-Kompatibilität realistisch, aber mit hohem Implementierungsaufwand verbunden
  • clang definiert __GNUC__=4, __GNUC_MINOR__=2 und __GNUC_PATCHLEVEL__=1, um Kompatibilität zu GCC 4.2.1 zu beanspruchen
  • clang ist heute fast ein eigenes Unterstützungsziel, aber es war so viel Aufwand nötig, damit sich der Linux-Kernel mit clang kompilieren ließ, dass Patches in beiden Projekten erforderlich waren

GCC-Makros und das Problem des Hinterherlaufens

  • Auch das Vorgeben, GCC zu sein, hat Probleme
  • Viele Codebasen prüfen nur #ifdef __GNUC__ und verwenden dann womöglich neuere GCC-Erweiterungen ohne Versionsprüfung
  • In diesem Fall muss ein alternativer Compiler ständig hinterherlaufen
  • Einer der Gründe, warum clang trotz Unterstützung für GNU-Erweiterungen, die neuer als 4.2.1 sind, den Wert seiner __GNUC__-Makros nicht erhöht, liegt genau hier
  • Hintergrund dazu findet sich in der LLVM-Diskussion über die Anhebung von Clangs __GNUC__-Minor-Version

Ein besserer Weg und der aktuelle Stand

  • Idealerweise sollten statt compiler­spezifischer Guards und Versionsprüfungen Feature-Test-Makros viel breiter eingesetzt werden
  • Nützliche Feature-Test-Makros sind __has_builtin, __has_feature und __has_attribute
  • Auch standardisierte Makros nach dem Muster von __STDC_NO_VLA__ könnten häufiger verwendet werden
  • In der *NIX-Welt ist ein quasi-duopolistischer Zustand von GCC und clang derzeit, im Guten wie im Schlechten, der Standard
  • Die Entwicklung unabhängiger kleiner C-Compiler geht dennoch weiter

1 Kommentare

 
Lobste.rs-Kommentare
  • (Autor des kefir-Compilers) Das __attribute__-Problem in <sys/cdefs.h> gehört meiner Erfahrung nach zu den unerquicklichsten Fällen. Es zerstört epoll, übliche packed-Strukturen, Konstruktoren und Symbolsichtbarkeit, weshalb ich am Ende diesen Monkeypatch-Header mit kefir mitliefern musste
    Das ist nicht ideal, aber vermutlich der praktikabelste Ansatz, und in der Praxis konnten wir damit die meisten Custom-Patches aus externen Test-Suites entfernen
    Eine andere Fehlerklasse sind fehlerhafte alternative Codepfade. Manche Projekte versuchen, den Compiler zu erkennen und sich entsprechend anzupassen, aber auf alternativen Compilern werden diese Fallback-Pfade oft zu wenig getestet, sind voller Bugs oder schlecht gepflegt. Aus Sicht eines Compiler-Autors ist das viel frustrierender, als direkt mit „nicht unterstützter Compiler“ zu scheitern. Man muss dann nämlich seltsame Fehlkompilierungen selbst debuggen, etwa Breiteninkonsistenzen bei Integer-typedefs zwischen Programm und vorcompilierten Bibliotheken

    • Ähnliches passiert auch bei Terminals. Wenn man $TERM nicht auf xterm-256color setzt und sich als xterm ausgibt, geht alles Mögliche kaputt
      Ich habe wirklich keine Ahnung, wie man das lösen soll. Am Ende müssen unsere Projekte wohl einfach weit genug verbreitet und bekannt werden. Ganz einfach!
    • Der Ansatz mit Monkeypatch-Headern scheint auch von slimcc verwendet zu werden und wirkt wie ein ziemlich brauchbarer Kompromiss
      Diese seltsamen Fehlkompilierungen, die durch schlecht gepflegte Compiler-Erkennungs-Fallbacks entstehen, habe ich wohl auch schon ein paar Mal erlebt, und sie sind wirklich lästig
  • Ich entwickle cproc hauptsächlich auf linux-musl, daher wusste ich nicht, dass glibc __attribute__ bei anderen Compilern deaktiviert, aber das ist tatsächlich eine ziemlich schlechte Situation. In den Kommentaren steht zwar, dass die Verwendung von Attributes ignoriert werden könne, berücksichtigt aber nicht, dass die meiste Anwendungscode sys/cdefs.h indirekt einbindet und dabei Attributes nutzen kann, die nicht ignoriert werden dürfen
    Neben packed werden auch aligned und constructor häufig verwendet
    Ich frage mich, ob das irgendwo im Issue-Tracker gemeldet ist. Der Großteil der Attribute-Nutzung in cdefs.h scheint bereits durch __glibc_has_attribute geschützt zu sein, deshalb frage ich mich, was die pauschale Deaktivierung von __attribute__ überhaupt erreicht und ob man sie entfernen könnte
    Problematisch sind auch Funktionen, die libc-Header verwenden, für die Compiler ihre Unterstützung nicht sauber signalisieren können. Es sind Dinge, die sich nicht über __has_attribute oder __has_builtin erkennen lassen; ein Beispiel, das mir einfällt, sind __asm__-Labels. NetBSD nutzt sie zum Umbenennen von Symbolnamen und wirft #error, wenn weder __GNUC__ noch __PCC__ gesetzt ist. Ich weiß allerdings auch nicht, was man sonst vorschlagen sollte, außer es einfach zu versuchen und bei fehlender Unterstützung scheitern zu lassen
    Ich hatte auch Probleme rund um __builtin_va_list. Manche libc-Implementierungen definieren ohne __GNUC__ define va_list to void * oder haben sogar widersprüchliche Definitionen. Auch das lässt sich nicht mit __has_builtin testen. __has_builtin(__builtin_va_arg) könnte ein hinreichend guter Test sein, aber ich weiß nicht, wie man macOS dazu bringen sollte, das zu korrigieren

    • Ich habe schnell nach der Verwendung von __attribute__ in /usr/include/sys und /usr/include/bits gesucht und viele ungeschützte Stellen gefunden. Meistens waren es __format__, __aligned__, __noreturn__, also müsste man auch diese korrigieren
      Insgesamt scheint glibc Kompatibilität mit Nicht-GCC-Compilern nicht besonders zu priorisieren, daher weiß ich nicht, wie wahrscheinlich es ist, dass solche Patches angenommen würden. Nach einem System-Upgrade Anfang des Jahres hat glibc in Linux-Headern ungeschützte Nutzung von __SIZE_TYPE__ hinzugefügt, wodurch mein Compiler einige Projekte nicht mehr kompilieren konnte. Ich habe das gemeldet, aber es ist noch nicht behoben, und am Ende habe ich vordefinierte Makros im Stil von __X_TYPE__ ergänzt, um GCC zu entsprechen
      Für das Problem mit __asm__-Labels fällt mir auch keine gute Lösung ein. Wenn das Umbenennen per asm-Name für die Funktionsfähigkeit wirklich zu 100 % nötig ist, könnte es besser sein, es einfach zu versuchen und bei Fehlern zu scheitern, statt den Compiler zu prüfen
      __builtin_va_list ist ziemlich gravierend. Ich hätte erwartet, dass __has_builtin(__builtin_va_list) funktioniert, aber anscheinend ist das nicht der Fall