osdev.labedz.org

Opis

Konsolidator GNU LD jest programem mającym za zadanie łączenie kilku plików obiektowych, realokacje zmiennych oraz powiązanie ze sobą referencji symboli w jednym pliku obiektowym [BINUTILS]. W celu zwiększenia kontroli nad procesem konsolidacji, program ld akceptuje pliki skryptu konfigurującego zgodnego z językiem 'Link Editor Command Language' firmy AT&T.

Dzięki użyciu bibliotek ogólnego przeznaczenia BFD (biblioteka udostępniająca interfejs do operacji na plikach obiektowych) konsolidator obsługuje wiele rodzai plików obiektowych, między innymi: COFF, ELF i a.out. Istnieje również możliwość łączenia ze sobą plików różnych rodzajów, w jeden, określony plik obiektowy. Jeżeli istniała by potrzeba dodania nowego typu do już obsługiwanych, wystarczy dołożyć procedury obsługi danego typu do biblioteki BFD.

Dużą zaletą konsolidatora LD jest to, iż w przypadku napotkania błędu, program nie przerywa natychmiast działania, a w miarę możliwości stara się kontynuować procedurę łączenia, dzięki czemu istnieje też możliwość wykrycia błędu programów na innym etapie testowania.

Konsolidator ld uruchamia się z poziomu linii poleceń, wywołując program nasm wraz z potrzebnymi parametrami.

$ ld [opcje] plik_obiektowy ...
-o plikZmienia domyślną nazwę pliku wynikowego
-f formatOkreśla format pliku wyjściowego.
-I katalogDodaje katalog do listy katalogów z plikami wynikowymi.
-b formatFormat pliku wejściowego
-tPowoduje wypisanie wszystkich nazw plików używanych w trakcie konsolidacji
-T plik_skryptuPowoduje użycie przez konsolidator dodatkowego pliku z poleceniami dotyczącymi procesu łączenia
--verbosePokazuje stadardowe ustawienia programu
Tabela: Typowe opcje konsolidatora LD

Opcje - ustawienia

Kompilator GCC uruchamia się z poziomu linii poleceń, wywołując program gcc wraz z potrzebnymi parametrami.

$ gcc [ -c|-S|-E] [-g] [-Opoziom] [-Wostrzeżenia] [-Ikatalog] [-o plik_wyjściowy] plik_wejściowy

Język skryptowy

Każdy proces konsolidacji jest kontrolowany przez zestaw instrukcji zawartych w pliku skryptowym. Jego głównym zadaniem jest opisanie w jaki sposób sekcje z plików wejściowych mają być odwzorowane w pliku wyjściowym, jednakże umożliwia on również przeprowadzenie wielu innych operacji.

Jeżeli podczas konsolidacji nie zostanie określony plik skryptowy, program linkera zastosuje plik standardowy - odpowiedni do rodzaju systemu operacyjnego (można go zobaczyć wywołując program z opcją '--verbose').

W celu uściślenia języka skryptowego programu konsolidatora, dokonano szereg założeń i uściśleń. Program linkera łączy pliki wejściowe w jeden plik wyjściowy. Plik wyjściowy i każdy plik wejściowy ma określony format nazywany formatem pliku obiektowego. Każdy plik obiektowy posiada między innymi listę sekcji. Sekcje w plikach wejściowych nazywają się sekcjami wejściowymi, i analogicznie - w pliku wyjściowym sekcje nazywane są sekcjami wyjściowymi. Każda sekcja w pliku obiektowym posiada nazwę i rozmiar. Większość sekcji posiada związany z nią blok danych, zwany zawartością sekcji. Każda sekcja może być określona jako ładowalna, co oznacza, że jej zawartość powinna zostać załadowana do pamięci przed uruchomieniem pliku wejściowego. Sekcja bez zawartości może być określona jako alokowalna, co oznacza że określona część pamięci komputera powinna być zarezerwowana, lecz żadna zawartość nie powinna być tam ładowana (w niektórych przypadkach pamięć ta powinna być wyzerowana). Pozostałe typy sekcji zazwyczaj zawierają dane potrzebne do procesu wykrywania błędów (debugowania).

