Bisher habe ich Programme immer in MASM32 geschrieben, wenn es darum ging, möglichst effiziente und platzsparende Executables zu erzeugen. Mann kann aber auch prima mit nativem C oder C++ die Dateigröße drastisch reduzieren.
Wir wollen nun folgendes Beispielprogramm minimieren:

// Simple demo that prints the command line.
#include <iostream>
using namespace std;
int main( int argc, const char* argv[] ) {
	cout << argv[0] << endl;
	return 0;
}

Zunächst sollte man sich einmal fragen, was denn für Runtimes verwendet werden dürfen:

  1. VC200x Runtimes verfügbar:
    In diesem Fall sind wir fein raus, wir können einfach dynamisch gegen die CRT linken (Kompilerschalter /MD) und uns einer kleinen EXE/DLL etc. erfreuen. Im Beispielfall sind dies nur 8704 Bytes. Das ist schon recht ordentlich. Allerdings benötigen wir zum Starten die MSVCP90.dll und MSVCR90.dll.
  2. Keine aktuellen Runtimes von Visual C++:
    Jetzt müssen wir alle Bibliotheken statisch linken (Kompilerschalter /MT). Das sieht schlecht aus, denn so vergrößert sich unsere Dateigröße schnell um etliche Kilobytes (auf 104.448 Bytes!). Man kann folgendes tun, um die Dateigröße trotzdem klein zu halten:

    • Auf iostream verzichten
      Verwendet man stattdessen das stdio.h und printf kann man die Dateigröße halbieren: 52.736 Bytes mit folgendem Code:

      #include <stdio.h>
      int main( int argc, const char* argv[] ) {
      	printf( "%sn", argv[0] );
      	return 0;
      }
    • Auf C++/C API verzichten und stattdessen native Windows API verwenden
      Schon alleine durch das Benutzen einer Klasse auf dem Stack muss die komplette Behandlung für new und delete mitgelinkt werden werden – und das ist ziemlich viel, wenn es auf ein paar Bytes ankommt. Auch malloc und free linken schon einen Teil der Runtime hinzu. Stattdessen kann mann ja die Win32-API-Funktionen HeapAlloc und HeapFree verwenden. Leider ist es mir mit neueren Versionen des Studios nicht gelungen, gegen die alte Version der Runtime (msvcrt.dll), die ja schon seit Windows XX dabei ist, zu linken. Für C++ würde dies auch nicht viel Sinn machen, da der Kompiler hier doch einiges gelernt hat und sich der Standard ja auch weiterentwickelt hat.
    • Auf Runtime komplett verzichten
      Dies ist ein großer Schritt, da von der Runtime viele Aufgaben übernommen werden, die einem erstmal gar nicht auffallen, wie z.B. das Parsen der Kommandozeilenparameter. Oder die Textausgabe auf stdout umleiten. Dies muss dann alles manuell gemacht werden. Dazu muss man zunächst einen eigenen Programmeinstiegspunkt definieren (Linkerschalter /ENTRY:"myMain"), da sonst automatisch die Runtime dazwischengeschaltet wird (ist beispielsweise nötig um globale Variablen dynamisch zu initialisieren). Verzichtet man dann noch auf die übrigen Runtime-Funktionen und setzt stattdessen auf Windows-API, kommt man mit folgendem Code auf gerade einmal 3.072 Byte!

      #include "Windows.h"
      #pragma comment( lib, "kernel32" )
      void myMain() {
      	HANDLE std_out = ::GetStdHandle( STD_OUTPUT_HANDLE );	
      	char* lpCommandLine = ::GetCommandLine();
      	DWORD bytes_written = 0;
      	::WriteFile( std_out, lpCommandLine, lstrlen( lpCommandLine ), 
      		&bytes_written, NULL );
      	::ExitProcess( 0 );
      }

      Will man natürlich auf andere Parameter der Kommandozeile zugreifen, so muss diese erst mühsam geparst werden :-(.

Weitere Schritte um die Dateigröße zu minimieren

Im Folgenden zeige ich noch ein paar weitere Tricks, wie man die Größe der Datei schrumpfen lassen kann.

  • Unnötigen Ballast abwerfen
    • Manifest deaktivieren: von 3.072 auf 2.560 Bytes
    • Keine Exceptions verwenden
    • Kein RTTI verwenden (Kompilerschalter /GR-)
    • Keine Exceptions verwenden
    • Für Größe optimieren und kleineren Code bevorzugen (Kompilerschalter /O1 und /Os)
    • no buffer security check (Kompilerschalter /GS-)
  • PE-Sektionen vereinen und damit Alignment-Overhead reduzieren (Linkerschalter /MERGE:.src=.dest)
    Hier ist extreme Vorsicht geboten, sonst funktioniert das Programm nicht mehr! Das Zusammenführen von .rdata and .text kann beispielsweise ganz gut funktionieren, weil beide Bereiche Daten enthalten, auf die nur lesend zugegriffen wird.
  • Datei-Alignment reduzieren
    Im Normalfall gibt es eine Alignment von 512 Bytes für alle Bereiche einer PE-Datei. Dieses lässt sich jedoch reduzieren (Linkerschalter /ALIGN:XXX). Auch hier ist extreme Vorsicht geboten, sonst funktioniert das Programm nicht mehr!
  • Eigenen DOS-Stub verwenden
    Windows Programme bestehen aus einem DOS-Header und einem PE-Header. Ersterer dient dazu, das ursprüngliche Dateiformat einzuhalten und auch gleichzeitig Kompatibilität zu wahren. In unserem Beispiel ist der komplette DOS-Header 216 Byte groß. Mit einem minimaleren Stub (nur 64 Byte) lässt er sich auf 152 Byte quetschen.

Mit den beschriebenen Methoden lässt sich unser Beispielprogramm auf gerade ein mal 1.688 Bytes reduzieren. Nicht schlecht oder? Das wird selbst mit MASM32 nicht ganz einfach.

Links zum Thema