Optymalizacja plikow PE

W dzisiejszych czasach coraz czesciej mozna znalezc ogromne, wolno ladujace sie aplikacje. Prawie nikt nie zwraca juz uwagi na zla jakosc wygenerowanego pliku exe, sadzac, ze zrekompensuje ja wzrastajaca moc obliczeniowa komputerow i coraz wieksza pojemnosc dyskow twardych. W rezultacie uzytkownik musi niekiedy czekac kilka minut, zanim program bedzie gotowy do pracy. W artykule tym postaram sie przedstawic kilka sposobow, ktore pozwola na zredukowanie rozmiaru plikow exe, a niekiedy rowniez na skrocenie czasu ich ladowania. Tekst ten jest adresowany glownie do programistow piszacych w jezykach wysokiego poziomu, choc niektore porady beda przydatne rowniez dla osob uzywajacych asemblera. Czesc przedstawionych tu technik wykorzystuje narzedzia zawarte w pakiecie Visual C++, jednakze obecnie wiekszosc kompilatorow dostepnych na rynku rowniez oferuje podobne mozliwosci. Aplikacje te sa takze dolaczone do Platform SDK Microsoft'u. Artykul jest podzielony na dwie czesci. Pierwsza opisuje metody uniwersalne, ktore nie wymagaja ingerencji w kod zrodlowy. Techniki przedstawione w czesci drugiej maja pewne wymagania jesli chodzi o biblioteki uzywane w programie. Po tym wstepie przejdzmy do opisu poszczegolnych metod.

1. Techniki uniwersalne
=======================

1.1 Zmniejszanie rozmiaru pliku:
--------------------------------

1.1.1 Kompilacja w trybie Debug.
Ten punkt jest tak oczywisty, ze prawdopodobnie nie powinien sie tu wcale znalezc. :-) Pliki wykonywalne kompilowane w trybie Release sa niekiedy kilkukrotnie mniejsze od wersji zawierajacych informacje debug. Obecnie sytuacja jest nieco lepsza niz dawniej, poniewaz narzedzia obsluguja format NB10, ktory przechowuje wszystkie informacje w dodatkowym pliku PDB. Nie oznacza to jednak, ze mozna bagatelizowac ten problem. Faktem jest, ze kod generowany w wersji debug jest pozbawiony jakiejkolwiek optymalizacji. Oprocz tego w trybie tym standardowo wlaczany jest tak zwany 'incremental linking', ktory ma w zalozeniu przyspieszyc laczenie obiektow w gotowy exe. Aby to osiagnac rozdziela on funkcje w pliku exe dosc dlugimi ciagami instrukcji int 3. Dzieki temu, jesli funkcja zostanie zmieniona i nieznacznie zwiekszy swoj rozmiar, linker moze nadpisac czesc int 3 i nie musi relokowac kodu w pliku exe. W ten sposob modyfikowany jest jedynie maly fragment pliku zawierajacy dana funkcje. Jesli wylaczymy tryb debug i incremental linking, kosztem wydluzonego czasu kompilacji uzyskamy znacznie mniejszy plik exe. Za wylaczenie incremental linking w kompilatorze Visual C++ odpowiada parametr /INCREMENTAL:NO.

1.1.2 Wlaczenie optymalizacji kompilatora.
Kolejna istotna kwestia jest wlaczenie optymalizacji. Wiekszosc kompilatorow obsluguje optymalizacje pod katem rozmiaru generowanego kodu. Wiele osob jednak nie stosuje jej i wlacza optymalizacje predkosci kodu. Tymczasem roznice pomiedzy jakoscia kodu w obu przypadkach nie sa znaczne. Najczesciej dotycza one zastepowania pewnych instrukcji dluzszymi odpowiednikami (np. imul zastepowany jest przez lea), oraz rozwijaniem tzw intrinsic'ow (np. funkcja strlen() generuje kod oparty na repz scasb). Poza tym zoptymalizowany kod nie sprawdza nadpisania stosu watku przez bledny kod, nie inicjuje dla malych funkcji wskaznika ramki stosu (EBP) i nie generuje funkcji, ktore nie sa nigdzie wywolywane. Jesli programista w wielu miejscach w programie zadeklarowal identyczne stringi, sa one laczone w jeden. Podsumowujac, jesli zalezy Ci na kazdym bajcie, powinienes wlaczyc optymalizacje rozmiaru pliku. W Visual C++ odpowiada ze nia przelacznik /O1.