Każda ładowalna lub alokowalna sekcja wyjściowa posiada dwa adresy. Pierwszym jest adres wirtualny VMA (ang. virtual memory address) - jest to adres jaki otrzyma ta sekcja po uruchomieniu programu. Drugim adresem jest adres ładowania LMA (ang. load memory address) - jest to adres pod jaki zostanie dana sekcja załadowana. W większości przypadków oba typy adresów są takie same. Przykładem wyjątku może być program którego dane umieszczone są w pamięci typu ROM, a podczas uruchomienia programu są ładowane do pamięci typu RAM. W tym przypadku adres programu w pamięci ROM powinien być adresem LMA, a adres w pamięci RAM adresem VMA.

Każdy plik obiektowy posiada także listę wszystkich użytych symboli, zwaną tablicą symboli. Symbol może być określony jako zdefiniowany i niezdefiniowany. Każdy symbol posiada nazwę oraz adres. Symbolem jest każda nazwa zmiennej, stałej czy funkcji. Każda zmienna czy funkcja, która jest nie nazwana, zostanie w pliku wyjściowym określona jako symbol niezdefiniowany.

Plik skryptu konsolidatora jest zwykłym plikiem tekstowy w którym znajduje się zestaw komend. Każda komenda jest słowem kluczowym po którym może nastąpić argument, lub wartość której można przypisać do zmiennej. Komendy mogą być oddzielone od siebie średnikami. Znaki odstępu są ignorowane.

Najprostszym możliwym skryptem konsolidatora jest po prostu jedna komenda: SECTIONS. Komenda ta używana jest do określenia planu pamięci pliku wyjściowego.

Załóżmy że nasz program składa się tylko z kodu, ustawionych i nieustawionych danych (ang. initialized/uninitialized data). Składniki te będą umieszczone odpowiednio w sekcjach .text, .data i .bss. Załóżmy także, że tylko te sekcję znajdują się w plikach wejściowych. Powiedzmy, że kod powinien zostać załadowany pod adres 0x10000, a dane powinny zaczynać się od adresu 0x80000000. Przykład pliku skryptu konsolidatora, który wykona plik wyjściowy według powyższych założeń:

SECTIONS { . = 0x10000; .text : { *(.text) } . = 0x8000000; .data : { *(.data) } .bss : { *(.bss) } }

Słowo kluczowe SECTIONS poprzedza zestaw komend przydziału symboli i opisu sekcji wyjściowych zamkniętych w nawiasach klamrowych.

Pierwsza komenda powyższego przykładu ustawia wartość specjalnego symbolu ' . ', który jest licznikiem lokacji. Jeśli adres sekcji wyjściowej nie zostanie zdefiniowany w inny sposób, to adres jest pobierany właśnie z aktualnej wartości licznika lokacji. Licznik ten jest powiększany o wielkość każdej sekcji wyjściowej. Na początku (po słowie kluczowym SECTIONS) ma on wartość zero.

Druga komenda definiuje sekcję wyjściową .text. Dwukropek jest częścią składni skryptu, który nie wpływa na sposób generacji plików wynikowych. Po nazwie sekcji, w nawiasach klamrowych umieszcza się listę sekcji wejściowych, które powinny zostać umieszczone w tej sekcji wyjściowej. Znak gwiazdki jest typowym znakiem globalnym i określającym każdą nazwę pliku wejściowego. Wyrażenie *(.text) oznacza każdą sekcję wejściową .text zawartą we wszystkich plikach wejściowych. Ponieważ licznik lokacji został ustawiony uprzednio na wartość 0x10000, konsolidator w pliku wyjściowym umieści sekcję .text pod adresem właśnie 0x10000.

