GCC Weak Symbols und no-as-needed
« | 20 Aug 2022 | »Mit dem GCC 6 hat noch alles funktioniert und seit dem Upgrade auf GCC 10 crashen unsere Binaries …
Und wieder einmal durfte ich lernen, dass kleine unscheinbare Änderungen an Standard-Features große Ausfälle bei den Resultaten auslösen können.
Doch so lernte ich etwas über “Weak Symbols”, also “Schwache Symbole” in Bibliotheken.
(Bei der Gelegenheit ein großes Danke an meinen Kollegen, der das Problem analysiert, gelöst und mir vermittelt hat.)
Spezielle Features von Compilern oder Betriebssystemen interessieren mich normalerweise wenig. Schließlich scheitert man an genau diesen, wenn man plattformunabhängigen Code erzeugen will.
Darunter fielen bisher Microsofts
Delay-Loaded DLLs
oder so manche __declspec
Erweiterungen und das gleiche gilt auch für __attribute__
beim GCC.
Nur __declspec(dllexport/dllimport)
und
__attribute__((visibility ("default/hidden")))
sind die Ausnahme, weil sie
de-facto Standards auf ihren jeweiligen Plattformen sind.
GCC Weak-Functions
Der GCC erlaubt, dass eine Funktion oder eine Variable deklariert und
exportiert werden kann, ohne dass sie tatsächlich implementiert sein muss.
Mit dem Attribut __attribute__((weak))
bei z.B. einer Funktion, wird
diese exportiert ohne vorhanden sein zu müssen.
1int lib_function(int param) __attribute__((weak));
Man kann somit ein “Interface” in einer Bibliothek deklarieren ohne es zu implementieren. Und die Bibliothek kann dann bei Bedarf eine andere Bibliothek laden oder direkt linken, die den gleichen Funktionsnamen regulär implementiert.
Denn Weak-Symbole werden von normalen (Strong-)Symbolen überschrieben. So kann man öffentliche von privaten Bibliotheken trennen und z.B.: die privaten Bibliotheksteile dann beliebig austauschen.
Program) IFACE[["Public Interface Library
lib_function() weak"]] IMPL[["Private Implementation Library
lib_function() strong"]] PROG --Link--> IFACE IFACE --Link--> IMPL PROG -. "Invoke
lib_function()" .-> IMPL
Problem Linker-Optimierungen
Dumm ist dann nur, wenn eine Bibliothek mit weak
Symbolen eine andere
mit strong
Symbolen linkt und dann keine direkten Aufrufe zur zweiten
Lib enthält.
Denn moderne Compiler ermitteln, dass keine Aufrufe stattfinden und
entfernen die Bibliothek dann aus der Liste der Abhängigkeiten.
Diese Feature wird durch das Linker-Flag --as-needed
explizit aktiviert.
Am Ende wird die “private” Bibliothek nicht geladen und die Interface-
Bibliothek steht alleine da mit einer “leeren” weak
Referenz.
Ein Programm, das diese weak
Funktion aufruft, springt somit in einen
Null-Pointer hinein,
was selten gut ausgeht.
Und so erklärt sich, warum ältere Compiler ohne diese Optimierung Code
generieren, der solche weak
Funktionen in Bibliotheken samt deren
Abhängigkeiten laden kann, während neuere Kompilate zu einem
Segmentation fault (core dumped)
führen.
Lösung: --no-as-needed
Mit dem Linker-Flag --no-as-needed
, bzw. -Wl,--no-as-needed
beim Compiler
wird diese Optimierung deaktiviert und auch moderne GCC Compiler können dann
jene Bibliotheken korrekt laden, die per weak
Symbols auf andere
Bibliotheken weiterverlinken.
Fazit
Wieder mal etwas interessantes gelernt!
Ein Codebeispiel
im Blog-Classroom zeigt den Sachverhalt im Code auf.
Denn diese Arbeitsweise entdeckten wir bei einer 3rd-Party-Komponente, die
offenbar ähnlich über solche weak
Funktionen zwischen mehreren
Implementierungen umschalten konnte.
Ich bin allerdings der Meinung, dass man das mit dlopen/dlsym
hätte lösen
sollen und keine (jetzt fragile) GCC-Compiler-Erweiterungen hätte nutzen
sollen.
Denn Compiler-Standards ändern sich und weder der C noch der C++ Standard
definieren Richtlinien, wie sich exportierte Funktionen im Detail verhalten.