1.1.3 Zmniejszenie wyrownania sekcji.
Wewnetrznie struktura pliku PE w znacznej mierze oparta jest na sekcjach. Sekcje to inaczej obszary pamieci, zawierajace fragmenty programu majace takie same wlasciwosci, np. bedace kodem, stalymi, lub zmiennymi. W tym miejscu musze wprowadzic definicje wyrownania sekcji. Otoz jest to pewna stala wartosc, bedaca zawsze potega dwojki. Sekcje w pliku exe na dysku MUSZA zaczynac sie pod offsetem bedacym wielokrotnoscia tej stalej. To samo dotyczy sekcji po zaladowaniu do pamieci - kazda z nich jest mapowana pod adres bedacy wielokrotnoscia wyrownania. Standardowo w linkerze Microsoftu obie te wartosci wynosza 4096 (wielokrotnosc rozmiaru strony pamieci). Liczbe te dobrano tak, aby proces ladowania pliku wykonywalnego byl jak najszybszy. Niestety traci sie w ten sposob pewna ilosc miejsca wynikajaca z roznicy pomiedzy rzeczywistym rozmiarem danych w sekcji, a nastepna wielokrotnoscia stalej wyrownania. Przypuscmy na przyklad, ze pewna sekcja zawiera 1050 bajtow kodu. Standardowe wyrownanie w pliku to 4096 bajtow. Tak wiec rozmiar tej sekcji na dysku bedzie wynosil: 5045 + dopelnienie do nastepnej wielokrotnosci 4096 = 8192 bajtow. Plik ten zajmuje wiec 8192 - 5045 = 3147 bajtow zbednego miejsca. Statystycznie w ten sposob dla pliku o n sekcjach tracimy: 0,5 * 4096 * n bajtow miejsca. Nasuwa sie pytanie: czy istnieje sposob zmniejszenia wartosci wyrownania? Okazuje sie, ze linker Microsoftu obsluguje parametr /ALIGN. Gdy sprobujemy go jednak uzyc do obnizenia wyrownania ponizej wartosci 4096, nasz plik exe przestanie dzialac na niektorych wersjach systemu Windows 9x. Dlaczego tak sie dzieje? Otoz pewne wersje tego systemu maja najwidoczniej problem z zaladowaniem sekcji nie bedacych wielokrotnoscia rozmiaru strony pamieci. Problem ten nie wystepuje w zadnej wersji systemu NT z jaka mialem stycznosc. Co wiec mozemy w takim razie zrobic? Kluczem do rozwiazania problemu jest fakt, ze dla kazdej sekcji mozna okreslic oddzielne wyrownanie dla pliku, oraz dla pamieci. W ten sposob mozemy zmniejszyc nasze wyrownanie na dysku, nie powodujac blednego dzialania programu na niektorych systemach. Do tego celu mozemy posluzyc sie nieudokumentowanym parametrem linkera Microsoftu /OPT:NOWIN98 lub /FILEALIGN:x, gdzie x to nowa wartosc wyrownania. Najmniejsza liczba, dla ktorej plik udaje sie uruchomic na wszystkich wersjach systemu Windows to 512 bajtow (co byc moze jest zwiazane z rozmiarem sektora dysku). Jest to rowniez wartosc, na jaka ustawia wyrownanie parametr /OPT:NOWIN98.

