W głab C #3,
[ Pobierz całość w formacie PDF ]
V. Kod wynikowy 83 V. Kod wynikowy Niniejszy rozdział różni się trochę od reszty książki. Rozważania w nim zawarte "leżą na niższym poziomie abstrakcji", nie dotyczą bowiem składni ani bibliotek języka C lecz procesu powstawania programu wynikowego i sposobów ingerencji w ten proces. Podczas gdy w pozostałych rozdziałach starałem się opisywać język C w sposób niezależny zarówno od kompilatora jak i sprzętu, ten rozdział będzie dotyczył tylko komputerów kompatybilnych z IBM PC, pracujących pod nadzo- rem systemu DOS. Każdy, kto zetknął się z językiem C, zastanawiał się zapewne, dlaczego naj- prostszy program: main() {} daje tak duży kod wynikowy. Otóż dzieje się tak nie dlatego, że kompilatory C dają bardzo nieoptymalny kod, tylko dlatego, że bardzo poważnie traktują każdy program. W wyniku takiego poważnego podejścia, do każdego programu dołącza- ny jest spory fragment kodu (w dalszej części rozdziału będę określał go mianem Startup), który po pierwsze przygotowuje pewne dane dla programu głównego a po drugie robi wszystko co możliwe, żeby zabezpieczyć system przed zmianami jakie może poczynić program. Startup zachowuje szereg informacji o stanie syste- mu w chwili uruchomienia programu: adres zmiennych środowiskowych, wersję DOS-u, wektory niektórych przerwań, itp. Kolejną wykonywaną czynnością jest przygotowanie pewnych danych i funkcji wykorzystywanych przez program główny. Instalowana jest na przykład standardowa funkcja obsługi błędu dzielenia przez zero, funkcje arytmetyki zmiennoprzecinkowej, zapamiętywana jest wartość zegara BIOS-u w celu ewentualnego użycia przez funkcję clock . Jedną z ważniejszych funkcji wykonywanych przez Startup jest przygotowanie argumentów dla funkcji main . Funkcja ta może mieć trzy argumenty: main(int argc, char *argv[], char *envp[]) Oczywiście, nie każdy program musi używać argumentów, a nie każdy, który ich używa, musi używać wszystkich. Poprawne są również następujące nagłówki funkcji main : main() main(int argc) main(int argc,char *argv[]) W pierwszym argumencie, oznaczonym tutaj argc, Startup przekazuje do progra- mu głównego liczbę argumentów w wierszu wywołania programu plus jeden. Drugi argument, oznaczony argv, jest wskaźnikiem do tablicy wskaźników wska- zujących kolejne argumenty wywołania. Na przykład, jeżeli program został wy- wołany w następujący sposób: 84 Wgłąb języka C program -u book a:\*.c b:\*.c to argumenty funkcji main będą miały następujące wartości: argc=5 argv[0]="program" /* DOS 3.0 i wyzsze */ argv[1]="-u" argv[2]="book" argv[3]="a:\*.c" argv[4]="b:\*.c" Ostatni argument funkcji main , oznaczony envp, jest wskaźnikiem do tablicy wskaźników do zmiennych środowiskowych. Ostatnim elementem tablicy jest wskazanie puste NULL. Poniższy program używa trzeciego argumentu funkcji main , do wypisywania wartości zmiennych środowiskowych w chwili uruchomienia programu. #include <stdio.h> void main(int c, char *v[], char *env[]) { while(*env) printf("%s\n",*env++); } Po wykonaniu wszystkich opisanych czynności, Startup wywołuje program głów- ny, czyli funkcję main . Po powrocie z funkcji main , czyli po zakończeniu dzia- łania programu, Startup odtwarza zapamiętane przed wywołaniem programu informacje (na przykład wektory przerwań) i wraca do DOS-u. 1.Zmieniamy Startup Często pisze się proste programu, nie wymagające zachowywania wektorów prze- rwań, instalowania procedur obsługi błędów i wykonywania wszystkich tych ope- racji, które robi Startup. Chcielibyśmy, żeby takie programy były krótkie a mogą one być krótkie, a nawet bardzo krótkie. Żeby tak się stało, trzeba pozbyć się zbędnego kodu, a więc trzeba usunąć oryginalny Startup. Spróbujmy stworzyć własną wersję Startup-u, która będzie ograniczała się jedynie do wywołania funkcji main , umożliwiała zrobienie z prostego programu małego COM-a. W tym miejscu trzeba kilka słów poświęcić sposobowi w jaki kompilator (i konsolidator) dołączają Startup do programu. W przypadku kompilatorów firmy Borland jest to realizowane w sposób bardzo prosty i przejrzysty. Kod Startup zawarty jest w osobnych plikach o nazwach c0?.obj (? symbolizuje tu literę oznaczającą model pamięci np. s - model small ). Dla każdego modelu pamięci istnieje osobny kod Startup i osobny moduł c0?.obj . V. Kod wynikowy 85 Konsolidacja programu w środowiskach firmy Borland wygląda następująco: tlink c0s.obj program ... , program , , cs.lib (kropki oznaczają ewentualne kolejne moduły programu). Dzięki takiemu rozwiązaniu zastąpienie Startup-a sprowadza się do zastąpienia modułu c0s.obj z powyższego przykładu innym, np.: tlink tsr16.obj program ... , program , , cs.lib W środowisku Microsoft C nie ma oddzielnych modułów Startup. Kod startowy "zaszyty" jest w bibliotekach i automatycznie dołączany do każdego programu łączonego z biblioteką. Aby standardowy kod Startup nie został dołączony do programu i mógł być zastąpiony innym, w programie należy zdefiniować zmien- ną o nazwie _acrtused (dlatego we wszystkich programach przedstawionych w tym rozdziale taka zmienna jest zdefiniowana): int _acrtused=0; i dokonać konsolidacji programu z opcją /NOE, np. tak: link /NOE tsr16.obj program ... , program , , slibce.lib Wszystkie opisane w tym rozdziale przykłady kodów startowych służą do otrzymywania programów typu COM. Punkt wejścia programu typu COM musi mieć przemieszczenie 100h. Nasz pierwszy, najprostszy Startup od razu wywoła funkcję _main , a po powrocie z tej funkcji wróci do DOS-u. Przypominam, że identyfikatory globalne w języku C są automatycznie poprzedzane podkreśleniem. Dlatego funkcja main nazywa się w rzeczywistości _main. ; plik c0.asm .MODEL SMALL extrn _main:near .CODE ORG 100h start: call _main ; wywolaj funkcje main mov ah,4Ch int 21h ; wróc do DOS-a end start Proszę zwrócić uwagę, że przed powrotem do DOS-u ustawiamy tylko zawartość rejestru AH, natomiast rejestr AL będzie zawierał wartość zwróconą przez funkcję main . Ta właśnie będzie zwrócona przez program. Po asemblacji pliku c0.asm otrzymamy moduł c0.obj. Możemy teraz napisać pierwszy program, w którym oryginalny Startup zastąpi- my naszą minimalną wersją. Program będzie zamieniał ze sobą porty drukarki, a więc po jego wykonaniu port pierwszy stanie się drugim i vice versa. Progra- 86 Wgłąb języka C mik taki jest przydatny np. gdy mamy uszkodzony port LPT1 i chcemy "podsta- wić" go portem LPT2. /* plik lptport.c */ int _acrtused=0; void main() /* program wymienia adresy portów LPT1 i LPT2 */ { int x; x=*(int far *)0x408; /* 40h:08h adres LPT1 */ *(int far *)0x408=*(int far *)0x40A; /* 40h:0Ah adres LPT2 */ *(int far *)0x40A=x; } Program zamienia ze sobą adresy portów drukarki umieszczone w obszarze da- nych BIOS-u. Załóżmy, że program znajduje się w pliku lptport.c . Po skompi- lowaniu w modelu Small 1) otrzymamy moduł lptport.obj . Możemy teraz utworzyć program wykonywalny: tlink /t c0.obj lptport.obj , lptport.com lub dla Microsoft C link /NOE c0.obj lptport.obj , lptport , , slibce exe2bin lptport.exe W bieżącym katalogu zostanie utworzony program LPTPORT.COM . Jego dłu- gość w zależności od kompilatora i ustawionych opcji wyniesie około 50 bajtów! Wprawdzie nie robi on wiele, ale ten sam program skompilowany "standardowo" zajmuje przecież prawie 4 tysiące bajtów. Następny programik będzie służył do wyboru opcji w programach wsadowych (typu .bat). Będzie on pobierał z klawiatury odpowiedź na zadane pytanie i zwracał odpowiednią wartość. Wartość zwracaną przez program można w programach wsadowych testować przy pomocy warunku: if ERRORLEVEL liczba Warunek ten jest spełniony, gdy wartość zwrócona przez ostatnio wywołany program jest równa lub większa od wartości liczba. Ponieważ funkcje obsługi wejścia w standardowych bibliotekach C są dość ob- szerne, będziemy czytać klawiaturę bezpośrednio przy pomocy funkcji BIOS-u. Odpowiednią do naszego programu będzie funkcja 0 przerwania 16h. Funkcję 1 Wszystkie programy należy kompilować bez informacji dla debuggera, a w kompila- torach firmy Microsoft z opcją /Gs V. Kod wynikowy 87 _getch czytającą znak z klawiatury zdefiniujemy w osobnym pliku getch.asm , gdyż będziemy jej używać także w innych programach 2) . ; plik getch.asm .MODEL SMALL .CODE proc __getch ; funkcja pobiera znak z bufora klawiatury mov ah,0 ; i zwraca jako rezultat int 16h ; je ż eli bufor jest pusty czeka na znak ret endp public __getch end Wykorzystana funkcja 0 przerwania 16h czeka na naciśnięcie klawisza i zwraca w rejestrze AX kod naciśniętego klawisza. Wartości tej nie trzeba nigdzie przepi- sywać, ponieważ program w języku C spodziewa się przekazania wartości funkcji typu int właśnie w rejestrze AX. Program ma pobierać odpowiedź na pytanie. Załóżmy, że po naciśnięciu klawi- sza T (odpowiedź twierdząca) program będzie zwracał wartość 116 (kod znaku 't') a w przeciwnym wypadku wartość 0. /* plik tak_nie.c */ int _acrtused=0; main() { char c=_getch(); /* pobierz znak z klawiatury */ if(c=='t'||c=='T') return 't'; /* jezeli 't' lub 'T' */ else return 0; /* w przeciwnym razie */ } Teraz kompilujemy nasz program w modelu Small i tworzymy program wyko- nywalny. tlink /t c0.obj getch.obj tak_nie.obj , tak_nie lub link /NOE c0.obj getch tak_nie , tak_nie , , slibce exe2bin tak_nie.exe Programik ma tylko 50 bajtów długości. Można go wykorzystać w programach wsadowych w następujący sposób: 2 Moduły asemblerowe, w których definiowane są symbole używane w programie w C należy kompilować programem TASM z opcją /ml [ Pobierz całość w formacie PDF ] |