GCC Problem: Statische Shared-Libs

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.

graph TD L1[Shared Lib #1
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.

graph TD S[\Public Static Lib
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.

1$ nm ./libshared_lib1.so
2...
30000000000001139 T shared_lib1_function
40000000000001154 T static_lib_function

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.