Spis treści

Własny kod startowy

W każdym podręczniku języka C znajdziemy informację o tym, że wykonywanie programu rozpoczyna się od funkcji main(). Rzeczywiście, pisząc program odnosimy takie wrażenie i nic nie wydaje się świadczyć o tym, że może być inaczej. Tymczasem nie jest to prawdą. Między miejscem, gdzie system operacyjny wykonuje skok do świeżo załadowanego kodu programu, a pierwszą linijką main() jest wykonywane co najmniej kilka, a najczęściej kilkadziesiąt kilobajtów kodu. Kodu, dodajmy, najczęściej niezupełnie potrzebnego.

Co się w takim kodzie odbywa? Zaczniijmy od tego, że można sobie wyobrazić program w ogóle nie posiadający kodu startowego. System mógłby z miejsca po załadowaniu programu skoczyć do main(). Taki program jednak poprawnie uruchamiałby się jedynie z konsoli tekstowej. Próba uruchomienia go z Ambienta zakończyłaby się zawieszeniem procesu. Ambient mianowicie zaraz po stworzeniu procesu dla uruchamianego programu, wysyła do portu tego procesu specjalną wiadomość. Wiadomość ta pełni podwójną rolę. Po pierwsze zawiera w sobie parametry uruchomienia programu, takie jak jego ikona i ewentualne inne ikony które brały udział w uruchomieniu. Po drugie odpowiedź na tę wiadomość jest dla Ambienta sygnałem, że program się zakończył, wysłanie takiej odpowiedzi jest obowiązkowe. To minimum obowiązków, jakie musi wypełnić kod startowy. Dochodzi do tego otwarcie niezbędnych bibliotek współdzielonych. W przypadku korzystania z libnixa lub ixemul.library kod startowy tworzy też „standardowe otoczenie” dla funkcji biblioteki standardowej C i POSIX-a (więcej na ten temat). Z tego względu kod dołączany do programu przy użyciu jednej z tych bibliotek jest dość złożony, a przez to długi.

Kiedy warto?

Zaletą własnego kodu startowego będzie z pewnością jego krótkość. Zmniejszenie czasu uruchamiania się programu jest praktycznie niezauważalne. Po własny kod z pewnością warto sięgnąć przy pisaniu kilkukilobajtowych „pchełek”, bo w takim przypadku standardowy kod startowy może być większy od reszty programu. Własnego kodu można też używać dla prostej satysfakcji urwania kilkunastu kilobajtów z „binarki” programu. Własnego kodu nie możemy natomiast użyć, jeżeli linkujemy z ixemul.library. W przypadku libnixa zależy to od użytych jego funkcji. Większość prostych funkcji standardowej biblioteki C nie wymaga czynności przygotowawczych i będą współpracować z naszym kodem. Są jednak i takie, które wymagają wywołania w kodzie startowym określonych konstruktorów. Jeżeli takich funkcji użyjemy, linker w czasie kompilacji sypnie nam błędami o nierozwiązanych symbolach. Wtedy nie ma wyjścia – albo rezygnujemy z tych funkcji, albo z własnego kodu startowego. Własny kod będziemy więc stosowali głównie w programach, w których rezygnujemy z biblioteki standardowej C na rzecz natywnego API MorphOS-a, albo korzystamy z niej w stopniu minimalnym.

Po podjęciu decyzji o użyciu własnego kodu startowego, trzeba poinformować o niej kompilator. Pominięcie kodu standardowego osiągamy używając opcji −nostartfiles. Jeżeli więc mamy własny kod, ale chcemy spróbować używać z nim libnixa, podajemy opcje −noixemul −nostartfiles. Jeżeli zaś chcemy dodatkowo pozbyć się biblioteki standardowej C, zamiast −nostartfiles dajemy −nostdlib, która domyślnie wyłącza też standardowy kod startowy.

Przejdźmy do konkretów

Zanim to jednak zrobię, chciałbym zauważyć, że oprócz czynności wykonywanych przed wywołaniem funkcji main() programu, są również czynności wykonywane po jej zakończeniu, czyli coś, co moglibyśmy nazwać „kodem końcowym”. Ze względu na to, że zazwyczaj ten kod znajduje się tam, gdzie kod właściwie startowy (ten sam plik, ta sama nawet funkcja), łącznie określa się to wszystko mianem kodu startowego.

Jak już wspomniałem na początku, wykonywanie się programu wcale nie musi rozpoczynać się od funkcji main(). Gdzie się zatem rozpoczyna? Otóż wykonanie załadowanego z dysku pliku wykonywalnego rozpoczyna się od jego początku, czyli, w przypadku języka C, od pierwszej funkcji (ponieważ w C kod nie może występować poza funkcją). Tu ważna uwaga. Kompilator GCC 2.95.3 zawsze rozmieści funkcje w kodzie wynikowym w takiej kolejności, w jakiej występują w źródle. W przypadku GCC 4, jego agresywny optymalizator potrafi zmienić kolejność funkcji w obrębie jednego pliku źródłowego. Aby więc mieć pewność, że nasza funkcja startowa na pewno będzie pierwsza w kodzie wykonywalnym, powinna się ona jako jedyna znajdować w oddzielnym pliku źródłowym i być linkowana jako pierwsza (kolejność linkowania jest zawsze przestrzegana). Po tym wstępie możemy przejść do kodu.

