Naturgemäß nutzen C-Programme zumeist Funktionen der C-Laufzeitbibliothek zur Ein- und Ausgabe. Eine einfache Kompilierung und Bindung auf der Kommandozeile unter Visual C 2008 mit Bindung von Kernel- und Winsock-Funktionen kann dann wie folgt aussehen:
cl test.c /link kernel32.lib ws2_32.lib
Der Schalter /MT, der unseren Objektkode mit der statischen C-Laufzeitbibliothek libcmt.lib verbindet, ist dabei die Vorgabe.
Der Schalter /MD würde statt dessen die Bibliothek msvcrt.lib mit einbinden, die als dünne Zwischenschicht lediglich die Aufrufe der betreffenden Funktionen an die dynamische C-Laufzeitbibliothek msvcrt.dll durchreicht, was den Kode um mindestens 40 KB reduziert. In diesem Falle muß bei Ausführung des Programmes jedoch ein Manifest (als gleichnamige Manifestdatei oder als gebundene Resource) bestehen, welches die korrekte Version der dynamisch geladenen C-Laufzeitbibliothek angibt.
Für einen Server in C mit multiplen Threads wollten wir nun jedoch die Windows-Funktion CreateThread()
nutzen, bei der nach Möglichkeit die CRT-Funktionen durch Windows-Pendants ersetzt werden sollten.
Anmerkung: Eine Alternative zu CreateThread()
ist _beginthread()
, welches die Laufzeitbibliothek für den Thread initialisiert, so daß Laufzeitfunktionen in Threads eingesetzt werden können.
Zur Umschreibung nutzten wir also die Windows-Funktion WriteConsole()
statt der Bibliotheksfunktion printf()
etc.
Für die weiterentwickelte Server-Variante, die mit select()
statt mit Threads arbeiten sollte, hatten wir dies so belassen und dachten, außer dem globalen Initialisierungsteil der Laufzeitbibliothek für das Programm keine C-RTL-Funktionen in unserem Code mehr aufzurufen. Warum also noch den Initialisierungsteil behalten und den Kode unnötig aufblähen? Also versuchten wir, uns der C-Laufzeitbibliothek mit dem Schalter /nodefaultlib ganz zu entledigen. Aber dann spuckte der Binder fünf fehlende Symbole aus:
test.obj : error LNK2019: Verweis auf nicht aufgelöstes externes Symbol "__vsnprintf" in Funktion "_StringVPrintfWorkerA@20". test.obj : error LNK2019: Verweis auf nicht aufgelöstes externes Symbol "___security_cookie" in Funktion "_myprintf". test.obj : error LNK2019: Verweis auf nicht aufgelöstes externes Symbol "@__security_check_cookie@4" in Funktion "_myprintf". test.obj : error LNK2019: Verweis auf nicht aufgelöstes externes Symbol "_memset" in Funktion "_main". LINK : error LNK2001: Nicht aufgelöstes externes Symbol "_mainCRTStartup". test.exe : fatal error LNK1120: 5 nicht aufgelöste externe Verweise.
Der letzte Fehler war ein klarer Fehler von uns: der ursprüngliche Einsprung zur Bibliotheksinitialisierung mainCRTStartup()
existierte ja nun nicht mehr, also benötigte der Binder einen neuen Einsprungpunkt. Wir ergänzten also die Kommandozeile um den Schalter /entry:main, was den letzten Fehler beseitigte.
Die CRT-Funktion _vsnprintf()
stellte sich als aufgerufene Funktion vom Makro StringCchVPrintf()
heraus; wir ersetzten es durch wvsprintf()
aus der Bibliothek user32.lib und der erste Fehler verschwand auch. Bei der Gelegenheit ersetzten wir auch gleich alle (wenig portablen) in strsafe.h deklarierten Funktionen, so StringCchCopy()
durch lstrcpy()
aus kernel32.lib und StringCchPrintf()
durch wsprintf()
aus user32.lib.
memset()
entpuppte sich ebf. als intern durch das Makro ZeroMemory()
aufgerufene CRT-Funktion. Also ersetzten wir den Code
ZeroMemory(&defAddr, sizeof(defAddr))
durch eine statische Initialisierung der Variablen mit
struct addrinfo defAddr = { 0 }
,
was den vierten Fehler behob.
Verblieben die zwei unaufgelösten, ominösen Coookie-Fehler, die sich als Verweise auf Microsoft-interne Symbole herausstellten: Dahinter verbergen sich Routinen zur Überprüfung von Pufferüberlaufen (buffer overrun detection) bei den Funktionen, die der MS-Compiler als gefährdet einstuft:
In Visual Studio 2002 a new compiler switch /GS was introduced to the Visual C++ compiler. When this switch is set, the compiler injects buffer overrun detection code in the compiled code. On functions that the compiler thinks might be subject to buffer overruns, the compiler inserts a security cookie before the return address. The security cookie is computed once at module load and its value is pushed to the stack on function entry. Then, on function exit, the compiler helper is called to make sure the cookie's value is still the same. If the value was changed, this is treated as a sign of a buffer overrun in the stack corruption. In this way it is possible to detect some direct buffer overruns into the return address.
Was tun? Im ersten Schritt hatten wir runtmchk.lib (früher: bufferoverflowU.lib) dazugebunden, dann aber erkannt, daß trotz erfolgreicher Kompilierung eine eigene Puffertestung zu schreiben wäre (s. Dokumentation zur Bibliothek).
Also haben wir statt dessen den Schalter /GS- gesetzt, der den Vorgabewert /GS, also die standardmäßige Überprüfung, abschaltete:
cl /GS- test.c /link /nodefaultlib /entry:main kernel32.lib ws2_32.lib user32.lib
Zusammen mit dem Schalter zur Optimierung auf Speichergröße /O1 verblieben von 56 KB Kompilat der ersten Fassung somit gerade noch einmal 8 KB in der letzten – ein Siebtel der ursprünglichen Kompilatgröße!
Viel Spaß beim Ausprobieren zum Minimieren!