GCC Problem: Statische Shared-Libs
« | 12 Sep 2021 | »Da denkt man, statische Bibliotheken (static libraries) wären stets “privat” für ihre Nutzer und nur dynamisch gelinkte Bibliotheken (shared libraries) würden ihr Dienstleistungsangebot öffentlich preisgeben.
Doch nicht so beim GCC.
Denn der spielt gerne Twitter und teilt auch Privates quer über
Modulgrenzen hinweg, ohne dass jemand danach gefragt hätte.
Neulich in der Firma
Wir haben einen Bug in einer statischen Lib gefixt. Im Test funktioniert alles, doch im Programm zusammen mit anderen Bibliotheken ist es so, als würde der Patch einfach fehlen.
Wie kann es sein, dass sich eine Bibliothek stets anders verhält, je nachdem in welchen Prozess sie geladen wird?
Die Antwort ist eine neue Art vom “Diamond of Death”:
Zwei dynamische Bibliotheken linken eine bestimmte statische Bibliothek und dann werden beide in einen Prozess genutzt.
Wird nun ein Bug in einer Lib festgestellt, der aus der gemeinsamen statischen Lib kommt und welcher sich in der zweiten shared-lib nicht bemerkbar macht, wird oft nur die “fehlerhafte” Bibliothek samt der statischen korrigiert. Man liefert am Ende nur eine Patch-Datei aus, weil man die zweite für OK hält.
Doch nachdem in Linux grundsätzlich alle
Funktionen einer Bibliothek öffentlich sind (außer man geht dagegen explizit
vor), werden auch die statischen Funktionen in jeder *.so
Datei publiziert.
Und wenn ein Prozess nun beide Libs lädt, ist plötzlich eine Funktion in zwei Implementierungen vorhanden und der Prozess entscheidet sich eben für eine und verwirft die andere.
Was wir also eigentlich wollen ist, dass jede statische Bibliothek in ihrer Variante in einer dynamischen Bibliothek integriert ist, damit sie unabhängig von einander sind.
shared_func1] --static_func-->S1(Private Static Lib
Local Variant X
static_func) L2[Shared Lib #2
shared_func2] --static_func-->S2(Private Static Lib
Local Variant Y
static_func) P((Main
Executable)) P --shared_func1--> L1 P --shared_func2--> L2
Problem: Ein Diamant
Doch leider bekommen wir nur EINE gemeinsam benutzte Variante der statischen Bibliothek. Sie verhält sich also genau so wie eine dynamische Bibliothek.
static_func/] L1[Shared Lib #1
shared_func1] --static_func--> S L2[Shared Lib #2
shared_func2] --static_func--> S P((Main
Executable)) P --shared_func1--> L1 P --shared_func2--> L2
Wenn man per nm
in die .so
Dateien hineinsieht, stellt man fest, dass
die Funktionen der statischen Bibliothek von der dynamischen exportiert
werden.
Lösung: Privatisierung
Linux und der GCC machen es sich stets einfach und exportieren grundsätzlich
alles. Das kann man aber über die Compiler-Option -fvisibility=hidden
abschalten und dann deklariert man per
__attribute__ ((visibility ("default")))
nur genau die Funktionen als
“öffentlich”, die man auch als solche benutzen will.
Siehe GCC-Doku
Alternative: Lokales Laden
Wenn man mit dlopen
arbeitet, kann man auch die Option RTLD_LOCAL
nutzen um dem Prozess das
Publizieren von geladenen Symbolen zu untersagen. Dann sollte jede Lib nur
ihre eigenen mitgebrachten Funktionen sehen und nichts von fremden .so
Dateien.
Fazit
Ich habe noch nie viel davon gehalten, wenn GCC Bibliotheken ihre gesamten Implementierungsdetails mitveröffentlichen. Das führt nur dazu, dass private APIs wie öffentliche behandelt werden und beim nächsten Update regnet es dann unter Umständen Seg-Faults.
Aber dass “Teil-Patches” durch spezielle statische Bibliotheks-Konstruktionen
plötzlich “verschwinden”, war mir bisher auch noch nie so deutlich
aufgefallen.
Und schließlich kann es leicht passieren, dass eine statische “Utility-Lib”
in mehreren Komponenten zum Einsatz kommt, die unterschiedlich release-t
werden. Wenn diese Komponenten in einem Prozessraum zusammenkommen, wird das
Chaos perfekt.
Man muss also immer darauf achten, dass beim Update einer statischen Lib, auch alle von dieser abhängigen dynamischen Bibliotheken re-release-t werden müssen. Oder man verhindert gleich von Anfang an, dass private statische Funktionen plötzlich öffentlich “geteilt” werden.
Wie auch immer … solche störrischen Details sind stets lästig, weil man dann lange nach neuen Alt-Bugs an falschen Stellen sucht.
Also Obacht: Immer nur das notwendigste exportieren lassen.