#include <proto/exec.h>
#include <proto/dos.h>
#include <dos/dos.h>
#include <workbench/startup.h>

Zaczynamy jak zwykle od zainkludowania niezbędnych plików nagłówkowych. Będziemy się posługiwać dwiema podstawowymi bibliotekami: exec.library i dos.library. To wyjaśnia zagadkę dlaczego standardowy kod startowy, czy to libnixa czy to ixemul.library zawsze otwiera nam te dwie biblioteki – po prostu sam ich potrzebuje...

struct Library *SysBase;
struct Library *DOSBase;

Skoro używać będziemy tych dwóch bibliotek, potrzebne są zmienne do przechowywania ich baz.

extern ULONG Main(struct WBStartup *wbmessage);

Tu z kolei mamy deklarację głównej funkcji naszego programu. Ponieważ w pliku z kodem startowym powinna się znajdować tylko jedna funkcja (z powodów wspomnianych wyżej), reszta kodu musi być gdzie indziej, stąd konieczność zadeklarowania funkcji. Jej nazwa jest całkowicie dowolna, dla jasności w przykładzie nazwałem ją Main(). Jej parametrem jest otrzymana od Ambienta wiadomość WBStartup. Jeżeli nie chcemy z niej korzystać, naszą funkcję główną możemy zadeklarować po prostu jako:

extern ULONG Main(void);

Następna rzecz to definicja zagadkowego symbolu __abox__:

ULONG __abox__ = 1;

Symbol ten potrzebny jest systemowi do odróżnienia plików wykonywalnych MorphOS-a od innych plików binarnych w formacie ELF. Brak tego symbolu spowoduje potraktowanie MorphOS-owego pliku wykonywalnego jako pliku w amigowym formacie PowerUP i wykonanie go w ramach wstecznej kompatybilności poprzez bibliotekę ppc.library z trudnym do przewidzenia skutkiem.

ULONG Start(void)
{
  struct Process *myproc = 0;
  struct Message *wbmessage = 0;
  BOOL have_shell = FALSE; 
  ULONG return_code = RETURN_OK;

W tym miejscu zaczyna się wykonywanie programu. Nazwa tej funkcji jest również dowolna. Na początek kilka zmiennych. Zmienna myproc będzie zawierała wskaźnik na nasz proces, wbmessage to wskaźnik na wiadomość otrzymaną od Ambienta. Zmienna have_shell będzie oznaczała czy program został wywołany z konsoli czy z Ambienta. Wreszcie return_code to wynik, jaki nasz program zwróci konsoli. Jeżeli program wykona się bez błędów jest to zazwyczaj 0 i taką właśnie wartość ma stała RETURN_OK.

  SysBase = *(struct Library**)4L;

Pora na zainicjalizowanie SysBase, czyli bazy biblioteki exec.library. Jest to biblioteka wyjątkowa, ponieważ jest zawsze otwarta, a wskaźnik na jej bazę jest, z powodów historycznych i dla zachowania wstecznej kompatybilności, umieszczony pod adresem $00000004. Mając już dostęp do exec.library możemy zbadać, czy program został uruchomiony z konsoli czy z Ambienta:

  myproc = (struct Process*)FindTask(0);
  if (myproc->pr_CLI) have_shell = TRUE;

Informacja ta pochodzi ze struktury Process opisującej nasz program. Następnie dochodzimy do odebrania wiadomości startowej od Ambienta, oczywiście robimy to tylko wtedy, gdy program nie został odpalony z konsoli:

  if (!have_shell)
  {
    WaitPort(&myproc->pr_MsgPort);
    wbmessage = GetMsg(&myproc->pr_MsgPort);
  }

Ze struktury Process wyciągamy adres portu procesu a następnie z portu odbieramy wiadomość startową. Wiadomość tę można później przekazać naszej głównej funkcji Main(), o ile będzie nam tam potrzebna.

  if (DOSBase = OpenLibrary((STRPTR)"dos.library", 0))
  {

Kolejnym krokiem jest otwarcie dos.library. W zasadzie ten prosty kod startowy jej nie potrzebuje. Niemniej po pierwsze trudno sobie wyobrazić program, który nie będzie jej potrzebował później, po drugie skoro standardowe kody startowe otwierają tę bibliotekę, to po prostu trzymam się konwencji. dos.library otwiera się zwyczajnie, jak każdą inną bibliotekę.

    return_code = Main((struct WBStartup*)wbmessage);

Nadeszła pora na wykonanie zasadniczej funkcji naszego programu. Przekazanie do niej struktury WBStartup jest opcjonalne. Natomiast nie jest opcjonalne odebranie wyniku i użycie go później jako wartości zwracanej do systemu.

    CloseLibrary(DOSBase);
  }
  else return_code = RETURN_FAIL;

Od tego momentu zaczynamy sprzątanie. Zamykamy dos.library. Jeżeli otwarcie tej biblioteki się nie udało, zwracamy stałą RETURN_FAIL. Jest to najcięższy kaliber błędu i oznacza całkowitą niemożność wykonania programu. Co prawda w praktyce trudno sobie wyobrazić brak dos.library w systemie, bo wtedy po prostu MorphOS nie byłby się w stanie uruchomić. Jednak niemożność otwarcia biblioteki może mieć i inne przyczyny, na przykład prozaiczny brak pamięci. Dlatego trzeba się i na taką sytuację przygotować.

  if (wbmessage)
  {
    Forbid();
    ReplyMsg(wbmessage);
  }

Finalna obsługa wiadomości startowej Ambienta. Nawet jeżeli z niej nie korzystamy, przy wychodzeniu z programu trzeba na nią odpowiedzieć. Pewną zagwozdką jest jednak funkcja Forbid(), która jak wiadomo blokuje wielozadaniowość. Gdzie jest jednak odpowiadająca jej Permit()? Z pozoru kod ten jest bez sensu, program wyłącza wielozadaniowość i wychodzi z siebie. Sens jednak powraca, jeżeli wiemy, że w momencie gdy proces, który zablokował wielozadaniowość, kończy się, przełączanie procesów jest automatycznie przywracane. Zwróćmy uwagę na skutek. Odpowiedź na wiadomość startową jest umieszczana w porcie Ambienta przez ReplyMsg(), przy wyłączonej wielozadaniowości. Przez to Ambient nie jest w stanie tej wiadomości odebrać przed zakończeniem się procesu. W efekcie Ambient ma stuprocentową gwarancję, że odebranie wiadomości WBStartup następuje w czasie, gdy uruchomiony proces już nie istnieje w systemie. Oczywiście to zatrzymanie wielozadaniowości jest bardzo krótkie, bo dalej nasz program nic już nie robi oprócz przekazania wyniku działania systemowi:

  return return_code;
}

