C dla każdego (cz. 21.)
Start inaczej
Tematem dzisiejszego odcinka jest moduł startowy.
Przydatny będzie poprzedni listing, ponieważ
zamierzamy uruchomić go ponownie, w inny sposób.
Do każdego tradycyjnie skompilowanego programu
dołączany jest moduł startowy. Jego zadaniem jest
stworzenie właściwego środowiska pracy. Programy
zgodne z normą ANSI mają prawo oczekiwać między
innymi strumieni "stdin", "stdout", "stderr" oraz
parametrów "argc" i "argv" w funkcji "main()".
Właśnie zadaniem modułu startowego jest wzajemne
"dopasowanie" systemu operacyjnego l programów.
Uruchamiając nasz program, system operacyjny
rozpoczyna jego działanie od pierwszego bajtu
kodu, czyli w naszym wypadku - od pierwszej
funkcji.
Przede wszystkim musimy uzyskać dostęp do
biblioteki Exec. Wskaźnik na nią znajduje się pod
adresem 0x00000004 -o przepisujemy go do zmiennej
"SysBase". Następnie otwieramy bibliotekę
"dos.library" i zapisujemy jej adres w zmiennej
"DOSBase". Kolejnym krokiem jest sprawdzenie, czy
działamy pod systemem Workbench, czy Shell -
rozstrzygamy to na podstawie zawartości pola
"pr_CLI" w strukturze naszego procesu.
Przy uruchomieniu z Workbencha otrzymujemy w porcie
naszego procesu (pole "pr_MsgPort") strukturę
"WBStartup", której zawartość omówiliśmy miesiąc
temu. Trzonem tej struktury jest struktura
"Message" i, jak każdą wiadomość, trzeba ją po
wykorzystaniu odesłać do nadawcy.
Przy uruchomieniu z Shella system przekazuje nam
poprzez rejestry procesora "AO" i "DO" dwa
argumenty: pierwszy zawiera wskaźnik na linię
argumentową (zakończoną znakiem nowej linii i
zerem), w drugim zaś zapisana jest jej długość. My
jednak nie będziemy z tych argumentów korzystać.
W tym momencie standardowy moduł startowy wykonuje
masę przeróżnych operacji, specyficznych dla
konkretnej implementacji.
Będzie to więc np. inicjalizacja standardowych
strumieni wejścia/ wyjścia, operacji
zmiennoprzecinkowych, automatyczne otwieranie
bibliotek, dostosowanie linii argumentowej do
standardowej postaci itp. Nasz krótki moduł
startowy oczywiście tego wszystkiego nie robi -
patrz listing 16.
Należy podkreślić, że moduł startowy w każdym
kompilatorze pisze się nieco inaczej. Nasz listing
działa z kompilatorami, do których mamy dostęp:
SAS/C 6.56 i GCC 2.7.2.1 (ADE snapshot 961012).
Dostosowanie go do innych nie powinno być jednak
trudne.
Weźmy na przykład symbol "__saveds". Jego zadaniem
jest ustawienie na początku funkcji rejestru
procesora "a4" na blok danych. Operacja ta jest
potrzebna wówczas, gdy odwołania do danych są
wykonywane względem rejestru "a4". Jest to
standardowy tryb w kompilatorze SAS/C (opcja
DATA=near), a w GCC można go uzyskać przy użyciu
opcji "-fbasereI". Alternatywnym sposobem dostępu
do danych jest adresowanie bezwzględne - programy
są nieco dłuższe i powolniejsze, ale rozmiar bloku
danych nie jest ograniczony do 64 KB.
Przy uruchomieniu z Workbencha, zmienną "argc"
ustawiamy na O, a "argv" wskazuje na strukturę
"WBStartup". Przy uruchomieniu z Shella wypadałoby
właściwie obsłużyć linię argumentową i umieścić w
tablicy "argv", ale ze względu na ograniczoną
ilość miejsca pozostawiamy to zadanie Czytelnikom.
Po wykonaniu tych czynności możemy już wywołać
funkcję "main()".
Po wyjściu z tej funkcji trzeba po sobie
"posprzątać". Zamykamy bibliotekę "dos.library", a
przy uruchomieniu z Workbencha zwracamy wiadomość
"WBStartup", informując Workbench, że
zakończyliśmy działanie. Aby Workbench nie usunął
naszego programu z pamięci przed zakończeniem
przez nas pracy nad nim, tę ostatnią operację
trzeba wykonać przy wyłączonym multitaskingu.
Służy do tego funkcja:
void Forbid( void );
Działanie naszego programu kończy się z chwilą
opuszczenia funkcji "usermain()".
Spróbujmy przetestować nasz moduł startowy przy
użyciu listingu z poprzedniej części. Musimy
jednak dokonać w nim drobnej modyfikacji.
Zakładaliśmy, że to moduł startowy otworzy
bibliotekę "icon.library". Teraz musimy zrobić to
sami.
Wywołując kompilator SAS/C, musimy pamiętać o
wyłączeniu sprawdzania przepełnienia stosu - nasz
moduł startowy nie zawiera odpowiednich funkcji.
My wywołaliśmy kompilator tak:
sc resopt nostartup nostackcheck link
progname=listingl6 listingl6.c listing15.c
Kompilując z użyciem GCC, trzeba wymusić
umieszczanie napisów w bloku danych - kompilator
umieszcza je standardowo w bloku kodu, przed
funkcjami, co w wypadku modułu startowego
niechybnie spowoduje problemy. Kompilator ten w
specyficzny sposób traktuje też funkcję o nazwie
"main", umieszczając na jej początku dodatkowy kod
- najlepiej zmienić jej nazwę na np. "mymain".
gcc -fwritable-strings -fbasereI -nostdlib
-Dmain=myinain -o listingl6 listingl6.c
listingl5.c
Zazwyczaj używamy własnego modułu startowego, gdy
piszemy bardzo krótki program i ten standardowy
moduł jest dla nas niepotrzebnie rozbudowany. Dla
powyższego przykładu, z użyciem dodatkowych opcji
optymalizujących kod, rozmiary uzyskanych plików
wykonalnych przedstawiają się następująco:
Moduł startowy SAS/C GCC
Standardowy 2356 2820
Własny 908 1012
Wiemy już, co należy zrobić po uruchomieniu
programu. Dla zachowania symetrii należy też
omówić, jak uruchomić inny program. W tym celu
można posłużyć się funkcją:
LONG SystemTagList( STRPTR command, struct Tagitem
*tags );
LONG Syscem( STRPTR command, struct Tagitem *tags
);
LONG SyscemTags( STRPTR command, unsigned long
tagitype, ... );
Można tu zaobserwować pewną "dowolność". Ta sama
funkcja, z takimi samymi parametrami, jest
dostępna pod dwoma różnymi nazwami (SystemTagList
i System).
Parametr "command" to napis zawierający nazwę
programu wraz z argumentami.
Tagami zdefiniowanymi dla tej funkcji są:
SYSJnput, SYS_Output - (BPTR) pliki wejściowy i
wyjściowy;
jeśli któryś z plików nie zostanie zdefiniowany,
funkcja wykorzysta odpowiednie pliki programu
macierzystego.
SYS_Asynch - (BOOL) start asynchroniczny (w tle);
uwaga: ustawienie tego tagu powoduje, że System()
po zakończeniu pracy zamyka pliki WE/WY (nawet
wówczas, gdy korzystał z plików procesu
macierzystego)!
Ponadto do funkcji można przekazywać niektóre tagi
zdefiniowane dla potrzeb nie omówionej przez nas
funkcji CreateNewProc(), jak np. "NP_StackSize"
(rozmiar stosu w bajtach, standardowo 4000),
"NP_Priority" (priorytet procesu, standardowo taki
sam, jak procesu macierzystego).
Wynikiem działania funkcji jest kod wyjścia
uruchomionego programu lub -1, gdy utworzenie
nowego procesu się nie powiodło.
Spójrzmy na listing 17. Program uruchamia
synchronicznie komendę "List", skierowując jej
strumień wyjściowy do utworzonego wcześniej pliku.
Jako opcjonalny pierwszy argument listingu można
podać np. wzorzec "#?.c" - zostanie on dołączony
na koniec wywołania komendy "List". Jeżeli
wszystko pójdzie dobrze, to funkcja "System()"
zwróci 0. W takim wypadku, za pomocą funkcji
"Seek()" wracamy na początek pliku. Ponieważ
jednak funkcja "Seek()" jest niebuforowana, a
program "List" mógł używać funkcji buforowanych,
trzeba najpierw zsynchronizować bufory ze stanem
faktycznym za pomocą funkcji Flush() -
wspominaliśmy o tym przy omawianiu plików.
Następnie program odczytuje po jednej linii z
pliku, korzystając z funkcji:
STRPTR FGets( BPTR fh, STRPTR buf, unsigned long
buflen );
Jako argumenty podajemy otwarty plik, bufor
docelowy oraz jego długość. Funkcja zwraca adres
bufora lub O, gdy nie mogła nic odczytać (co
oznacza, że wystąpił błąd lub dotarliśmy do końca
pliku - można to rozpoznać przy użyciu "IoErr()").
Dla każdej odczytanej nazwy pliku nasz listing
uruchamia asynchronicznie zewnętrzny program.
Standardowym jest "More", można jednak przy
uruchamianiu listingu podać nazwę innego, jako
opcjonalny, drugi argument. Ostatnią operacją jest
zamknięcie pliku i jego skasowanie za pomocą
funkcji:
LONG DeleteFile( STRPTR name );
Argumentem jest nazwa obiektu (wbrew nazwie
funkcji, może nim być również katalog, tyle że
musi być pusty). Funkcja zwraca O, gdy usunięcie
obiektu się nie powiodło.
Listing nr 16
Listing nr 17