Der Begriff der “statisch-gelinkten-Bibliotheken” (static libraries
genannt) ist meines Erachtens nur entstanden, um ein Gegenteil zu
“dynamisch-gelinkten-Bibliotheken” (auch als shared libraries
bekannt)
zu schaffen.
Denn eigentlich war vor der Ära der Betriebssysteme quasi alles “statisch”
gelinkt, oder anders gesagt, ein Programm bestand aus genau einem Code-Modul,
welches die gesamte Funktionalität beinhaltete.
Auf Mikrocontrollern wird auch heute in erster Linie statisch kompiliert.
Und nachdem dieses Thema gerne zum Diskussionspunkt zwischen “gutem” und “schlechten” Programmierstil wird, möchte ich hier mal alle meine Erkenntnisse zusammentragen.
Im klassischen C und C++, wie auch in vielen anderen Nicht-Interpreter Sprachen werden Quellcodes in Maschinencode übersetzt und Funktionen wie auch Variablen erhalten beim Kompilieren vorerst Platzhalter für die Adressen, an denen sie operieren sollen.
Der Linker hat dann nach dem Kompilieren die Aufgabe, die einzelnen Programmteile in eine Binärdatei zusammenzufassen und alle Platzhalter durch Adressen zu ersetzen, damit der Prozessor sie beim Ausführen anspringen kann.
Beim statischen Kompilieren passiert genau das und wir erhalten am Ende eine Binärdatei (unter Windows: eine EXE) mit dem gesamten Programmcode. Und auf Mikrocontrollern war und ist das die primäre Art der Kompilierung.
Moderne Betriebssysteme erlauben uns allerdings Code auch “dynamisch” zu
linken. Es können dabei mehrere Bibliotheksdateien entstehen (unter Windows
*.dll
und unter Linux *.so
), die vom Hauptprogramm “dynamisch” eingebunden
werden.
Das Programm wird dabei mit Verweisen versehen, dass Code-Teile in anderen
Bibliotheken implementiert sind und entweder kümmert sich das Betriebssystem
darum, dass diese “dynamischen” Bibliotheken mit dem Programm mitgeladen
werden, oder das Programm leitet die notwendigen Schritte selbst ein.
Beispiel: 1 Programm + 2 dynamische Libs = 3 finale Binärdateien
Beispiel: 1 Programm + 2 statische Libs = 1 finale Binärdatei
Dynamische Bibliotheken wurden geschaffen, damit mehrere Programme nicht stets Kopien der gleichen Algorithmen und Funktionen in sich tragen müssen. Eine dynamische Bibliothek exportiert also häufig genutzte Funktionen und mehrere Programme können dann auf die gleiche Bibliothek verweisen und ihren Code gemeinsam nutzen.
Das spart einerseits Speicherplatz und wenn eine Bibliothek einen Fehler aufweist, kann man diesen an einer Stelle fixen womit alle Programme den Bugfix erhalten, ohne selbst angepasst werden zu müssen.
Vor allem Betriebssysteme stellen ihre eigenen Funktionen deshalb gerne als dynamische bzw. gemeinsam-genutzte Bibliotheken (== shared library) zur Verfügung.
Würde man eine Vielzahl von Programmen erstellen, die alle im gleichen Umfeld arbeiten, wären dynamische Libs von Vorteil, wo jede Funktionsgruppe eine Bibliothek bildet und die Programme dann recht klein sind und nur noch die Bibliotheksfunktionen mit etwas Logik zusammenziehen.
Wären die Programme mit statischen Bibliotheken gebaut, wären sie wesentlich größer, da sie alle Kopien der statischen Funktionen in sich tragen. So kann ein statisches Projekt gerne auch einige Megabytes größer ausfallen, als mit dynamischen Libs.
Warum sollte man es also bei eigenen Programmen anders machen und statische Libs nutzen?
Dynamische Bibliotheken müssen ein festes API definieren, also eine binäre Schnittstelle, die zwischen Programm und Bibliothek vereinbart ist, und die sich nicht ändern darf, ansonsten kommt es zu Korruption, Abstürzen oder Schlimmerem.
Auch der Compiler muss sich exakt
an diese Schnittstelle halten, und leider verhindert genau das die Anwendung
von Optimierungen.
Bei statischen Bibliotheken kann der Compiler bei aktivierter
Link-Time-Optimization
(LTO, unter MSVC auch Whole-Program-Optimization oder WPO genannt)
am Ende in alle Funktionen hineinsehen und viele Optimierungen
durchführen, die durch ein “fixiertes dynamisches API” verhindert werden.
Beispiel:
Dynamisch gelinkt, muss die Funktion get_setting()
eine echte Funktion
sein und der Compiler muss Code für den Aufruf und die Rückgabe generieren.
Statisch gelinkt, erkennt der Compiler die Konstante und kann so tun, als
hätten wir printf("%d", 42);
oder sogar nur printf("42");
geschrieben.
Und das ist wesentlich schneller und kann auch weniger Speicherplatz erfordern.
Ein anderer Vorteil ist, dass das Deployment einer einzigen größeren
“All-in-one-EXE” viel einfacher ausfallen kann, als mit vielen kleinen
.dll
oder .so
Dateien nebst der ausführbaren Programmdatei.
Ich ziehe daher im Embedded-Bereich statisch kompilierte “All-in-one-Programme” einer Fülle von kleinen Bibliotheken stets vor, weil man da nicht versehentlich vergessen kann, eine der vielen dynamischen Libs mitzuliefern.
Letztendlich entscheidet aber der Anwendungsfall, wie effizient welche Methode am Ende ist.
Grundsätzlich teilt man dem Compiler und dem Linker per Kommandozeilenparameter mit, ob aus einer Bibliothek eine statische oder dynamische werden soll und ähnliches geschieht auch bei dem finalen Programm, wenn es entweder die eine oder andere Variante von Bibliotheken nutzen soll.
Hinweis: Auch die C-Runtime ist eine Bibliothek und kann statisch und dynamisch gelinkt werden.
Statische Bibliotheken haben beim MSVC
die Dateierweiterung .lib
und sind quasi eine Zusammenfassung aller .obj
Dateien, die beim Kompilieren einzelner .c
und .cpp
Dateien entstehen.
In der Visual Studio IDE entscheidet man in den Projekt-Einstellungen per
Menü, was man haben möchte und dann werden alle Dateien in dem Projekt
bereits mit den korrekten Optionen gebaut.
Will man eine statische Bibliothek selbst anlegen, muss man all .c
und
.cpp
Dateien mit cl.exe /c
zu Objektdateien kompilieren und dann
per lib.exe
die entstandenen .obj
Dateien in eine .lib
Datei verwandeln.
Falls man nicht den /Fo:
Parameter setzt um den Namen der Objektdatei
manuell zu bestimmen, erhält diese immer den Basisnamen der Ursprungsdatei.
z.B.: my_file.c
wird zu my_file.obj
Die entstandene .lib
Datei ist unsere statische Bibliothek und diese kann
zum Linken des ausführbaren Programms einfach der Liste der zu linkenden
Dateien beim Aufruf von link.exe
hinzugefügt werden:
In der Standard-Konfiguration, versucht der MSVC stets die C/C++-Runtime dynamisch zu linken. Das macht es erforderlich, dass man immer die C/C++ Runtime Libraries installieren muss, damit das eigene Programm läuft.
Mit der Kommandozeilen-Option /MT
(bzw. /MTd
im Debug Modus) wandert
der C-Runtime Code “statisch” in die EXE und somit ist keine
Runtime-Lib-Installation mehr notwendig.
Beim GCC werden statische Bibliotheken mit dem Tool-Aufruf ar -r
aus
kompilierten Objektdateien (.o
) erzeugt und haben am Ende die
Dateierweiterung .a
.
Der erste Dateiname nach den Kommando-Optionen ist die Zieldatei der
statischen Bibliothek, danach folgen alle Objektdateien, die eingebunden
werden sollen.
Die GCC und Unix Konvention legt für alle Bibliotheken das Präfix
lib
fest. Aus dem Projekt namens my_static_lib
wird dann eine
libmy_static_lib.a
Datei.
Dieser Konvention sollte man nach Möglichkeit folgen, damit alle weiteren
Tools die Bibliothek problemlos finden können.
Für C++ Dateien wird gcc
einfach durch g++
ersetzt,
der ar
Aufruf bleibt hingegen gleich.
Zum Erstellen eines ausführbaren Programms, wird über die Linker Option
-l
der Namen des Bibliothekenprojektes angegeben.
Der Parameter -lmy_static_lib
bewirkt, dass libmy_static_lib.a
statisch
gelinkt wird. Falls es auch eine libmy_static_lib.so
geben sollte,
kann man den Dateinamen auch vollständig angeben, z.B.: -llibmy_static_lib.a
.
Auch unter Linux wird die C-Runtime grundsätzlich dynamisch eingebunden.
Linkt man sie jedoch statisch, hat man bessere Chancen, dass ein Programm,
das in Distribution A
erstellt wurde auch in Distribution B
mit einer
anderen Standard-C-Runtime funktioniert.
Ehrlicherweise muss man aber auch sagen, dass das nicht immer empfohlen ist
und zwischen unterschiedlichen Distributionen auch Probleme mit statischen
C-Runtimes auftreten können (z.B. mit C++ std
Exceptions).
Die Optionen -static-libgcc
und -static-libstdc++
schalten die statische
Einbindung der C-Runtime und der C++-Runtime ein.
In CMake gibt es zwei Möglichkeiten, wie man festlegt, dass eine Bibliothek statisch oder dynamisch (shared) werden kann.
CMakeLists.txt
in add_library
STATIC
nach dem Bibliotheksnamen wird sichergestellt,
dass der Code als statische Bibliothek gebaut wird:
BUILD_SHARED_LIBS
add_library
weder STATIC
noch SHARED
explizit angibt, entscheidet die globale Variable BUILD_SHARED_LIBS
,
wie die Bibliothek gebaut werden soll.OFF
oder FALSE
, entsteht eine
statische Bibliothek, andernfalls kommt eine dynamische (shared) Library
am Ende heraus.
1 cmake -DBUILD_SHARED_LIBS=OFF .
Ausführbaren Programmen wird in CMake
per
target_link_libraries
mitgeteilt, welche Bibliotheken zu linken sind:
Um ein ausführbares Programm zu bauen, sucht CMake
die Bibliotheken
automatisch. Es kann jedoch hilfreich sein mit
set_target_properties
zu deklarieren, ob nach statischen oder dynamischen Libs zuerst gesucht wird:
Auch in CMake
kann es Sinn machen, die C-Runtime statisch einbinden zu lassen.
Dafür nutzen wir je nach Compiler-Typ die notwendigen Compiler-Switches.
1 if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") 2 # MSVC 3 set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} /MT") 4 set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} /MTd") 5 set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MT") 6 set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MTd") 7 elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") 8 # GCC 9 set(CMAKE_FIND_LIBRARY_SUFFIXES ".a") 10 set(CMAKE_EXE_LINKER_FLAGS "-static-libgcc -static-libstdc++") 11 endif()
In CONAN Umgebungen wird zwischen mit der “Option”
shared
festgelegt, wie eine Bibliothek gebaut oder gelinkt werden soll.
Da die benutzte Options-Variante den Hash eines CONAN
Pakets ändert,
muss sowohl beim Bauen der Lib die korrekt Option gesetzt sein, als auch
bei der Einbindung als requirements
.
In conanfile.py
kann der default-Wert der shared
Option auf False
gesetzt werden um statische Libs per default zu erzeugen. Der
conan build
Schritt muss dann diese Information an das darunterliegende
Build-System weiterreichen und auch die Paketierung wird am Ende je nach
Bibliothekstyp etwas anders aussehen:
1 import os 2 from conans import tools, ConanFile 3 4 class my_shared_lib(ConanFile): 5 name = "my_shared_lib" 6 version = "1.0" 7 options = { "shared": [True, False] } 8 default_options = { "shared": False } 9 10 def build(self): 11 cmake = CMake(self) 12 if self.options.shared: 13 # dynamic 14 cmake.definitions["BUILD_SHARED_LIBS"] = "ON" 15 else: 16 # static 17 cmake.definitions["BUILD_SHARED_LIBS"] = "OFF" 18 cmake.build() 19 20 def package(self): 21 if self.options.shared: 22 # dynamic 23 self.copy(pattern="*.dll", dst="bin", keep_path=False) 24 self.copy(pattern="*.lib", dst="lib", keep_path=False) 25 self.copy(pattern="*.dylib", dst="lib", keep_path=False) 26 self.copy(pattern="*.so*", dst="lib", keep_path=False) 27 else: 28 # static 29 self.copy(pattern="*.lib", dst="lib", keep_path=False) 30 self.copy(pattern="*.a", dst="lib", keep_path=False)
CONAN
Optionen wie shared
lassen sich dann an der Kommandozeile auch
explizit setzen, wenn man von den default_options
abweichen will:
1 conan create path/to/conanfile.py -o my_shared_lib:shared=False
Alternativ kann man im aktuellen konfigurierten CONAN
Profil auch eine
Option setzen, die benutzt wird, falls sie durch keine andere Methode explizit
gesetzt wird:
Bei den requirements
des konsumierenden Projektes muss die gleiche
shared
Option angefordert werden, mit der die Bibliothek gebaut wurde.
Andernfalls sucht CONAN nach dem falschen Paket-Hash in seinen Caches.
Obwohl ich ein großer Fan von statischen Bibliotheken bin, habe ich auch einige Situationen kennenlernen dürfen, in denen es zu Problemen kommen kann.
Grundsätzlich sollte man sich an folgende Regel halten:
Entweder, man nutzt nur statische Bibliotheken, oder man nutzt nur dynamische Bibliotheken.
Denn es sind in erster Linie die Mischformen, bei denen es zu unerwarteten
Problemen kommt.
Meiner Erfahrung nach, führt Microsoft’s MSVC ein paar Schritte aus, die
Probleme umgehen sollen. Im GCC finden diese vermutlich nicht statt und daher
kann ich über folgende Probleme berichten:
GCC Symbol-Zusammenführung
Wenn ein globales Symbol in mehreren Modulen vorkommt, legt der GCC die
Adressen in ein Symbol zusammen. Codes wie jene von Konstruktoren und
Destruktoren werden aber nicht zusammengelegt, sondern mehrfach ausgeführt.
Daraus ergibt sich das Problem:
A
nutzt statische Lib und “erbt” globale VariableB
nutzt statische Lib und “erbt” ebenfalls eine globale
VariableDieses Beispiel ist dokumentiert im Artikel:
double free wegen const std::string
GCC Code-Überschreibung von öffentlichen Funktionen
Wenn mehrere geladene dynamische Bibliotheken Funktionen mit gleichen Namen
exportieren, gewinnt eine der Funktionen und überschreibt die andere.
Dynamische Bibliotheken, die Funktionen einer gemeinsamen statischen
Bibliothek exportieren, müssen alle mit dem gleichen Stand der statischen
Bibliothek kompiliert worden sein. Andernfalls rufen Objekte der dynamischen
Bibliothek Funktionen einer anderen Version der statischen Bibliothek auf,
als die, mit der sie selbst gebaut wurden.
A
nutzt statische Lib und “erbt” und exportiert die FunktionB
nutzt statische Lib und “erbt” und exportiert ebenfalls
die Funktion.B
wird neu kompiliert mit neuer Implementierung in
statischer Lib.A
und B
.B
verhält sich wie erwartet.A
sollte das alte Verhalten umsetzen, weil A
nicht geändert wurde.A
erhält aber Teile der Features der geänderten statischen Lib.Diese Problem ist in GCC Problem: Statische Shared-Libs dokumentiert.
Link Time Optimization
Programme, die mit statischen Libs gebaut wurden haben in GCC Umgebungen
einen wesentlich größeren Speicherbedarf, als z.B. beim MSVC, dessen
Projekte per Standard die “Whole-Program-Optimization” nutzen.
Das GCC Äquivalent heißt “Link-Time-Optimization” (LTO)
und wird im Artikel GCC Binaries sind viel zu groß
beschrieben.
Leider gibt es in manchen Umgebungen Probleme mit der LTO, weshalb sie je nach Situation nicht fehlerfrei anwendbar ist.
Ein Beispiel befindet sich in: GCC Link Time Optimization
opengate.at
.