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