1.1.4 Laczenie sekcji.
Zasadniczym celem istnienia sekcji w pliku PE (Portable Executable) jest rozdzielenie czesci programu majacych rozne wlasciwosci. Kazda sekcja posiada maske bitowa, w ktorej miedzy innymi okreslony jest dozwolony rodzaj dostepu (odczyt/zapis/wykonanie), sposob traktowania sekcji przez menedzer pamieci w windowsie itd. Technicznie nic nie stoi na przeszkodzie, aby zdefiniowac kilka sekcji z kodem. Niestety rozwiazanie to ma te wade, ze nie zyskujemy praktycznie nic, a poprzez wyrownanie sekcji niepotrzebnie powiekszamy plik exe. W wiekszosci przypadkow nowoczesne linkery dobrze radza sobie z tym zagadnieniem i automatycznie lacza sekcje majace takie same charakterystyki. Niestety, kilka razy spotkalem sie z przypadkiem, ze po uzyciu pewnych bibliotek wygenerowana binarka zawierala wiele sekcji z takimi samymi wlasciwosciami. Aby sprawdzic, czy plik wykonywalny ma optymalna ilosc sekcji najlatwiej posluzyc sie edytorem plikow PE (np. tym zawartym w ProcDumpie). Wszystkie sekcje w polu Characteristics powinny miec rozne wartosci. Jesli tak nie jest, 2 sekcje z jednakowymi wlasciwosciami nalezy polaczyc. Mozna tego dokonac przekazujac do linkera parametr /MERGE:zrodlo=cel. W rezultacie sekcja 'zrodlo' zostanie dodana do sekcji 'cel', co powinno zredukowac rozmiar pliku. Na przyklad, przypuscmy, ze pewien plik exe zawiera dwie sekcje: '.data' i '.data1'. Obie maja te same charakterystyki - 0xC0000040. Do linkera przekazujemy parametr /MERGE:.data1=.data i pozbywamy sie w ten sposob sekcji .data1. Z tym tematem wiaze sie jeszcze jedno zagadnienie. Co stanie sie, jesli polaczymy dwie sekcje, ktore nie posiadaja takich samych wlasciwosci. Generalnie sekcji takich nie powinno sie laczyc. Linker wyswietla wtedy ostrzezenie, a wygenerowana sekcja ma identyczne wlasciwosci, jak sekcja docelowa podana w parametrze MERGE. Nie powinno byc problemow z laczeniem sekcji .idata .edata i .rdata, poniewaz maja one podobne atrybuty. Nie zawsze jednak plik taki dziala prawidlowo. Co sie bowiem stanie, jesli dodajemy sekcje ze zmiennymi do sekcji z kodem? Otoz w pliku wynikowym bedziemy probowali dokonac operacji zapisu na zmiennych w sekcji, ktora ma we wlasciwosciach jedynie przywilej EXECUTE. O blad strony przy takim scenariuszu nietrudno. Rozwiazaniem jest modyfikacja w dowolnym edytorze pliku exe i zmiana charakterystyk odpowiedniej sekcji na takie, ktore sa suma bitowa (OR) wlasciwosci dwoch laczonych sekcji z oryginalnego pliku. Poza tym nie nalezy laczyc sekcji .rsrc, poniewaz czesc funkcji WinAPI do obslugi resource'ow przestanie dzialac. Zlym pomyslem jest rowniez laczenie sekcji majacych atrybut SHARED, poniewaz moze to spowodowac nieprzewidywalne skutki po zaladowaniu kilku kopii programu. Takze sekcje zawierajace dane .tls (ze zmiennymi oddzielnymi dla watkow) powinny pozostac niezmienione. Jak widac, operacja ta nalezy do ryzykownych i z pewnoscia wymaga testowania pliku wykonywalnego na kilku konfiguracjach. Uwazam, ze laczenia sekcji z roznymi atrybutami powinno sie dokonywac jedynie w ostatecznosci.

1.1.5 Usuwanie danych relokacji.
Podczas generowania kodu dla pliku exe lub dll linker wybiera pewien domyslny adres w pamieci, pod ktory bedzie ladowany dany modul. Jesli w chwili wczytywania aplikacji dany obszar w przestrzeni adresowej procesu jest wolny, Windows wczytuje plik pod sugerowany adres bez dodatkowych czynnosci. Niestety, w przypadku plikow dll czesto zdarza sie, ze w danym miejscu w pamieci juz znajduje sie jakas biblioteka. Wowczas Windows musi przeniesc ladowany kod pod inny adres w pamieci. Proces ten nazywany jest relokacja. Poniewaz jednak w kodzie czesto wykorzystuje sie bezposrednio adresy komorek pamieci, istnieje koniecznosc zmodyfikowania pewnych fragmentow kodu tak, aby dzialaly spod innego adresu, niz pierwotnie. Do tego celu sluzy tablica relokacji, zawarta najczesciej w oddzielnej sekcji ('.reloc' dla Visual C++). Nie kazdy jednak rodzaj pliku wykonywalnego moze byc relokowany. Pliki exe, sa ladowanie domyslnie pod adres 0x400000 (czwarty MB pamieci). Poniewaz kazdy proces otrzymuje wlasna przestrzen adresowa, nie ma mozliwosci, aby plik exe musial byc relokowany. Dlatego dane o relokacji mozna w ich przypadku pominac. Do tego celu sluzy parametr /FIXED linkera. Inna opcja to uzycie funkcji RemoveRelocations() z biblioteki ImageHlp. Mozemy w ten sposob zaoszczedzic nawet do kilkuset kilobajtow pamieci w przypadku bardzo duzych programow. Najnowsze wersje linkerow MS potrafia same wylaczac obsluge relokacji w plikach exe kompilowanych w trybie release. W przypadku trybu debug, musimy to zrobic recznie. Sytuacja komplikuje sie jesli chodzi o pliki dll. Usuniecie z nich sekcji .reloc spowoduje, ze modul przestanie dzialac na wiekszosci maszyn, poniewaz konflikty adresow sa tam bardzo czeste. Dlatego zdecydowanie odradzam stosowanie tej opcji w przypadku dynamicznych bibliotek.

