Środowisko programistyczne

ŁF
<html> <body style="FONT-FAMILY: Verdana; FONT-SIZE: 10pt;"> <style> table {font-size: 10pt} h3 { color: #000000; font-weight: bold; font-size: 18pt; background-color: #e8d5ba; text-align: center; padding: 5; border: double 1pt; width: 100%; margins: 0; paddings: 4 } h4 { color: #000000; font-weight: bold; font-size: 16pt; background-color: #c5dcdc; text-align: center; padding: 5; border: double 1pt; width: 100%; margins: 0; paddings: 4 } h5 { color: #000000; font-weight: normal; font-size: 11pt; background-color: #c5dcc5; text-align: left; padding: 5; border: double 1pt; width: 100%; margins: 0; paddings: 4 } h6 { color: #0000cd; font-weight: bold; font-size: 12pt; font-family: monospace, Courier, "Courier CE"; text-align: left; text-indent: 10pt; word-spacing: 3pt; width: auto } em { color: #0000cd; font-weight: normal; font-size: 12pt; font-family: Courier, "Courier CE"; text-align: left; text-indent: 10pt; word-spacing: 0pt; width: auto } </style>

LINUX - Środowisko programistyczne.

1. Narzędzia programistyczne w systemach typu Unix

Język C jest językiem programowania ogólnego przeznaczenia. Jest on jednak mocno związany z systemem Unix, dla potrzeb którego został opracowany. System operacyjny, kompilator C i niemal wszystkie programy usługowe systemu Unix zostały napisane w C (lub C++). W starszych wersjach systemów (np. w systemie SunOS 4.x) kompilator języka C był standardowo dostarczany przez producenta i nosił nazwę cc. Powodem tego była budowa systemu, która w przypadku zmiany konfiguracji jądra wymagała jego rekomplilacji. Obecnie większość systemów nie wymaga takich operacji i w związku z tym kompilator C nie jest standardowym elementem systemu.

(1.1) Etapy kompilacji programu w języku C
</p>

Proces tworzenia binarnego kodu wykonywalnego, zwany zwyczajowo kompilacją, składa się z kilku odrębnych etapów. Są to:

prekompilacja - stworzenie ostatecznego tekstu źródłowego programu poprzez włączenie plików wskazanych dyrektywą preprocesora #include, wykonanie podstawie makrodefinicji #define i opcjonalne usunięcie komentarzy,
kompilacja właściwa - wyszukanie tokenów (słowa kluczowe, operatory) i przekształcenie ich na wewnętrzną reprezentację; reprezentacja wewnętrzna jest następnie przekształcana na kod asemblera,
optymalizacja kodu asemblera - opcjonalna modyfikacja kodu asemblera w celu zwiększenia jego efektywności (np.: zmiana sposobu obliczania adresów względnych, eliminacja nieużywanych fragmentów kodu, optymalizacja przydziału rejestrów),
asemblacja - przetworzenie kodu asemblera w relokowalny kod w języku maszynowym, który umieszczany jest w pliku obiektowym (ang. object file); etap ten wykonywany jest przez program as (systemowy) lub gas (z pakietu GNU),
konsolidacja - konsolidator (ang. link editor), dokonuje szeregu operacji w celu stworzenia pliku binarnego z kodem wykonywalnym w tym :
  • przeszukanie standardowego zestaw bibliotek oraz bibliotek wskazanych w linii wywołania w celu włączenia do programu kodu funkcji niezdefiniowanych w modułach stworzonych przez użytkownika,
  • przypisanie kodu maszynowego do ustalonych adresów,
  • utworzenie wykonywalnego pliku binarnego w formacie ELF (ang. Executable and Linking Format)
<dl> <dt>Konsolidacja wykonywana jest przez program ld. </dl>
(1.2) Kompilator języka C

Polecenie wywołania kompilatora ma następującą postać :

gcc [opcje] plik [plik]...

Zawartość argumentu pliki określana jest na podstawie rozszerzenia, zgodnie z następującą konwencją przedstawioną w tablicy 5.1 (pokazano wybrane przykłady).

Tablica 5.1 Znaczenie rozszerzeń nazw plików</caption>
Rozszerzenie
</td> Zawartość</td> </tr> .c</td> tekst źródłowy w C</td> </tr> .i</td> kod źródłowy C po prekompilacji</td> </tr> .ii</td> kod źródłowy C++ po prekompilacji</td> </tr> .s</td> kod w języku asemblera</td> </tr> .h</td> plik nagłówkowy</td> </tr> .cc
.C
.cp
.cxx
.cpp
.c++
</td> tekst źródłowy w C++</td> </tr> .f
.fpp
.FPP
</td> tekst źródłowy w FORTRAN-ie</td> </tr> .o</td> kod relokowalny (wynik asemblacji)</td> </tr> .so</td> biblioteka dzielona</td> </tr> .a</td> biblioteka statyczna</td> </tr> </table>

Do najczęściej używanych opcji kompilatora gcc należą:

`-o nazwa` - powoduje utworzenie programu wynikowego o nazwie podanej przez użytkownika,
`-E` - powoduje zatrzymanie po etapie prekompilacji, wyniki są wypisywane na ekran,
`-S` - powoduje zatrzymanie po etapie generowania kodu asemblera, wyniki są umieszczane w pliku z rozszerzeniem .s,
`-c` - powoduje zatrzymanie po etapie asemblacji, wyniki są umieszczane w pliku z rozszerzeniem .o,
`-Dmakro ` -

użycie opcji jest równoznaczne z umieszczeniem linii #define makro naśpoczątku pliku zawierającego tekst źródłowy,

`-Umakro ` -

użycie opcji jest równoznaczne z umieszczeniem linii #undef makro naśpoczątku pliku zawierającego tekst źródłowy,

`-O` - powoduje wykonanie optymalizacji,
`-Opoziom` - powoduje wykonanie bardziej złożonej optymalizacji, o zakresie wskazanym przez parametr poziom,
`-g` - powoduje włączanie do pliku wynikowego informacji (numery linii, typ i rozmiar identyfikatorów, tablica symboli) umożliwiających śledzenie wykonywania programu wynikowego,
`-Ikatalog` - powoduje włączenie katalogu katalog do zestawu katalogów, w których należy poszukiwać plików nagłówkowych,
`-Lkatalog ` -

powoduje włączenie katalogu katalog do ścieżki poszukiwań bibliotek; w linii wywołania opcja ta musi poprzedzać opcję -l (wyjaśnienie poniżej),

`-lident` - polecenie dla konsolidatora powodujące, że biblioteka libident.a jest przeszukiwana w celu znalezienia kodu funkcji zewnętrznych,
`-pipe` -

powoduje, że kompilator gcc zamiast tworzyć pliki pośrednie do komunikacji pomiędzy programami wykonującymi kolejne etapy kompilacji wykorzystuje do tego celu łącza (ang. pipe); opcja nie działa gdy asembler nie może czytać danych ze strumienia wejściowego,

`-ansi ` -

tekst źródłowy musi być w 100% być zgodny z normą ANSI języka C.

`-traditional` -

kompilator toleruje starsze konstrukcje języka C.

Program gcc sam rozpoznaje kod źrodłowy stworzony w języku C lub C++ i wywołuje odpowiedni kompilator. Jednak w przypadku języka C++ wskazane jest by użytkownik samodzielnie wywoływał kompilator języka C++, który nosi nazwę g++.

Najprostszym sposobem użycia kompilatora jest wydanie polecenia kompilacji pojedynczego pliku zawierające kompletny tekst źródłowy programu, np. :

gcc main.c

Kompilator potraktuje wtedy zawartość pliku main.c jako tekst źródłowy w C (zgodnie z obowiązującą konwencją) i wykona wszystkie etapy kompilacji, aż do uzyskania kodu wykonywalnego, który zostanie umieszczony w pliku o standardowej nazwie a.out. Na etapie konsolidacji pod uwagę będzie wzięta tylko biblioteka libc.a, zawierająca kod standardowych funkcji języka C takich jak printf(), fopen(), itd. Wszystkie pliki pośrednie zostaną usunięte.

W bardziej skomplikowanym przypadku, gdy tekst źródłowy znajduje się nie w jednym lecz w kilku plikach, np.: main.c, data.c, input.c i output.c i dodatkowo programista użył funkcji matematycznych, polecenie kompilacji powinno wyglądać następująco :

gcc input.c output.c data.c main.c -lm

Kompilator dla każdego z plików wykona wszystkie etapy kompilacji, aż do uzyskania plików obiektowych main.o, data.o, input.o i output.o, a następnie dokona ich konsolidacji biorąc tym razem pod uwagę oprócz biblioteki libc.a także bibliotekę libm.a zawierającą kod funkcji takich jak sin(), cos(), i.t.d. Program wykonywalny znajdzie się w pliku a.out. Podobnie jak w poprzednim przykładzie wszystkie pliki pośrednie oprócz plików obiektowych zostaną usunięte.

Argumentami wywołania kompilatora gcc mogą być różne typy plików i możliwa jest sytuacja, w której każdy z argumentów jest innego typu (uzyskany został przez zatrzymanie kompilacji po innym etapie), np.:

gcc main.o data.s input.i output.c -lm



Pliki, które w czasie prekompilacji mają być włączone do tekstu programu (przy pomocy dyrektywy #include) muszą znajdować się w bieżącym katalogu (dotyczy to plików nagłówkowych stworzonych przez użytkownika) lub w standardowym katalogu instalacyjnym kompilatora (standardowe pliki nagłówkowe kompilatora. np. stdio.h). Jeśli programista chce umieścić własne pliki nagłówkowe w innym katalogu niż pliki z tekstem źródłowym, to by uczynić je dostępnymi dla kompilatora, musi w linii wywołania użyć opcji -Ikatalog, gdzie katalog jest nazwą tego katalogu (względną lub bezwględną). Przykładowo, jeśli pliki nagłówkowe zostały umieszczone w katalogu ../headers to wywołanie kompilatora ma postać:

gcc -I../headers input.c output.c data.c main.c

Podobnie jest w przypadku bibliotek. Konsolidator oczekuje, że biblioteki znajdują się w standardowym katalogu instalacyjnym. Jeśli użytkownik korzysta z innych bibliotek (np. stworzonych samodzielnie) to musi poinformować konsolidator przy pomocy opcji -Ldir, gdzie one się znajdują. Informacja ta musi poprzedzić opcję -l. Przykładowo, jeśli dodatkowa biblioteka nosi nazwę libusux.a i znajduje się w katalogu ../libs, to wywołanie kompilatora powinno mieć postać:

gcc main.c data.c input.c output.c -L../lib -lusux

Należy zwrócić uwagę na regułę nadawania bibliotekom nazw. Nakazuje ona, by nazwa bibioteki miała postać libident.a, gdzie pole ident może mieć długość od 1 do 7 znaków. W linii wywołania kompilatora, po opcji -l, podawana jest tylko część ident zamiast całej nazwy biblioteki, np.: -lm dla biblioteki libm.a.

(1.3) Funkcja main() w programach dla systemów typu Unix

Program wykonywany w środowisku systemu Unix otrzymuje od procesu, który go wywołał dwa zestawy danych: argumenty oraz środowisko. Dla programów stworzonych w języku C są one dostępne w postaci tablic zawierających wskaźniki, z których wszystkie oprócz ostatniego wskazują na napisy zakończone bajtami zerowymi. Ostatni wskaźnik ma zawsze wartość NULL. Dla innych języków, Pascal czy FORTRAN, przyjęto inne konwencje.

Tak więc, prototyp funkcji main() dla programu tworzonego dla systemu Unix jest następujący:

int main (int argc, char *argv[], char *envp[]);

Pierwszy parametr przechowuje liczbę argumentów przekazanych w wierszu poleceń, a drugi jest tablicą wskaźników do napisów będących tymi argumentami. Argumenty te mogą być całkowicie dowolnymi napisami.

Trzeci argument funkcji main() to wskaźnik na tablicę napisów tworzących środowisko. W przypadku środowiska istnieje wymaganie, aby każdy z napisów miał postać zmienna=wartość.

W celu uzyskania dostępu do środowiska można też wykorzystać globalną zmienną environ, która jest zadeklarowana w następujący sposób:

extern char *environ[];

Zmienna ta jest tablicą wskaźników do każdego elementu środowiska, czyli napisu o postaci zmienna=wartość. Deklaracja zmiennej environ zamieszczona jest w pliku nagłówkowym <unistd.h>. W związku z tym, programista musi jedynie włączyć ten plik do swojego programu dyrektywą #include.

Poniżej pokazano przykład prostego programu wypisującego wszystkie napisy składające się na definicję środowiska:

#include <unistd.h>
#include <stdio.h>
int main (int argc, char *argv[])
{
int i;
for (i=0; environ[i]!=NULL; i++)
printf("%s\n", environ[i]);
exit(0);
}

Dostęp do środowiska możliwy jest także za pośrednictwem następujących funkcji systemowych systemu Linux:

const char *getenv(const char *nazwa);
int putenv(const char *napis);
int setenv(const char *nazwa, const char *wartość, int zastąpienie);

W przypadku funkcji getenv() jedynym argumentem jest nazwa zmiennej środowiska, której wartość nas interesuje. Jeśli zmienna taka istnieje to otrzymamy wskaźnik do napisu stanowiącego jej wartości. Jeśli zmienna nie istnieje to wynikiem będzie NULL.

Funkcja putenv() umożliwia programiście zdefiniowanie wartości zmiennej środowiska lub też zmianę wartości zmiennej już istniejącej w środowisku. Przekazywany tutaj argument jest napisem o postaci zmienna=wartość.

Funkcja setenv(), zapożyczona z systemów Unix linii BSD, służy do modyfikacji środowiska. Umożliwia ona jednak oddzielne przekazanie nazwy zmiennej środowiska oraz jej wartości, co jest wygodniejszym rozwiązaniem z punktu widzenia programisty. Ponadto trzeci argument pozwala decydować czy należy zmienić wartość już istniejącej zmiennej. Tak więc, jeśli zastąpienie ma wartość 0 to istniejąca zmienna nie będzie modyfikowana.

2. program śledzący

W pakiecie GNU dostępny jest program śledzący (nazywany też czasem uruchomieniowym) gdb. Jego wywołanie jest następujące:

gdb [opcje] program [core]

lub

gdb [opcje] program [process_ID]

Program umożliwia krokowe wykonywanie programu, ustawianie pułapek, śledzenie wartości zmiennych i wyrażeń i inne typowe dla programów uruchomieniowych operacje. Oczywiście uruchamiany program (a dokładnie każdy plik zawierający tekst źrodłowy) musi być skompliwany z użyciem opcji -g. Po wywołaniu program gdb komunikuje się z użytkiem za pomocą prostej powłoki takiej jak np. bash. Jedną z komend jest help, która pozwala uzyskać informacje o dostępnych poleceniach i ich składni w aktualnej wersji programu. Wybrany zestaw poleceń prgramu gdb zaprezentowany jest w tablicy 5.2.

<font size="+1">Polecenie (skrót polecenia)</span> <font size="+1">Opis</span>
`attach (at)` Dołącza gdb do działającego procesu. Jedynym argumentem jest PID procesu, do którego chcemy się dołączyć. Polececenie powoduje zatrzymanie działąjącego procesu, przerywając funkcję sleep() lub dowolną inną przerywalną funkcję systemową.
`backtrace (bt)` Wypisuje zawartość stosu.
`break (b)` Ustawia pułapkę (ang. breakpoint), którą można określić podając jako argument nazwę funkcji, numer wiersza kodu w bieżącym pliku, parę `nazwa_pliku:numer_wiersza` lub dowolny adres komórki pamięci. Każdej pułapce jest nadawany unikalny numer referencyjny.
`clear` Usuwa pułapkę. Przyjmuje takie same argumenty jak `break`.
`condition` Zmienia pułapkę o podanym numerze referencyjnym w taki sposób, że przerwanie następuje tylko w przypadku spełnienia podanego warunku.
`delete` Usuwa pułapkę o podanym numerze referencyjnym.
`detach` Odłącza gdb od aktualnie przyłączonego procesu.
`display` Wyświetla wartość podanego wyrażenia przy każdym zatrzymaniu działania programu. Każde zdefiniowane wyrażenie otrzymuje unikalny numer referencyjny.
`help` Pomoc. Wywołane bez argumentów podaje listę dostępnych tematów. Wywołane z argumetem (np. nazwą polecenia) podaje informacje szczegółowe (np. o danym poleceniu), np. `help set`.
`jump` Powoduje wykonanie skoku pod podany adres. Działanie jest kontynuowane od podanego adresu. Adres może być podany jako numer wiersza lub adres komórki pamięci.
`list (l)` Wywołane bez argumetów wypisuje 10 wierszy kodu źródłowego otaczających bieżący adres. Kolejne wywołania wyświetlają kolejne 10 wierszy. Podanie argumentu `[nazwa_pliku:]numer_wiersza` powoduje wypisanie kodu we wskazanym pliku wokół wskazanego numeru wiersza.
Podanie zakresu wierszy zamiast pojedynczego numeru powoduje wypisanie tych wierszy.
`next (n)` Wykonuje program do następnego wiersza kodu źródłowego bieżącej funkcji. Nie wchodzi do kodu wywoływanych funkcji.
`nexti` Przechodzi do następnej instrukcji języka maszynowego. Nie wchodzi do kodu wywoływanych funkcji.
`print (p)` Wypisuje wartość wyrażenia w czytelnej postaci, tzn. jeśli wskazany obiekt jest np. napisem to wypisany będzie napis, jeśli obiekt jest np. strukturą to wypisane zostaną jej pola.
`run (r)` Uruchamia od początku aktualnie śledzony program. Argumenty polecenia są argumentami wywołania programu.

Argumety wywołania śledząnego programu można również zdefiować przy pomocy polecenia `set args`.

`set` Umożliwia zmianę wartości zmiennych, np. `set a = 3.14.`
`step (s)` Wykonuje program instrukcja po instrukcji dopóki nie osiągnie nowego wiersza w kodzie źródłowym.
`stepi` Wykonuje jedną instrukcję języka maszynowego. Wchodzi do kodu wywoływanych funkcji.
`undisplay` Kończy wyświetlanie wyrażenia wskazanego przez numer referencyjny. Wywołane bez argumentu powoduje zakończenie wyświetlania wartości wszystkich zdefiniowanych wyrażeń.
`whatis` Wypisuje typ wyrażenia podanego jako argument.

Śledzenie i wyszukiwanie błędów w złożonym programie przy pomocy samego tylko programu gdb nie jest zbyt wygodne ze względu na brak graficznego interfejsu użytkownika jaki zwykle posiadają zintegrowane środowiska dostarczane na platformy MS Windows czy też komercyjne oprogramowanie dla systemow typu Unix. Interfejsu takiego dostarcza program o nazwie DDD (produkt z pakietu GNU).

3. Program make

</p>

Program make jest standardowym narzędziem dostępnym w środowisku systemu Unix ułatwiającym programiście pracę nad tworzeniem programu. Podstawową funkcją programu make jest zarządzanie kompilacją zbioru tekstów źródłowych składających się na dany program przy zachowaniu minimalnego kosztu operacji. Oznacza to, że po zmodyfikowaniu części tekstów źródłowych, kompilacji zostaną poddane wyłącznie te moduły, które są od nich uzależnione. O tym, jakie polecenia należy wykonać, aby z tekstów źródłowych otrzymać programy wykonywalne decydują stworzone przez użytkownika reguły transformacji. Reguły te umieszcza się w pliku sterującym (ang. makefile). Plik sterujący powinien znajdować się w katalogu, który zawiera teksty źródłowe danego programu. Uruchomienie procesu kompilacji następuje poprzez wywołanie programu make. Program odczytuje wtedy całą zawartość pliku sterującego i na podstawie jego treści, jak również na podstawie wbudowanych reguł transformacji, treści linii wywołania, wartości zmiennych środowiska, czasu systemowego oraz czasu modyfikacji plików tworzy wynikowy ciąg poleceń prowadzący do uzyskania żądanego celu. Następnie polecenia te są wykonane, przy czym wykonanie każdego z nich poporzedzone jest wypisaniem jego treści.

(3.1) Parametry wywołania
</p>

Wywołanie programu make ma następującą postać:

make [opcje] [makrodefinicje] [-f plik_sterujący] [cel]

Najczęściej stosowane opcje to:

-d - włącza tryb szczegółowego śledzenia,
-f plik_sterujący - umożliwia stosowanie innych niż standardowe nazw plików sterujących,
-n - powoduje wypisanie poleceń na ekran zamiast ich wykonania,
-p - powoduje wypisanie makrodefinicji i reguł transformacji,
-s - wyłącza wypisywanie treści polecenia przed jego wykonaniem,
-i - powoduje ignorowanie błędów kompilacji (stosować z ostrożnością!).

W najprostszym przypadku linia polecenia zawiera tylko słowo make. Program próbuje wtedy odczytać polecenia z pliku sterującego o jednej z nazw: makefile, Makefile lub MakeFile. Jeśli w bieżącym katalogu nie ma pliku o takiej nazwie to zgłaszany jest błąd. Jeśli plik sterujący nosi inną nazwę to należy użyć wywołania make -f plik_sterujący. W linii wywołania można zdefiniować nowe lub zmienić istniejące już makrodefinicje, np.: make "CC=gcc". Można też polecić osiągnięcie innego celu niż domyślny, np.: make all, make clean lub make install.

(3.2) Plik sterujący
</p>

Plik sterujący zawiera definicje relacji zależności, które mówią w jaki sposób i z jakich elementów należy stworzyć cel (program, bibliotekę, lub plik obiektowy) i wskazują pliki, których zmiany implikują wykonanie powtórnej kompilacji poszczególnych celów. Plik sterujący może również zawierać zdefiniowane przez programistę reguły transformacji.

Program make, stosując wbudowane reguły transformacji, potrafi samodzielnie wykonać proste sekwencje poleceń, ale potrzebuje wskazówek programisty by utworzyć bardziej skomplikowane cele, takie jak program wykonywalny. Programista dokonuje tego poprzez umieszczenie w pliku sterującym definicji określających, z jakich elementów należy tworzyć program wynikowy iśjak te elementy zależą od innych obiektów np. plików nagłówkowych. Wszystkie relacje zależności pomiędzy obiektami opierają się na porównywaniu czasu ostatniej modyfikacji plików oraz na sprawdzaniu czy dane pliki istnieją. Ogólna postać definicji, jaką można umieścić w pliku sterującym, jest następująca:

`<font color="#0000cd">cel1 [cel2...] :[:] [lista_obiektów_odniesienia]
[<TAB> polecenia] [#komentarz]
[<TAB> polecenia] [#komentarz]
</font>`

gdzie <TAB> oznacza znak tabulacji .

Każde polecenie umieszczone w definicji wykonywane jest w oddzielnej kopii powłoki. Standardowo jest to Bourne shell (sh). Jeśli użytkownik chce wywoływać inny rodzaj powłoki, to musi przedefiniować zmienną SHELL. Złożone polecenia powinny być zawarte w pojedynczym wierszu, gdyż tylko wtedy wykonane będą przez tą samą kopię powłoki. Elementarne składniki polecenia należy umieścić obok siebie, oddzielając je średnikami, a jeśli cały wiersz jest zbyt długi, to należy rozbić go na kilka stosując znak kontynuacji- \, jak pokazano na poniższych przykładach:

prog : source/main.o source/input.o source/output.o
     cd source; $(CC) -o prog main.o input.o output.o


prog : source/main.o source/input.o source/output.o
&n    cd source; \
&n    $(CC) -o prog main.o input.o output.o

Jeśli użytkownik chce zapobiec wypisywaniu treści polecenia podczas jego wykonywania to może umieścić @ jako pierwszy znak w poleceniu.Poniższy przykład pokazuje typową definicję relacji zależności:

prog : main.o input.o output.o
 
     $(CC) -o prog main.o input.o output.o

Mówi ona, że cel prog zależy od trzech obiektów odniesienia: main.o, input.o, output.o. Jeśli którykolwiek z tych obiektów ulegnie zmianie (tzn. zostanie na nowo skompilowany), to cel prog należy utworzyć ponownie. Jeżeli obiekt odniesienia nie istnieje,to zostanie stworzony przy zastosowaniu wbudowanej reguły transformacji (lub innej definicji podanej przez programistę wśpliku sterującym).W drugim wierszu definicji umieszczone jest polecenie, które należy wykonać by zbudować cel prog. W tym przypadku, należy wywołać kompilator z opcją zmieniającą nazwę pliku wynikowego na nazwę celu i wykonać konsolidację plików main.o, input.o, output.o z biblioteką standardową.
Programista nie musi umieszczać w pliku sterującym instrukcji powodujących ponowne tworzenie plików obiektowych w przypadku zmiany tekstów źródłowych, gdyż jest to wykonywane automatycznie. Musi natomiast poinformować program make o wszystkich plikach włączanych do tekstów źródłowych dyrektywą #include, aby make mógł zareagować na zmiany zawartości tych plików (wyjątkiem od tej zasady są standardowe pliki nagłówkowe kompilatora, które nigdy nie ulegają zmianom). Informacja ta podawana jest w następującej postaci:

main.o : global.h
main.o input.o : czytaj.h
output.o main.o : global.h data.h matrix.h

Definicje te mówią, że w przypadku zmiany jakiegokolwiek pliku (plików) po prawej stronie ":" należy od nowa stworzyć plik (pliki) wymienione po lewej stronie ":", stosując wbudowane reguły transformacji. Program make inaczej interpretuje definicje zawierające separatory ":" oraz "::" . W przypadku definicji zawierającej ":" cel jest budowany jeśli:

  1. dowolny obiekt odniesienia jest "młodszy" od celu,
  2. cel nie istnieje.

Poniższy przykład, pokazuje użycie separatora"::" . Dwie definicje celu a umieszczone w jednym pliku sterującym możliwiają wykonania kompilacji "warunkowej":

a :: a.sh
      cp a.sh a

a :: a.c
      cc -o a a.c

Definicja pierwsza aktywna jest, gdy w bieżącym katalogu znajduje się plik a.sh, a druga jeśli plik a.c. W przypadku, gdy istnieją oba pliki, wykonana może być zarówno jedna z definicji lub też obie zależnie od tego czy cel jest "starszy" od a.sh czy od a.c czy jednocześnie od a.sh i a.c.
W następnym przykładzie użycia definicji z "::" cel ma nazwę pokrywającą się z nazwą katalogu zawierającego jego teksty źródłowe (sytuacja często spotykana w praktyce). Gdyby użyto definicji z pojedynczym "::" to cel nigdy nie zostałby utworzony, gdyż istnieje już w bieżącym katalogu obiekt o tej samej nazwie (to, że jest katalogiem nie ma znaczenia dla programu make).

program ::
      cd program; cc program.o -o program

Kolejność umieszczenia definicji w pliku sterującym nie wpływa na kolejność wykonywania poleceń przez program make. To, jakie czynności i w jakiej kolejność należy wykonać określa sam program make na podstawie wewnętrznej struktury danych (utworzonej w wyniku analizy całej treści pliku sterującego) i czasu ostatniej modyfikacji plików. Wyjątkiem od tej zasady jest określenie, który z celów zdefiniowanych w pliku sterującym będzie realizowany domyślnie, czyli po uruchomieniu programu make bez podania nazwy celu. Standardowo za domyślny przyjmowany jest pierwszy zdefiniowany cel.

Makrodefinicja jest to zmienna używana wścelu sparametryzowania reguł, dzięki czemu stają się one bardziej przejrzyste i łatwiejsze do modyfikacji. Deklaracja makrodefinicji to linia zawierająca znak równości i nie zaczynająca się od kropki ani tabulatora, np.:

OBJECTS = main.o data.o input.o output.o
HDRS = ../headers ../includes
CFLAGS= -g -DAUX -I$(HDRS)
CC = gcc
LIBS = -lusux

W celu odwołania się do zawartości makrodefinicji używa się konstrukcji $(nazwa_makrodef) lub ${nazwa_makrodef}. Jeśli nazwa makrodefinicji zawiera tylko jeden znak to można pominąć nawiasy. Stosowanie makrodefinicji daje możliwość łatwego wprowadzania modyfikacji takich jak zmiana opcji kompilatora, a także przystosowania pliku sterującego do nowego środowiska, np.: zmiana ścieżek dostępu do plików nagłówkowych.
Istnieje zestaw predefiniowanych makrodefinicji, do których programista może się odwoływać w pliku sterującym. Wartości tych makrodefinicji mogą być zmienione, przyczym nowe wartości widoczne są tylko w konkretnym pliku sterującym. Niektóre z nich zostały pokazane w tablica 5.2.

Tablica 5.2 Predefiniowane makrodefinicje</caption> Makrodefinicja </td> Wartość predefiniowana</td> Objaśnienie</td> </tr> AR</td> ar</td> program zarządzający bibliotekami</td> </tr> AS</td> as</td> asembler</td> </tr> ASFLAGS</td>
</td> opcje programu AS</td> </tr> CC</td> cc</td> kompilator języka C</td> </tr> CFLAGS</td>
</td> opcje programu CC</td> </tr> LD</td> ld</td> konsolidator</td> </tr> LDFLAGS</td>
</td> opcje programu LD</td> </tr> </table>

Istnieje również zbiór wbudowanych makrodefinicji, których wartości są określane w trakcie wykonywania poleceń zapisanych w definicjach (tzw. makrodefinicje dynamiczne). Stosowanie tych makrodefinicji znacznie ułatwia tworzenie definicji i reguł transformacji.W definicjach można stosować następujące makra :

$@ - aktualnie tworzony cel. W poniższym przykładzie $@ przyjmuje wartość prog:

prog : main.o input.o output.o
      $(CC) -o $@ main.o input.o output.o


$? - lista nieaktualnych obiektów odniesienia w stosunku do bieżącego celu. W poniższym przykładzie $? jest listą tych plików obiektowych spośród wszystkich z $(LIB_OBJ), które zostały zmodyfikowane po ostatnim utworzeniu biblioteki libusux.a:

libusux.a : $(LIB_OBJ)
      $(AR) -rv $@ $?


$ < - nieaktualny obiekt odniesienia powodujący wywołanie reguły, np.:

.c.o:
      $(CC) $(CFLAGS) -c $(<)


$ * - wspólny prefix celu i obiektu odniesienia, np.:

c.o:
      $(CC) $(CFLAGS) -c $*.c


Do powyższych makrodefinicji można zastosować modyfikatory D (ang. directory) oraz F (ang. file), umożliwające odzyskanie z nazwy celu części opisującej katalog i części będącej właściwą nazwą, np:

$(@D) - część "katalogowa" nazwy bieżącego celu

$(@F) - część "plikowa" nazwy bieżącego celu

Istnieją również makrodefinicje, które umieszczać wolno wyłącznie na liście obiektów odniesienia (po ":" lub "::"). Są to :

$$@ - kolejny cel (obiekt z lewej strony znaku : ), np.:

prog1 prog2 prog3 prog4 : [email protected]
     $(CC) -o $@ $?

Definicja ta powoduje wykonanie dla każdego z celów prog1, prog2, prog3, prog4 następującej sekwencji operacji :

  1. ustawienie bieżącego celu na progn
  2. ustawienie bieżącego obiektu odniesienia na progn.c,
  3. wywołanie kompilatora jeśli progn.c jest "młodszy" niż progn i utworzenie celu progn.


W chwili wywołania program make odczytuje zawartość zmiennych środowiska i dodaje je do zbioru makrodefinicji. Jeżeli nazwy makrodefinicji użytych wewnątrz pliku sterującego pokrywają się z nazwami użytymi w linii wywołania i/lub z nazwami zmiennych środowiska, to ostateczna wartość makrodefinicji wynika z następującej hierarchii (w kolejności od najwyższego do najniższego priorytetu):

  1. wartość nadana w linii wywołania,
  2. wartość zdefiniowana w pliku sterującym,
  3. wartość zmiennej środowiska,
  4. predefiniowana wartość makrodefinicji.

Program make posiada wewnętrzną tablicę reguł transformacji, które są wykorzystywane podczas kompilacji. Użytkownik może dodatkowo zdefiniować w pliku sterującym własne reguły, które przysłonią reguły wbudowane. Reguły określają w jaki sposób dokonać przejścia od plików zawierających teksty źródłowe do plików obiektowych i do programów wynikowych. Typy plików określane są na podstawie przyrostków (rozszerzeń nazw). Zdefiniowany jest standardowy zestaw tych przyrostków i skojarzonych z nimi typów plików, z których część zaprezentowano przy omawianiu kompilatora gcc.

Istnieją dwa typy reguł transformacji:

  1. dwuprzyrostkowe
  2. jednoprzyrostkowe.

Reguły dwuprzyrostkowe określają sekwencję poleceń jakie trzeba wykonać, aby przejść od pliku o typie skojarzonym z pierwszym przyrostkiem do pliku o typie skojarzonym z drugim przyrostkiem. Typowym przykładem jest reguła definiująca w jaki sposób uzyskać plik obiektowy z pliku zawierającego kod źródłowy w C:

.c.o:
      $(CC) $(CFLAGS) -c $(<)

Reguła ta mówi, że należy wywołać kompilator języka C z opcją zatrzymującą kompilację po etapie asemblacji, podając jako argument plik z tekstem źródłowym w C. Podobną postać mają reguły określające transformacje plików zawierających teksty źródłowe dla asemblera :

.s.o:
      $(AS) $(ASFLAGS) -o $(@) $(<)

Reguły jednoprzyrostkowe określają sekwencję polece ń jakie należy wykonać, aby przejść od pliku o typie skojarzonym z danym przyrostkiem do pliku bez przyrostka czyli np.: programu wynikowego. Typowy przykład to reguła definiująca sposób uzyskania programu, przyjmując za wejście plik z tekstem źródłowym w C :

.c:
      $(CC) $(CFLAGS) $(LDFLAGS) -o $(@) $(<)

Predefiniowane cele są w rzeczywistości wbudowanymi regułami, które są uaktywniane poprzez włączenie ich do pliku sterującego. Włączenie ich modyfikuje standardowy sposób działania programu make. Cele te są przedstawione w tablicy 5.3:

Tablica 5.3 Predefiniowane cele</caption> Cel </td> Objaśnienie</td> </tr> .DEFAULT:</td> Umieszczenie celu powoduje, że w przypadku, gdy brak jest definicji opisującej sposób zbudowania celu (a cel musi być osiągnięty) to stosowane są polecenia umieszczone w definicji celu .DEFAULT.</td> </tr> .IGNORE:</td> Umieszczenie tego celu jest równoznaczne z wywołaniem programu make z opcją -i.</td> </tr> .MAKESTOP [n]:</td> Umieszczenie tego celu powoduje zignorowanie całej treści pliku sterującego. Opcjonalnie można podać kod zakończenia (argument n), który standardowo ma wartość 0. Użycie celu .MAKESTOP umożliwia zaniechanie wykonywania wybranego pliku sterującego w celu szybkiego przejścia przez wielopoziomową strukturę wywołań programu make.</td> </tr> .PRECIOUS:</td> Umieszczenie tego celu zmienia standardowe zachowanie programu make w sytuacji przerwania jego pracy, powoduje zaniechanie usuwania utworzonych do tego czasu programów i plików obiektowych.</td> </tr> .SILENT:</td> Umieszczenie tego celu jest równoznaczne z wywołaniem programu make z opcją -s.</td> </tr> </table>

W pliku sterującym można umieścić instrukcję include (lub Include) o następującej składni:
`<font color="#0000cd">include nazwa_pliku
</font>`

Słowo include musi być umieszczone na początku linii zaczynając od pierwszej kolumny. Plik wskazany przez instrukcję include jest włączany do treści bieżącego pliku sterującego, po etapie wykonania podstawień makrodefinicji w pliku bieżącym. Jeśli nie jest możliwe odczytanie zawartości wskazanego pliku to program make przerywa pracę.

4. Biblioteki statyczne

Biblioteką statyczną (ang. library archive) jest archiwum plików obiektowych zawierające nagłówek, w którym znajdują się informacje m.in. o nazwach i położeniu wewnątrz archiwum poszczególnych obiektów oraz tablicę symboli bibliteki. Taka organizacja biblioteki zwiększa efektywność jej przeszukiwania podczas konsolidacji. Wraz z systemem dostarczane są jego standardowe biblioteki takie jak libc.a czy libm.a. Oprócz nich istnieją biblioteki zawierające zestaw funkcji przeznaczonych do specjalnych zastosowań jak np. libcurses.a umożliwiająca programiście programową obsługę terminali. Niezależnie od bibliotek dostępnych w danym systemie każdy użytkownik może tworzyć własne biblioteki, które mogą być następnie używane w taki sam sposób jak systemowe. Do tego celu używany jest program ar. Zasady posługiwania się tym programem są bardzo podobne do sposobu korzystania z programu tar. Wywołanie ar ma następującą składnię :

ar [opcje] archiwum plik1 ... plikn

Argument archiwum jest nazwą biblioteki, a plik1 plikn są plikami obiektowymi, z których należy stworzyć bibliotekę, lub które należy wyekstrahować lub usunąć z biblioteki. Najczęściej używane opcje to:

-d - usuwanie z archiwum wskazany plik,
-q - dodawanie pliku na koniec archiwum (UWAGA! nie sprawdza czy dany plik jest już w archiwum),
-r - zamiana (lub dodanie) wskazanego pliku w archiwum; jeśli pliku nie ma w archiwum to następuje jego dodanie, a w przypadku gdy nie istnieje archiwum - jego utworzenie,
-u - używana razem z -r powoduje, że zamiana następuje tylko wtedy, gdy data modyfikacji pliku w archiwum jest wcześniejsza niż data modyfikacji pliku podanego jako argumentu,
-s - ponowne utworzenie tablicy symboli biblioteki; umożliwia odtworzenie tablicy symboli po jej usunięciu programem strip,
-t -

wypisanie zawartości archiwum; zwykle używana razem z opcją -v,

-x -

ekstrakcja wskazanego lub wszystkich plików z archiwum (nie niszczy archiwum),

-v - wyświetlanie bardziej szczegółowych informacji podczas działania.

Przykładowo, jeśli programista chce utworzyć bibliotekę libusux.a z następujących plików: funkcja1.o, funkcja2.o i funkcja3.o, to polecenie może mieć postać:

ar -rv libusux.a funkcja1.o funkcja1.o funkcja1.o

lub

ar -ruv libusux.a funkcja1.o funkcja1.o funkcja1.o

Jeśli biblioteka libusux.a nie istniała wcześniej, to w obu przypadkach zostanie utworzona. Opcja -r stanowi zabezpieczenie przed sytuacją, w której do istniejącej już biblioteki dopisywane były by za każdym razem nowe wersje plików funkcja1.o, funkcja2.o i funkcja3.o. Sprawdzenie zawartości biblioteki możliwe poprzez wydanie następujące polenia:

ar -tv libusux.a

5. Biblioteki dzielone

Biblioteki dzielone mają szereg zalet w porównaniu z bibliotekami statycznymie:

  1. system dzieli kod wykonywalny pomiędzy wszystkie procesy korzystające z biblioteki dzielonej, dzięki czemu zmniejsza się zajętość pamięć systemu,
  2. kod biblioteki dzielonej nie jest kopiowany do plików wykonywalnych, a więc tylko jedna kopia kodu znajduje się na dysku,
  3. wykrycie błędu w bibliotece dzielonej wymaga wymiany tylko tej biblioteki bez konieczności rekompilacji wykorzystających ją programów.

Autorami powyższego artykułu są mgr inż. A. Wielgus i dr Z. Jaworski. Drobnych zmian i poprawek dokonał Ł. Fronczyk
W dziale źródła/C++ znajdują się przykładowe programy do tego i pozostałych artykułów z tego cyklu.
Wszelkie pytania proszę kierować na adres [email protected]

0 komentarzy