Statisches Linken
- Einleitung
- Statisches vs. dynamisches Linken
- Vorteile und Nachteile
- Manuelles statisches Linken in C und C++
- Windows mit Visual C++ MSVC
- Statische Bibliothek erstellen und in EXE linken
- C Runtime statisch linken
- Linux mit GCC
- Statische Bibliothek erstellen und in ein Executable linken
- C Runtime statisch linken
- Statisches Linken in CMAKE
- Ausführbares Programm in CMake gegen statischen Libs linken lassen
- C-Runtime in CMake statisch linken lassen
- Statisches Linken in CONAN
- Statische Bibliothek mit CONAN erstellen
- Statische Bibliothek mit CONAN in EXE linken
- Bekannte Problemfälle
- Weiterführende Links
Einleitung
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.
Statisches vs. dynamisches Linken
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
graph TD
subgraph "files.dll / libfiles.so"
dyn_open[[open]]
dyn_read[[read]]
dyn_write[[write]]
dyn_close[[close]]
end
subgraph "Program executable importing shared libraries at runtime"
main(main)
end
subgraph "math.dll / libmath.so"
dyn_sin[[sin]]
dyn_cos[[cos]]
dyn_tan[[tan]]
end
main --import--- dyn_sin
main --import--- dyn_cos
main --import--- dyn_tan
main --import--- dyn_open
main --import--- dyn_read
main --import--- dyn_write
main --import--- dyn_close
main --API
call--- dyn_sin
main --API
call--- dyn_cos
main --API
call--- dyn_tan
main --API
call--- dyn_open
main --API
call--- dyn_read
main --API
call--- dyn_write
main --API
call--- dyn_close
Beispiel: 1 Programm + 2 statische Libs = 1 finale Binärdatei
graph TD
subgraph "Program executable including static libraries at buildtime"
subgraph "static 'math' lib"
static_sin[[sin]]
static_cos[[cos]]
static_tan[[tan]]
end
subgraph "static 'files' lib"
static_open[[open]]
static_read[[read]]
static_write[[write]]
static_close[[close]]
end
static_main(main)
static_sin --local
call--- static_main
static_cos --local
call--- static_main
static_tan --local
call--- static_main
static_open --local
call--- static_main
static_read --local
call--- static_main
static_write --local
call--- static_main
static_close --local
call--- static_main
end
Vorteile und Nachteile
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:
1 // get_setting.c
2 int get_setting()
3 {
4 return 42;
5 }
6
7 // main_prog.c
8 int main()
9 {
10 printf("%d", get_setting());
11 return 0;
12 }
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.
Manuelles statisches Linken in C und C++
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.
Windows mit Visual C++ MSVC
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
Statische Bibliothek erstellen und in EXE linken
1 cl /c my_static_lib_file_1.c
2 cl /c my_static_lib_file_2.c
3 lib /out:my_static_lib.lib my_static_lib_file_1.obj my_static_lib_file_2.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:
1 cl /c myprog_file.c
2 link /out:myprog.exe myprog_file.obj my_static_lib.lib
C Runtime statisch linken
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.
1 cl /c /MT my_static_lib_file_1.c
2 cl /c /MT myprog_file.c
Linux mit GCC
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.
Statische Bibliothek erstellen und in ein Executable linken
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.
1 gcc -c my_static_lib_file_1.c
2 gcc -c my_static_lib_file_2.c
3 ar -r libmy_static_lib.a my_static_lib_file_1.o my_static_lib_file_2.o
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
.
1 gcc -c myprog_file.c
2 gcc myprog_file.o -lmy_static_lib -o myprog_file
C Runtime statisch linken
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.
1 gcc myprog_file.o -static-libgcc -lmy_static_lib -o myprog_file
2 g++ myprog_file.o -static-libgcc -static-libstdc++ -lmy_static_lib -o myprog_file
Statisches Linken in CMake
In CMake gibt es zwei Möglichkeiten,
wie man festlegt, dass eine Bibliothek statisch oder dynamisch (shared)
werden kann.
- Individuell in
CMakeLists.txt
in add_library
Mit dem Token STATIC
nach dem Bibliotheksnamen wird sichergestellt,
dass der Code als statische Bibliothek gebaut wird:
1 add_library(my_static_lib STATIC
2 my_static_lib_file_1.c
3 my_static_lib_file_2.c
4 )
- Global über die CMake-Variable
BUILD_SHARED_LIBS
Wenn man in add_library
weder STATIC
noch SHARED
explizit angibt, entscheidet die globale Variable BUILD_SHARED_LIBS
,
wie die Bibliothek gebaut werden soll.
Ist sie nicht gesetzt oder OFF
oder FALSE
, entsteht eine
statische Bibliothek, andernfalls kommt eine dynamische (shared) Library
am Ende heraus.
1 add_library(my_static_lib
2 my_static_lib_file_1.c
3 my_static_lib_file_2.c
4 )
1 cmake -DBUILD_SHARED_LIBS=OFF .
Ausführbares Programm in CMake gegen statische Libs linken lassen
Ausführbaren Programmen wird in CMake
per
target_link_libraries
mitgeteilt, welche Bibliotheken zu linken sind:
1 add_executable(myprog
2 myprog_file.c
3 )
4
5 target_link_libraries(myprog
6 my_static_lib
7 other_lib
8 ...
9 )
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:
1 set_target_properties(myprog PROPERTIES
2 LINK_SEARCH_START_STATIC 1
3 )
4 set_target_properties(myprog PROPERTIES
5 LINK_SEARCH_END_STATIC 1
6 )
C-Runtime in CMake statisch linken lassen
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()
Statisches Linken in CONAN
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
.
Statische Bibliothek mit CONAN erstellen
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:
1 #~/.conan/profiles/default
2 ...
3 [options]
4 *:shared=False
Statische Bibliothek mit CONAN in EXE linken
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.
1 import os
2 from conans import tools, ConanFile
3
4 class myprog(ConanFile):
5 name = "myprog"
6 version = "1.0"
7
8 def requirements(self):
9 self.requires("my_shared_lib/1.0@user/channel")
10 self.options["my_shared_lib"].shared = False
Bekannte Problemfälle
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:
- Statische Lib definiert eine globale Variable
- Dynamische Lib
A
nutzt statische Lib und “erbt” globale Variable
- Dynamische Lib
B
nutzt statische Lib und “erbt” ebenfalls eine globale
Variable
- Programm lädt beide Libs.
- Wir erhalten nur eine Adresse für die globale Variable aus beiden Modulen
- Wir erhalten aber zwei Konstruktor und zwei Destruktor-Aufrufe,
also je ein Aufruf pro Lib.
- Bug: Es kommt zur Korruptionen beim zweiten Destruktor-Aufruf.
Dieses 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.
- Statische Lib stellte eine Funktion bereit.
- Dynamische Lib
A
nutzt statische Lib und “erbt” und exportiert die Funktion
- Dynamische Lib
B
nutzt statische Lib und “erbt” und exportiert ebenfalls
die Funktion.
- Statische Lib wird aktualisiert und erhält neue Features.
- Nur dynamische Lib
B
wird neu kompiliert mit neuer Implementierung in
statischer Lib.
- Programm lädt beide Libs
A
und B
.
B
verhält sich wie erwartet.
A
sollte das alte Verhalten umsetzen, weil A
nicht geändert wurde.
- Bug:
A
erhält aber Teile der Features der geänderten statischen Lib.
Was dann passiert kann unvorhersehbar sein.
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
Weiterführende Links