1.1.6 Uzycie programu do kompresji plikow EXE.
Jesli pomimo uzycia przedstawionych tu technik plik nadal zajmuje duzo miejsca, pozostaje wykorzystanie kompresora plikow exe. Ich dzialanie polega na spakowaniu zawartosci pliku i dodaniu niewielkiego fragmentu kodu (tzw. stubu), ktory jest wykonywany kazdorazowo po uruchomieniu programu. Stub rozpakowuje w pamieci reszte pliku exe i skacze do wlasciwego kodu programu. Obecnie dostepnych jest sporo programow tego typu. Jednym z najpopularniejszych jest UPX. Oferuje on bardzo dobry stopien kompresji. Z rodzimych produkcji polecic moge FSG. Nalezy zwrocic uwage, ze glownym zalozeniem niektorych aplikacji tego typu jest zabezpieczenie programow przed modyfikacja i debuggowaniem. Nacisk klada wiec one na silne szyfrowanie i techniki utrudniajace zycie reverserom, a nie na zmniejszenie rozmiaru exe. Dlatego przed uzyciem PE compressora radze skonsultowac sie z instrukcja.

1.2 Przyspieszanie ladowania pliku:
-----------------------------------

1.2.1 Zmiana adresu bazowego aplikacji.
W punkcie 1.1.5 tego artykulu opisalem problemy wynikajace z konfliktow adresu bazowego wystepujacego czesto w przypadku dll'i. Standardowo sa one ladowane pod adres 0x10000000 (czyli 256MB pamieci). W przypadku, gdy inny modul zajmuje juz ten obszar, Windows musi relokowac biblioteke w inne miejsce. Wymaga to patchowania pliku w wielu miejscach i zajmuje pewna ilosc czasu. Rozwiazaniem jest wygenerowanie pliku, ktory bedzie uzywal innego adresu bazowego. Zmniejszy to szanse na konflikt z innym modulem. Jak to zrobic? Otoz kompilujemy i linkujemy nasza biblioteke jak zwykle. Uruchamiamy program, ktory umozliwia podglad modulow zaladowanych do pamieci (np Procdump). Sprawdzamy, pod jaki adres zostal relokowany nasz dll w kontekscie procesu (czyli pliku exe), ktory go zaladowal. Nastepnie linkujemy biblioteke drugi raz, podajac parametr /BASE:adres, gdzie adres to poczatek obszaru pamieci, pod jakim figurowal nasz modul w ProcDumpie. Ewentualnie mozemy posluzyc sie aplikacja REBASE dostepna w pakiecie Visual Studio i Platform SDK Microsoftu. Jej zaleta jest fakt, ze potrafi ona zmienic adres bazowy kilku bibliotek rownoczesnie. Trzecia opcja to zastosowanie funkcji ReBaseImage z biblioteki ImageHlp. Niestety, wszystkie te rozwiazania maja wade. Nawet jesli zmienimy adres bazowy dll'a na wlasnym komputerze, nie mamy gwarancji, ze na innej konfiguracji nie bedzie zachodzil konflikt. Dlatego dobrym pomyslem moze byc automatyczne rebase'owanie adresow wszystkich modulow w naszym programie w ramach procesu instalacji. W ten sposob mamy niemalze 100% pewnosci, ze biblioteki beda ladowane pod optymalne adresy.