$VER: czyli identyfikacja programu

Ten temat nie jest ściśle związany z samym kodem startowym, ale ponieważ najczęściej version string programu znajduje się w tymże kodzie, postanowiłem kilka słów o nim napisać. Czym jest version string? Jest krótkim tekstem w ściśle określonym formacie, zawierającym nazwę programu, jego numer wersji, datę kompilacji i opcjonalnie informację o autorze i jego prawach. Informację tę potrafi odczytywać wiele narzędzi, między innymi Ambient, systemowe polecenie Version, program Installer i inne. Tekstowy łańcuch identyfikacyjny rozpoczyna się ciągiem „$VER:”, aby można go było znaleźć w kodzie programu. Zdecydowana większość narzędzi wyszukuje ten tekst poczynając od początku pliku, dlatego dobrze, aby znalazł się możliwie blisko tegoż początku. Niestety zadeklarowanie tego tekstu jako prostej zmiennej powoduje umieszczenie go w sekcji danych programu, a sekcja ta w formacie ELF znajduje się zazwyczaj po sekcji kodu. Niemniej można wymusić umieszczenie version stringa w sekcji kodu:

__attribute__ ((section(".text"))) UBYTE VString[] =
 "$VER: program 1.0 (21.6.2011) © 2011 morphos.pl\r\n";

Specyficzne dla GCC rozszerzenie języka C __attribute__ wymusza umieszczenie tekstu w sekcji .text formatu ELF zawierającej kod wykonywalny programu. Ponieważ nasz kod startowy jest linkowany jako pierwszy, dzięki temu identyfikator programu znajdzie się na samym początku, zaraz za kodem funkcji Start(). A czemu nie przed? Z prostego powodu, wykonanie kodu zaczyna się od początku sekcji .text. Wtedy procesor spróbowałby „wykonać” version stringa, ze skutkiem, którego łatwo się domyślić... Eksperymentatorzy mogą sprawdzić...

Gotowy przykład

Dodatkiem do artykułu jest gotowy do skompilowania tradycyjny programik „Hello world!” (archiwum). Używa on tylko API systemu, a więc jest kompilowany z opcją −nostdlib. Rozmiar pliku wynikowego to 1592 bajty. Dla porównania taki sam program z użyciem libnixa i printf() z biblioteki standardowej zajmuje 30 964 bajty. Nawet gdy pozbędziemy się printf()-a i zastosujemy natywny Printf() z dos.library, wciąż dostaniemy 13 500 bajtów. Ponieważ projekt składa się z dwóch plików *.c, dodałem doń prosty (żeby nie powiedzieć prymitywny) plik makefile, dzięki czemu można skompilować całość wpisując w konsoli po prostu „make”.