Pozostałe linie definiują sekcję .data oraz .bss w pliku wyjściowym. Konsolidator umieści sekcję danych ustawionych pod adresem 0x80000000, natomiast sekcję danych nie ustawionych pod adresem 0x80000000 plus rozmiar sekcji danych ustawionych - czyli sekcja .bss będzie się znajdować zaraz po sekcji .data.

Parametrem nazwy sekcji może być wyrażenie, którego wartość określa konkretny adres w pamięci pod którym dana sekcja ma się znajdować.

Znaki globalne używane do określania nazw plików są identyczne z tymi co są używane w konsolach Unix'owych.

  • * - określa dowolną ilość znaków
  • ? - określa jeden dowolny znak
  • [znaki] - określa każdą nazwę składającą się z określonych znaków. Istnieje możliwość określenia przedziału typu [a-z] oznaczającego wszystkie małe litery.
SECTIONS { outputa 0x10000 : { all.o foo.o (.input1) } outputb : { foo.o (.input2) foo1.o (.input1) } outputc : { *(.input1) *(.input2) } }

Powyższy przykładowy skrypt nakazuje konsolidatorowi przeczytanie wszystkich sekcji wejściowych z pliku all.o i umieszczenie ich w sekcji wyjściowej outputa znajdującej się pod adresem 0x10000. Wszystkie sekcje .input1 z pliku foo.o będą umieszczone w tej samej sekcji pod kolejnym adresem. Wszystkie sekcje .input2 z pliku foo.o będą umieszczone w sekcji wyjściowej .outputb przed sekcjami .input1 z pliku foo1.o. Sekcje .input1 i .input2 z pozostałych plików będą umieszczone w sekcji wyjściowej .outputc.

Ponieważ w wielu rodzajach plików obiektowych, zwykłe symbole nie znajdują się w żadnej z sekcji wejściowych, konsolidator traktuje je jakby znajdowały się w specjalnej sekcji nazwanej COMMON. W większości przypadków symbole te będą umieszczane w sekcji wyjściowej .bss.

.bss { *(.bss) *(COMMON) }

Język skryptowy zawiera też kilka wbudowanych funkcji, które mogą być użyte w wyrażeniach. Jedną z nich jest ALIGN(wyrażenie), która zwraca wartość licznika lokacji z dopasowaniem adresu do wartości wyrażenia. Umożliwia to umieszczanie odpowiednich sekcji w określonym miejscu pamięci np. dopasowanie do wielkości stron adresu pod którym będą umieszczone tablice stron, lub też dopasowanie do wielkości słowa maszynowego sekcji danych w celu umożliwienia szybszego do nich dostępu.

Jeżeli istnieje potrzeba pominięcia niektórych, określonych plików, używane jest słowo kluczowe EXCLUDE_FILE. Na przykład:

(*(EXCLUDE_FILE (*crtend.o *otherfile.o) .ctors))

spowoduje, że wszystkie sekcje .ctors będą dołączone do sekcji wyjściowej oprócz plików *crtend.o i *otherfile.o.

Istnieje możliwość przypisanie dowolnemu symbolowi wartości używając typowych dla języka C operatorów:

symbol = wyrażenie; symbol += wyrażenie; symbol -= wyrażenie; symbol *= wyrażenie; symbol /= wyrażenie; symbol <<= wyrażenie; symbol >>= wyrażenie; symbol &= wyrażenie; symbol |= wyrażenie;

Pierwsza instrukcja jaka jest wykonywana przez program nazywa się punktem wejścia (ang. entry point). Aby ustawić adres takiej instrukcji, w pliku skryptowy, korzystamy z komendy ENTRY. Argumentem tej komendy jest nazwa symbolu (etykieta).

ENTRY(symbol)

Jest kilka sposobów na ustawienie punktu wejścia do programu. Program konsolidatora będzie próbował ustawić go w następujących krokach (dopóki któryś się nie powiedzie):

  • opcja -e etykieta w linii komend
  • polecenie ENTRY
  • wartość etykiety (symbolu) start
  • adres pierwszego bajtu sekcji .text
  • adres zerowy