1.2.2 Bindowanie importow.
Kolejnym powodem, dla ktorego ladowanie plikow PE moze trwac dlugo, jest koniecznosc dynamicznego laczenia bibliotek. Zastanowmy sie najpierw, co dzieje sie, gdy chcemy uzyc funkcji z pewnego dll'a bez posrednictwa GetProcAddress. Kazda importowana w ten sposob procedura zapisywana jest w tzw. tablicy importow jako string (bedacy nazwa funkcji), badz numer porzadkowy. W chwili gdy system Windows laduje modul (np. plik exe), musi on przeskanowac jego tablice importow. Dla kazdego wpisu w tej tablicy pobiera on adres funkcji z tablicy eksportow odpowiedniej biblioteki. Nastepnie adres ten zapisywany jest we wspomnianej wczesniej tablicy importow naszego modulu celem przyspieszenia dostepu do funkcji. Trzeba podkreslic, ze uzyskane w ten sposob informacje o adresach funkcji sa zapisywane jedynie w pamieci i tracone po zamknieciu aplikacji. Istnieje jednak sposob na pominiecie tego procesu. Jest to tak zwane bindowanie. Polega ono na wykonaniu opisanej wczesniej procedury dla kazdej importowanej przez plik wykonywalny funkcji. Zdobyte w ten sposob adresy sa zapisywane na stale w tablicy importow pliku exe. Dzieki temu system Windows moze pominac pracochlonne przetwarzanie listy eksportow kilku(nastu) bibliotek. Niestety, bindowanie ma rowniez swoja wade. Wiaze sie to z tym, ze w roznych wersjach tego samego dll'a funkcje moga miec rozne adresy. W tym wypadku zapisane przez nas wczesniej w pliku PE dane beda bledne. Na szczescie nie ryzykujemy nieprawidlowego dzialania programu, poniewaz tworcy Windows przewidzieli taka sytuacje i wprowadzili kontrole wersji uzywanych bibliotek. Jesli system operacyjny wykryje, ze dll ktorego probuje uzywac zbindowana aplikacja ma nieprawidlowa date kompilacji, pominie on zwyczajnie uzyskane wczesniej dane o adresach funkcji i przeprowadzi ponownie procedure laczenia z ta biblioteka. Majac ten teoretyczny wstep za soba przejdzmy do praktyki. Do bindowania modulow mozemy posluzyc sie aplikacja BIND (dolaczona do Platform SDK i kompilatorow MS). Taka sama role spelnia rowniez funkcja BindImage(Ex) z biblioteki ImageHlp. Podobnie jak zmiana adresu bazowego pliku, bindowanie ma najwieksze szanse powodzenia, jesli przeprowadzimy ja w ramach procesu instalacji naszego programu. Mamy wowczas pewnosc, ze adresy funkcji zapisane w pliku exe beda aktualne dla dll'i wystepujacych na systemie uzytkownika. Bedzie tak do momentu, az uzytkownik zainstaluje Service Pack'a, lub w inny sposob podmieni biblioteki systemowe.

2. Techniki wymagajace modyfikacji programu
===========================================

W tej czesci artykulu zajme sie opisem sposobow zmniejszania plikow exe, ktore wymagaja modyfikacji kodu zrodlowego. Metody te przynosza najlepsze rezultaty, lecz niestety wymagaja czasem sporego nakladu pracy. Wiele razy slyszalem wypowiedzi osob, ktore twierdzily, ze programy napisane w C/C++ nigdy nie dorownaja pod wzgledem rozmiaru swoim odpowiednikom w asemblerze. Jest to oczywiscie mit. Osobiscie nie widze przeszkod, by w C pisac np. intra 64kb, pod warunkiem, ze bedziemy przestrzegac kilka wymienionych ponizej zasad.

