Statisches Linken


  1. Einleitung
  2. Statisches vs. dynamisches Linken
  3. Vorteile und Nachteile
  4. Manuelles statisches Linken in C und C++
    1. Windows mit Visual C++ MSVC
      1. Statische Bibliothek erstellen und in EXE linken
      2. C Runtime statisch linken
    2. Linux mit GCC
      1. Statische Bibliothek erstellen und in ein Executable linken
      2. C Runtime statisch linken
  5. Statisches Linken in CMAKE
    1. Ausführbares Programm in CMake gegen statischen Libs linken lassen
    2. C-Runtime in CMake statisch linken lassen
  6. Statisches Linken in CONAN
    1. Statische Bibliothek mit CONAN erstellen
    2. Statische Bibliothek mit CONAN in EXE linken
  7. Bekannte Problemfälle
  8. 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.
MSVC Project Config

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.

MSVC Project Runtime

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.

  1. 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 )
    
  2. 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:

  1. 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

  2. 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.

  3. 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