5.PDF

(220 KB) Pobierz
3858098 UNPDF
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
3858098.001.png
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
3858098.002.png
Zgłoś jeśli naruszono regulamin