2.1 Wylaczenie obslugi wyjatkow C++.
------------------------------------
Wyjatki w C++, choc znacznie ulatwiaja np. obsluge bledow (brak zasobow w systemie itd.), wiaza sie ze znacznym powiekszeniem rozmiaru pliku exe. Wynika to z koniecznosci generowania przez kompilator kilku instrukcji, ktore inicjuja i usuwaja ramke obslugi wyjatkow. Dodatkowo dla wiekszosci funkcji tworzony jest fragment kodu, odpowiedzialny za niszczenie obiektow tymczasowych tworzonych na stosie w dowolnym momencie dzialania funkcji. Aby to osiagnac, rzeczywisty kod funkcji przeplatany jest z fragmentami danych na temat aktualnego stanu stosu. Dzieki tym informacjom kompilator moze zagwarantowac poprawne dzialanie programu i brak 'przeciekow' zasobow niezaleznie od miejsca, w ktorym podniesiony zostanie wyjatek. Niestety konsekwencja jest takze spowolnienie dzialania aplikacji (nawet jesli wyjatki nie sa wcale generowane). Co jednak, jesli mimo wszystko musimy uzyc jakies formy obslugi wyjatkow? Proponuje zastapic mechanizm C++ udostepnianym nam przez Windows SEH (Structured Exception Handling). Co prawda utrudnia on zaimplementowanie niszczenia zmiennych lokalnych, lecz nie powoduje znacznego powiekszenia rozmiaru pliku exe, ani znacznie wolniejszego funkcjonowania programu i generalnie wprowadza mniej kosztow dodatkowych, co moze byc istotne w przypadku osob nie do konca rozumiejacych mechanizm dzialania obslugi wyjatkow jezyka C++. W przypadku kompilatora Visual C++ wyjatki wylaczyc mozemy parametrem /GX-.

2.2 Uzycie biblioteki runtime w DLL.
------------------------------------
Kompilator VC++ udostepnia kilka wersji biblioteki standardowej. Wybierajac wersje w dll mozemy przeniesc czesc kodu z pliku exe do biblioteki. Dodatkowa zaleta jest fakt, ze system uzywa jednej kopii stron pamieci dla kodu wszystkich instancji dll'a w systemie, zaoszczedzamy wiec do kilkuset kb pamieci. Za linkowanie C runtime z bibliotece dynamicznej odpowiada parametr /MD i /MDd (wersja debug).

2.3 Zrezygnowanie ze standardowej biblioteki C/C++.
---------------------------------------------------
Nieco bardziej radykalnym krokiem jest calkowite zrezygnowanie z biblioteki C/C++. Oznacza to calkowite zastapienie wszystkich procedur do zarzadzania pamiecia, obslugi plikow, string'ow itd. i zastapienie ich odpowiednikami z WinAPI. Wymaga to sporo pracy, lecz znacznie zmniejsza rozmiar pliku. Z wlasnego doswiadczenia wiem, ze eliminujac z programu strumienie C++ mozna pozbyc sie okolo 400-500kb kodu. Obsluga systemu plikow dla C, choc ma znacznie mniejsze wymagania, rowniez moze miec znaczenie, jesli piszemy aplikacje o surowych wymogach co do zajmowanego miejsca.

