Dynamisch gelinkte Binaries.
Jedes der drei Programme gibt seinen Namen aus, welcher der C-Funktion main()
als erstes Element des Arrays argv
übergeben wird.
Dabei werden drei verschiedene Funktionen zur Ausgabe der Zeichenkette genutzt:
printf()
-
Die Funktion
printf
bietet umfangreiche Möglichkeiten zur formatierten Ausgabe von Zeichenketten und Zahlen. Da die gewünschten Formate bei der Übersetzung und beim Linken nicht bekannt sind (das Format könnte z. B. aus einer Datei gelesen werden), müssen alle Formatierfunktionen eingebunden werden. Die Nutzung vonprintf
wird das erzeugte Programm aufblähen. putf()
-
Die Funktion
puts
nutzt wieprintf
das<stdio.h>
-Ausgabesystem, braucht aber keine Formatierung. Das erzeugte Programm sollte also deutlich kleiner sein. write()
-
Die Funktion
write
ist nur der Wrapper für einen Betriebssystem-Aufruf. Das erzeugte Programm sollte also sehr klein sein.Bei diesem Programm nutzen wir eine eigene
-Funktion, um eine Verfälschung durch den Import vonstrlength
<string.h>
zu vermeiden.
Die Programme:
Übersetzt wird jeweils mit den Parametern:
gcc -Werror -Wall -Wextra -pedantic -O2 -march=x86-64 -fno-stack-protector -o {prog} {prog.c} strip {prog}
So kompiliert erhalten wir diese Dateigrößen.
Größe | Binary |
---|---|
6120 | hello-printf-dynamic |
6120 | hello-puts-dynamic |
6120 | hello-write-dynamic |
Die Programme sind gleich groß, oder besser klein. Das war zu erwarten, weil die Implementierung der Funktionen erst zur Laufzeit als dynamische Laufzeitbibliothek nachgeladen wird.
Statisch gelinkte Binaries.
Als nächstes linken wir statisch, indem wir dem Compiler-Aufruf ein -static
beifügen. Mit diesem zuerst überraschenden Ergebnis:
Größe | Binary |
---|---|
774760 | hello-printf-static |
774760 | hello-puts-static |
774760 | hello-write-static |
Obwohl wir nur kleine und dazu unterschiedliche Teile der Clib nutze, sind die erzeugten Binaries sehr und dazu gleich groß.
Was geht hier vor?
Statisch gelinkte Binaries ohne Aufruf einer Funktion der Clib
.
Möglicherweise hat der Aufruf von write
irgendwie Komponenten der Clib
nachgezogen. Um das auszuschließen, implementieren wir den Betriebssystem-Aufruf selbst und
tilgen so alle Referenzen auf die Clib:
Achtung: Die folgen beiden Code-Schnipsel passen nur zu einem Linux mit x64-Prozessor.
Wir compilieren:
gcc -Werror -Wall -Wextra -pedantic -O2 -march=x86-64 -fno-stack-protector -o hello-syscall-static -static hello-syscall.c syscall.c strip hello-syscall-static
Und werden enttäuscht, es hat sich nichts getan:
Größe | Binary |
---|---|
774760 | hello-syscall-static |
Verzicht auf crt0.o
.
Nun ruft das Betriebssystem nicht main
auf, sondern den Programm-Einsprungpunkt
_start
. Und der wird von der Startdatei crt0.o
bereitgestellt, die der Linker automatisch unserem Programm hinzufügt.
Wollen wir dies nicht, können wir das dem Linker über die Option -nostartfiles
mitteilen:
gcc -Werror -Wall -Wextra -pedantic -O2 -march=x86-64 -fno-stack-protector \ -o hello-syscall-static-tinystartfile \ -static hello-syscall.c syscall.c starter-callexit.c -nostartfiles
Dann müssen wir aber den Einsprungpunkt _start
selber bereitstellen.
Achtung: Dieser Code-Schnipsel passt nur zu einem Linux mit x64-Prozessor.
Diesmal ist das Ergebnis positiv: Das so erzeugte Binary ist massiv geschrumpft:
Größe | Binary |
---|---|
1352 | hello-syscall-static-tinystartfile |
Offensichtlich ruft das crt0.o
eine Menge von Funktionen der Clib
auf, die wir für unser einfaches Programm überhaupt nicht brauchen. ☹
Wenn wir in unserer Start-Datei den Aufruf von _exit
durch einen direkten Aufruf des Betriebssystems ersetzen, sparen wir noch ein paar Byte:
Größe | Binary |
---|---|
1088 | hello-syscall-static-tinystartfile-nouseexit |
Wenn wir in den letzten beiden Versuchen zur Ausgabe write
statt syscall
aufrufen, wird unser Binary wieder deutlich größer:
Größe | Binary |
---|---|
1352 | hello-syscall-static-tinystartfile |
1088 | hello-syscall-static-tinystartfile-nouseexit |
1920 | hello-syscall-static-tinystartfile |
1784 | hello-syscall-static-tinystartfile-nouseexit |
Es ist erschreckend, wie viel Code ein einfacher Wrapper erzeugt.
Résumé.
Statisch gelinkte Binaries sind größer, als sie technisch sein müssten, weil Komponenten eingebunden werden, die nicht benutzt werden. Dies ist unnötig, denn der Linker hat, wie statische Initialisierer zeigen, durchaus die Fähigkeit, auch indirekt genutzte Komponenten nur bei Bedarf einbinden.
Offensichtlich gehen die Designer der (GNU-)Clib davon aus, dass:
- große Teile der Clib immer gebraucht werden und/oder
- Programmgröße keine Rolle spielt und/oder
- eh dynamisch gelinkt wird.
Schade.
Sie können ein Paket mit allen verwendeten Dateien als clib-horror.zip herunterladen.