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:
#include <stdio.h> int main( int argc, const char** argv){ (void) argc; printf( "%s\n", argv[0] ); return 0; }
printf
#include <stdio.h> int main( int argc, const char** argv){ (void) argc; puts( argv[0] ); return 0; }
puts
#include <unistd.h> #define OUT_FILEHANDLE (1l) static size_t stringlen( const char * s ){ size_t len = 0; while( *s ) { ++len; ++s; } return len; } int main( int argc, const char** argv){ (void) argc; const size_t argv_0_len = stringlen( argv[0] ); int result; result = write( OUT_FILEHANDLE, argv[0], argv_0_len ); result = write( OUT_FILEHANDLE, "\n" , 1 ); (void) result; return 0; }
write
Ü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:
#include <string.h> #include "syscall.h" #define OUT_FILEHANDLE (1l) static size_t stringlen( const char * s ){ size_t len = 0; while( *s ) { ++len; ++s; } return len; } int main( int argc, const char** argv){ (void) argc; const size_t argv_0_len = stringlen( argv[0] ); (void) do_syscall( SYS_WRITE, OUT_FILEHANDLE, argv[0], argv_0_len ); (void) do_syscall( SYS_WRITE, OUT_FILEHANDLE, "\n", 1 ); return 0; }
hello-syscall.c
Achtung: Die folgen beiden Code-Schnipsel passen nur zu einem Linux mit x64-Prozessor.
#define SYS_WRITE ( 1l) #define SYS_EXIT (60l) extern long do_syscall( long number, ... );
syscall.h
#include "syscall.h" __attribute__ (( noinline )) long do_syscall( long number, ... ){ (void) number; __asm__( "mov %rdi,%rax \n" // Syscall# "mov %rsi,%rdi \n" // 1st Arg "mov %rdx,%rsi \n" // 2nd Arg "mov %rcx,%rdx \n" // 3rd Arg "mov %r8,%r10 \n" // 4th Arg "mov %r9,%r8 \n" // 5th Arg "mov 0x8(%rsp),%r9 \n" // 6th Arg "syscall \n" "retq \n" ); // not reached return -1; }
syscall.c
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.
#include <unistd.h> // _start is the entry point known to the linker __attribute__((force_align_arg_pointer)) void _start( void ){ //------------------------------------------------------------ // on entry: //------------------------------------------------------------ // [sp] n # -> argc //------------------------------------------------------------ // [sp + 8] &arg_0 @ -> argv // &arg_1 // [..] // &arg_n-1 // [sp + 8 + n*8] null //------------------------------------------------------------ // [sp +16 + n*8] &env_0 @ -> envp // &env_1 // [..] // &env_m-1 // null //------------------------------------------------------------ __asm__( "xor %ebp, %ebp \n" // mark end of stack frames "mov (%rsp), %edi \n" // register edi = 1st arg of main (argc) <- n "lea 8(%rsp), %rsi \n" // register rsi = 2nd arg of main (argv) <- address of arg_0 "lea 16(%rsp,%rdi,8), %rdx \n" // register rdx = 3rd arg of main (envp) <- address of env_0 "xor %eax, %eax \n" // register eax <- 0 (ABI) "call main \n" // main( %edi, %rsi, %rdx ) "mov %eax, %edi \n" // register edi = 1st arg of _exit <- return value of main() "xor %eax, %eax \n" // register eax = 0 (ABI) "call _exit \n" // _exit() terminates program // not reached "hlt \n" // illegal instruction in user mode ); }
starter-callexit.c
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:
#include "syscall.h" // _start is the entry point known to the linker __attribute__((force_align_arg_pointer)) void _start( void ){ //------------------------------------------------------------ // on entry: //------------------------------------------------------------ // [sp] n # -> argc //------------------------------------------------------------ // [sp + 8] &arg_0 @ -> argv // &arg_1 // [..] // &arg_n-1 // [sp + 8 + n*8] null //------------------------------------------------------------ // [sp +16 + n*8] &env_0 @ -> envp // &env_1 // [..] // &env_m-1 // null //------------------------------------------------------------ __asm__( "xor %ebp, %ebp \n" // mark end of stack frames "mov (%rsp), %edi \n" // register edi = 1st arg of main (argc) <- n "lea 8(%rsp), %rsi \n" // register rsi = 2nd arg of main (argv) <- address of arg_0 "lea 16(%rsp,%rdi,8), %rdx \n" // register rdx = 3rd arg of main (envp) <- address of env_0 "xor %eax, %eax \n" // register eax <- 0 (ABI) "call main \n" // main( %edi, %rsi, %rdx ) "mov %eax, %edi \n" // register edi = 1st arg of syscall <- return value of main() "movl $60, %eax \n" // register eax = syscall-number <- EXIT(60) "syscall \n" ); }
starter-syscall.c
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.