2.4 Wylaczenie stub'u C.
------------------------
Jest to ostateczna forma zaoszczedzenia miejsca w programie. W ten sposob pozbywamy sie mozliwosci uzywania standardowych bibliotek jezyka, wiec metode te powinno sie stosowac w ostatecznosci. Polega ona na wylaczeniu fragmentu kodu, ktory przygotowuje srodowisko dla programu napisanego w jezyku wysokiego poziomu. Jak wiadomo, w momencie gdy system operacyjny skacze do tzw. punktu wejscia pliku wykonywalnego, nasz kod nie jest wykonywany natychmiast. Wczesniej wywolywana jest funkcja, ktora dodaje do naszego programu kompilator. Jej zadania sa dwojakie. Jesli piszemy aplikacje pod konsole windowsa, funkcja ta pobiera linie polecen (command line) i przetwarza parametry na tablice wskaznikow. Gdy nasza aplikacja ma dzialac w trybie okienkowym, procedura ta pobiera tzw. uchwyt modulu naszego procesu (hInstance), linie polecen i dodatkowe parametry dotyczace trybu dzialania programu. Poza tym, jest ona rowniez odpowiedzialna za inicjacje menedzerow pamieci malloc i new, ustawienie ziarna generatora liczb losowych, przygotowanie systemu plikow itd. Dopiero potem wywoluje ona dostarczona przez programiste funkcje main lub WinMain. Wszystkie te dzialania zajmuja sporo miejsca. Nawet najprostszy "Hello world!" moze zajmowac kilkadziesiat kb. Wygenerowana (poleceniem /map linkera) mapa pamieci programu ujawnia, jak wiele zbednych niekiedy funkcji zawiera taka aplikacja. Jak wiec mozemy wylaczyc generowanie stub'u przez kompilator? Aby na to pytanie odpowiedziec, musimy najpierw wiedziec, skad kompilator wie jaki kod 'wkleic' na poczatku exe'ka. Otoz tak naprawde, linker nie wie nic o stubie. Po prostu domyslnie ustawia on jako punkt wejscia procedure mainCRTStartup (dla aplikacji konsolowych), badz WinMainCRTStartup (dla programow 'okienkowych'). Poniewaz domyslnie standardowe biblioteki jezyka sa wlaczone, przeszukuje on liby w poszukiwaniu tych funkcji. Okazuje sie, ze kazda wersja (sposrod LIBC, LIBCD, LIBCMT, LIBCMTD, MSVCRT i MSVCRTD) biblioteki C Runtime zawiera te dwie procedury. Tak wiec kompilator generuje odpowiedniego stuba z kodu zawartego w tych plikach .lib. Teraz odpowiedz na pytanie powinna byc oczywista. Aby wylaczyc generowanie stuba, musimy zmusic kompilator do ignorowania standardowych bibliotek. Mozemy to osiagnac parametrem /Zl kompilatora, lub /NODEFAULTLIB linkera. Jednoczesnie musimy sami napisac wlasna procedure, ktora zastapi mainCRTStartup, lub WinMainCRTStartup. Prototypy tych funkcji sa jednakowe: void mainCRTStartup(); i void WinMainCRTStartup() (obie funkcje w domyslnej konwencji przekazywania parametrow __cdecl). Alternatywnie mozemy sklonic linkera do akceptowania innej nazwy funkcji uzywajac parametru /ENTRY:nazwa_funkcji. To, co bedziemy robic we wlasnym stub'ie nie ma zadnego znaczenia. Metoda ta ma jednak powazne ograniczenie. Nie mozna uzywac zadnej funkcji z bibliotek standardowych jezyka. Przyczyny sa dwie. Po pierwsze, biblioteki zmuszeni bylismy odlaczyc, aby ignorowac stub'a. Po drugie, zadaniem stub'a byla inicjacja srodowiska, w ktorym dzialaja funkcje z tych bibliotek. Wszelkie proby np. alokacji pamieci menedzerem zakoncza sie niepowodzeniem. Z tego powodu zmuszeni jestesmy do przepisania czesci procedur w oparciu o API Win32. Jesli uzywamy C++, nie bedziemy mogli np. wywolac operatora new i delete (sa one zdefiniowane w wylaczonych plikach lib) dopoki sami nie napiszemy ich przeciazonych wersji. W zamian jednak uzyskamy plik wynikowy, ktory nie rozni sie rozmiarem od swojego odpowiednika w asemblerze (w praktyce bez specjalnych modyfikacji mozna zmusic kompilator do wygenerowania 4-kilobajtowej binarki).

Podsumowanie
============
Przedstawilem tutaj praktycznie wszystkie stosowane przeze mnie sposoby optymalizacji pliku wykonywalnego. Uzywajac je, mozliwe jest uzyskanie plikow o wielkosci kilku kilobajtow. Z pewnoscia czesc osob czytajacych ten tekst nie odkryla w nim nic nowego, ani przelomowego. Celowo jednak pominalem kilka metod, ktore przynosza niewielkie korzysci (takich jak modyfikacja stub'u dosa). Zdaje sobie sprawe, ze stosujac pewne techniki mozna przekroczyc bariere 1kb i zblizyc sie do ok 700-750 bajtow. Wygenerowane w taki sposob pliki nie dzialaja jednakze na wielu systemach. Poza tym celem tego artykulu nie bylo bicie rekordu najmniejszego pliku exe, lecz przekazanie praktycznych informacji programistom piszacym w jezykach wysokiego poziomu. W artykule tym staralem sie przedstawic informacje najrzetelniej jak moglem. Niewykluczone jednak, ze moglem popelnic gdzie bledy. Jesli zauwazyles jakas pomylke z mojej strony, prosze o wyslanie mi informacji na adres podany ponizej.

mIGu^CCRK