GCC Weak Symbols und no-as-needed

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.

graph LR PROG(Main
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.