Subsections

   
10 LAB: Budowanie programów w środowisku GNU

WPROWADZENIE

Temat: Kompilacja programu przy pomocy kompilatora GCC

Kompilator wywołuje się przy pomocy polecenia gcc. Ogólna postać to:

gcc [opcje] pliki-wejsciowe [-o plik-wykonywalny]
W związku z tym minimalna postać to:
gcc pliki-wejsciowe
W takim wypadku powstanie plik wykonywalny a.out.

Temat: Wykrywanie usterek w kodzie

Aby zwiększyć niezawodność kodu kompilator potrafi wykrywać usterki w kodzie, co sygnalizuje ostrzeżeniami (ang. warning). Aby włączyć wykrywanie wszystkich istotnych ostrzeżeń należy kompilować tak:
$ gcc -Wall hello.c -o hello

Aby zapewnić pełną zgodność ze standardem ANSI C należy kompilować tak:

$ gcc -Wall -ansi -pedantic hello.c -o hello
Powyższe opcje powinny być zawsze obecne przy kompilacji kodu!

Temat: Dołączanie bibliotek do własnych programów

W celu dołączenia zewnętrznych bibliotek należy je podać w linii poleceń kompilatora przez opcję -lnazwa.

Niestandardowe (własne) katalogi z plikami nagłówkowymi podaje się przez -I.

Niestandardowe (własne) katalogi z plikami bibliotek podaje się przez -L.

Temat: Kompilacja programu wielomodułowego, linkowanie modułów

Składający się z N modułów program można skompilować:
$ gcc -Wall -ansi -pedantic modul1.c modul2.c ... modulN.c -o program

Tym niemniej w ogólnym przypadku korzystniejsza jest kompilacja równoległa, po której następuje linkowanie modułów:

$ gcc -c -Wall -ansi -pedantic modul1.c -o modul1.o
$ gcc -c -Wall -ansi -pedantic modul2.c -o modul2.o
...
$ gcc -c -Wall -ansi -pedantic modulN.c -o modulN.o
$ gcc -Wall -ansi -pedantic modul1.o modul2.o ... modulN.o -o program
Dlaczego jest to lepsze rozwiązanie?

Temat: Make, wprowadzenie

Make jest narzędziem wspomagającym proces budowania wielomodułowego kodu ze złożonymi zależnościami pomiędzy plikami źródłowymi. W ogólnym przypadku jest przydatny tam, gdzie ma miejsce tworzenie plików wynikowych ze źródłowych, a zmiany w jednych pociągają rekurencyjnie zmiany w kolejnych.

Make działa zawsze w obrębie bieżącego katalogu. Do działania potrzebuje pliku konfiguracyjnego, o domyślnej nazwie Makefile.

Ogólne zasady dotyczące budowy Makefile:

Temat: Make, elementy zaawansowane

Niektóre z reguł w pliku Makefile nie muszą być związane z samą kompilacją, ale czynnościami pomocniczymi. Są to tak zwane cele ,,pozorne'' (ang. phony).

Wszystkie cele pozorne muszą być zadeklarowane jako takie, robi się to poprzez specjalny cel .PHONY którego prerekwizytami są wszsytkie cele pozorne w pliku.

Cele pozorne mają ważną cechę: nigdy nie są zaktualizowane (ang. up to date) (tzn. będą budowane przy każdym ich wywołaniu, bez względnu na ich preprekwizyty!).

W miarę rozbudowywania programu plik Makefile będzie powiększany i aktualizowany. Przy dopisywaniu kolejnych modułów można zauważyć pewne niedogodności w obecnym pliku:

Dlatego też używa się zmiennych. (przykłady w ćwiczeniu poniżej)

Temat: Konwencje make, nie tylko kompilacja

Make może zarządzać dowolnym procesem budowania plików, nie tylko kompilacją. W używanych plików występował jeden cel nie związany w ogóle bezpośrednio z kompilatorem, tzn. clean.

Wg konwencji plik Makefile powinien mieć przynajmniej 3 cele:

all
budujący cały program, cel domyślny,
clean
czyszczący kod po kompilacji
install
instalujący program po kompilacji
Często występuje też cel dist powodujący stworzenie archiwum kodu.

make można też uruchomić w trybie silent w którym nie wyświetla poleceń które wykonuje, a jedynie komunikaty wypisywane przez echo. W tym celu należy podac opcję -s

Przydatna jest też opcja -k która próbuje zrealizować jak najwięcej celów nawet w przpadku napotkania błędów, a także opcja -j N pozwalająca na równoległe uruchomienie N instancji make i zrównoleglenie procesu kompilacji, co jest przydatne w przypadku maszyn wieloprocesorowych.

Zadając opcję -f plik można podać plik o innej niż Makefile nazwie.

Temat: Dokumentacja kodu

Dobra dokumentacja jest krytyczna w procesie rozwijania programów. Istnieją narzędzia wspomagające tworzenie takiej dokumentacji.

Generują one dokumentację w sposób automatyczny, na podstawie komentarzy w kodzie źródłowym programu. Warunkiem ich użycia jest pisanie komentarzy zgodnie z określonymi regułami. Przykładem takiego programu jest Doxygen, dostępny pod adresem: http://www.doxygen.org.

ĆWICZENIA

   
10.1 Ćwiczenie: Kompilacja programu przy pomocy kompilatora GCC

W edytorze tekstu proszę wpisać kod źródłowy prostego programu hello.c:
#include <stdio.h>

int
main (void)
{
  printf ("Hello, world!\n");
}
Następnie skompilować go:
$ gcc hello.c -o hello
i uruchomić:
$ ./hello

10.2 Ćwiczenie: Wykrywanie usterek w kodzie

Proszę usunąć usterkę w kodzie z ćwiczenia 10.1.

Proszę spowodować błąd w programie usuwając średnik z kodu programu.

Niektóre usterki, o charakterze ostrzeżeń mogą w praktyce powodować błędy w czasie pracy programu. Proszę dopisać do funkcji main() linię:

printf ("2+2 jest %f\n", 4);
Co sygnalizuje kompilator?

Proszę uruchomić skompilowany program; jaki jest wynik?

Na czym polega błąd? Jak go usunąć?

Można wymusić traktowanie wszystkich ostrzeżeń jak sytacji błednych, przez parametr kompilatora: -Werror.

10.3 Ćwiczenie: Dołączanie bibliotek do własnych programów

W ramach ćwiczenia dodać do funkcji main() linię odwołującą się do funkcji ze standardowej biblioteki matematycznej:
printf ("2^3 jest %f\n", pow(2,3));

Jaki jest wynik kompilacji?

Program należy kompilować z odwołaniem (dla linkera) do biblioteki matematycznej:

$ gcc -Wall -ansi -pedantic hello.c -lm -o hello

Co należy dopisać na początku kodu dla bezbłednej kompilacji?

10.4 Ćwiczenie: Kompilacja programu wielomodułowego, linkowanie modułów

W ćwiczeniu będzie wykorzystywany przygotowany projekt, będący rozbudowaną wersją modułowego programu HelloWorld.

  1. Proszę pobrać plik archiwum projektu lab-make-all.tar.gz
  2. Archiwum należy skopiować do swojego katalogu domowego i rozpakować:
    $ tar xvzf lab-make-all.tar.gz
    
  3. Należy przejść do nowopowstałego katalogu lab-make i w nim ćwiczyć.
  4. W czasie ćwiczeń nie powinny być wymagane jakiekolwiek modyfikacje samego kodu źródłowego w C!
  5. Proszę przyglądnąć się kodowi projektu! Projekt składa się z 3 modułów: głównego i 2 pomocniczych, z których każdy ma swój plik nagłówkowy (proszę zwrócić uwagę na budowę nagłówków!), poza tym jest jeden główny plik nagłówkowy, używany przez wszystkie moduły.

Proszę skompilować program na oba ze sposobów (jedno, lub kilka wywołań kompilatora dla poszczególnych modułów).

Następnie należy usunąć z pliku hw_main.c wywołanie nagłówka hw_hello.h, skompilować i zlinkować program. Kiedy pojawiają się ew. ostrzeżenia?

Co się dzieje po usunięciu opcji -Wall?

Przy którym wywołaniu należy dodać odwołanie do biblioteki matematycznej -lm? Dlaczego?

Co się stanie po usunięciu wywołania nagłówka <math.h> z pliku hw_power.c? Na jakim etapie kompilacji?

Należy powtórzyć kompilację kodu, dla uproszczenia w 1. wariancie, dodając do linii wywołującej kompilator opcję -v.

Prześledzić kompilację z i bez używanych poprzednio opcji.

Jakie są jej poszczególne etapy?

UWAGA: Ćwicząc w kolejnych ćwiczeniach z Make warto samodzielnie rozbudowywać pliki Makefile. Tym niemniej w.w. archiwum zawiera dla uproszczenia wszystkie rozwiązania, tak więc osoby powolne, mniej zdolne, leniwe (?!) mogą się nimi posiłkować, nie zwalnia ich to jednak od rozumienia ćwiczeń!

10.5 Ćwiczenie: Make, podstawy

Należy zbudować prosty plik dla make, pozwalającący na zbudowanie każdego z modułów i całego programu. Plik należy zapisać pod nazwą Makefile.

UWAGA! proszę pamiętać o wpisywaniu znaku tabulacji w odpowiednich liniach!

hello: hw_hello.o hw_power.o hw_defs.h hw_hello.h hw_power.h
    gcc -Wall -ansi -pedantic -lm hw_main.c hw_hello.o hw_power.o -o hello
    echo hello: zbudowane!

hw_hello.o: hw_hello.c hw_defs.h 
    gcc -c -Wall -ansi -pedantic hw_hello.c

hw_power.o: hw_power.c hw_defs.h 
    gcc -c -Wall -ansi -pedantic hw_power.c

Teraz można zbudować program pisząc po prostu:

$ make

10.6 Ćwiczenie: Kiedy przydaje się Make?

Make przydaje się w wielu sytacjach, szczególnie wtedy gdy plik są powiązane złożonymi rekurencyjnymi zależnościami i modyfikacja jednego wymusza zmianę (rekompilację) innych.

   
10.6.0.1 Przykład 1

Zmiana/modyfikacja pliku hw_power.c powoduje, że:
  1. moduł hw_power musi być zrekompilowany, a
  2. cały program hello zrelinkowany
Proszę zmodyfikować plik hw_power.c, wczytując go do edytora i nagrywając, ew. można tylko zmienić jego datę modyfikacji pisząc:
$ touch hw_power.c

Proszę teraz uruchomić:

$ make
Jakie polecenia zostały uruchomione przez make? Co z 2. modułem (hw_hello)?

Proszę jeszcze raz uruchomić make. Co teraz się dzieje? Dlaczego?

Proszę zmodyfikować plik wynikowy hello, pisząc:

$ touch hello
Proszę jeszcze raz uruchomić make. Co teraz się dzieje? Dlaczego?

   
10.6.0.2 Przykład 2

Zmiana/modyfikacja pliku nagłówkowego hw_hello.h powoduje, że:
  1. moduł hw_main musi być zrekompilowany, a
  2. cały program hello zrelinkowany
Proszę zmodyfikować plik hw_hello.h, wczytując go do edytora i nagrywając, ew. można tylko zmienić jego datę modyfikacji pisząc:
$ touch hw_hello.h

Proszę teraz uruchomić:

$ make
Jakie polecenia zostały uruchomione przez make? Co z modułami (hw_hello, hw_power)?

Proszę jeszcze raz uruchomić make. Co teraz się dzieje? Dlaczego?

Ten przykład ujawnia pewien niedostatek stworzonego pliku Makefile! Jaki?

   
10.6.0.3 Przykład 3

Zmiana/modyfikacja pliku nagłówkowego hw_defs.h powoduje, że:
  1. wszystkie moduły muszą być zrekompilowane, a
  2. cały program hello zrelinkowany
Proszę zmodyfikować plik hw_defs.h, wczytując go do edytora i nagrywając, ew. można tylko zmienić jego datę modyfikacji pisząc:
$ touch hw_defs.h

Proszę teraz uruchomić:

$ make
Jakie polecenia zostały uruchomione przez make?

Proszę jeszcze raz uruchomić make. Co teraz się dzieje? Dlaczego?

10.7 Ćwiczenie: Make, elementy zaawansowane

Należy teraz stoworzyć ulepszoną wersję pliku. Starą wersję należy zachować pisząc:

$ cp Makefile Makefile-1

Do nowej wersji należy dopisać regułę, która będzie sprzątała po procesie kompilacji:

clean:
    rm -f *~
    rm -f *.bak
    rm -f hello hw_main.o hw_hello.o hw_power.o

Pierwsze dwa polecenia usuwają pliki kopii zapasowej, które mogą powstać w trakcie edycji plików źródłowych. Trzecie polecenie usuwa wszystkie pliki wynikowe powstające w procesie kompilacji.

Wszystkie cele pozorne muszą być zadeklarowane jako takie, robi się to poprzez specjalny cel .PHONY którego prerekwizytami są wszsytkie cele pozorne w pliku. Prosze dopisać do pliku linię:

.PHONY: clean

Cele pozorne mają ważną cechę: nigdy nie są zaktualizowane (ang. up to date) (tzn. będą budowane przy każdym ich wywołaniu, bez względnu na ich preprekwizyty!).

Proszę teraz uruchomić:

$ make clean
Jakie polecenia zostały uruchomione przez make?

Proszę jeszcze raz uruchomić make clean. Co teraz się dzieje? Dlaczego?

Proszę jeszcze raz uruchomić samo make. Co teraz się dzieje? Dlaczego?

Proszę jeszcze raz uruchomić samo make. Co teraz się dzieje? Dlaczego?

10.8 Ćwiczenie: Make, Zmienne i poprawny plik

W miarę rozbudowywania programu plik Makefile będzie powiększany i aktualizowany. Przy dopisywaniu kolejnych modułów można zauważyć pewne niedogodności w obecnym pliku:

Dlatego też używa się zmiennych.

Należy teraz stworzyć ulepszoną wersję pliku. Starą wersję należy zachować pisząc:

$ cp Makefile Makefile-2

Należy do pliku Makefile dopisać na początku linijki z deklaracjami zmiennych:

CC     = gcc
CFLAGS = -Wall -ansi -pedantic
LFLAGS = -lm
OBJS   = hw_hello.o hw_power.o hw_main.o
EXEC   = hello

Następnie rozbić dotychczasowy cel hello na dwa. Jest to dużo bardziej sensowne i poprawne rozwiąznie niż dotychczasowe (patrz powyższe przykłady uruchomienia):

$(EXEC): $(OBJS)
	$(CC) $(LFLAGS) $(OBJS) -o $(EXEC)
	echo hello: zbudowane!

hw_main.o : hw_defs.h hw_hello.h hw_power.h
	$(CC) -c $(CFLAGS) hw_main.c

Następnie należy uaktualnić pozostały fragment:

hw_hello.o: hw_hello.c hw_defs.h 
	$(CC) -c $(CFLAGS) hw_hello.c

hw_power.o: hw_power.c hw_defs.h 
	$(CC) -c $(CFLAGS) hw_power.c

clean:
	rm -f *~ *.bak
	rm -f $(EXEC) $(OBJS)

.PHONY: clean

Taki plik jest znacznie bardziej uniwersalny! Wprowadzenie zmiennych i wydzielenie hw_main jako osobnego modułu nie tylko usunęło w.w. niedogodności, lecz również uczyniło zależności znacznie bardziej przejrzyste (np. plik wynikowy jest jedynie linkowany i nie zależy od plików nagłówkowych!)

Proszę jeszcze raz przećwiczyć wszystkie poprzednie przykłady z nowym plikiem i sprawdzić, czy działa poprawnie!

10.9 Ćwiczenie: Make, podsumowanie

Choć teraz plik Makefile jest dużo lepszy, to przy jeszcze większych projektach można by go udoskonalić. W szczególności należy zwrócić uwagę, iż budowanie każdego z 3 modułów jest praktycznie identyczne z dokładnością do:
  1. nazw plików
  2. zależności (prerekwizytów)
Make posiada mechanizmy, które potrafią jeszcze bardziej uprościć budowanie kodu w tym wzorce reguł.

Należy teraz stowrzyć ulepszoną wersję pliku. Starą wersję należy zachować pisząc:

$ cp Makefile Makefile-3

Nowy plik Makefile powinien wyglądać teraz tak:

CC     = gcc
CFLAGS = -Wall -ansi -pedantic
LFLAGS = -lm
OBJS   = hw_hello.o hw_power.o hw_main.o
EXEC   = hello

all: $(EXEC)

$(EXEC): $(OBJS)
	$(CC) $(LFLAGS) $+ -o $@
	echo hello: zbudowane!

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

hw_main.o : hw_defs.h hw_hello.h hw_power.h
hw_hello.o: hw_defs.h 
hw_power.o: hw_defs.h 

clean:
	rm -f *~
	rm -f *.bak
	rm -f $(EXEC) $(OBJS)

.PHONY: clean

Proszę zwrócić uwagę co się zmieniło:

  1. dodano jako pierwszy nowy cel all, jedyne co robi to powoduje zbudowanie programu. Jego dodanie jest zwyczjowe (każdy Makefile powienien go mieć, podobnie jak cel clean), poza tym ułatwia wybór domyślengo celu, gdyby program miał więcej niż jedną możliwości budowania.
  2. w celu odpowiedzialnym za linkowanie użyto 2 specjalne zmienne: $@ oznacza nazwę danego celu, $+ oznacza nazwy wszystkich prerekwizytów
  3. reguły budujące (kompilujące) pliki obiektów binarnych zastąpiono jednym wzorcem, metaregułą budującą wszystkie z nich. Zmienna $< oznacza nazwę danego dopasowanego pliku (w tym przypadku z rozszerzenim .c).
  4. zależności poszczególnych plików zostały podane osobno. Do analizowania zależności z plikami nagłówkowymi Make używa wewnętrzych reguł (tzn. ,,wie'' że plików nagłówkowych nie trzeba budować, a jedynie sprawdzić ich daty modyfikacji).

Proszę ponownie przećwiczyć przykłady z nowym plikiem Makefile.

10.10 Ćwiczenie: Konwencje make, nie tylko kompilacja

Make może zarządzać dowolnym procesem budowania plików, nie tylko kompilacją. W używanych plików występował jeden cel nie związany w ogóle bezpośrednio z kompilatorem, tzn. clean.

Wg konwencji plik Makefile powinien mieć przynajmniej 3 cele:

all
budujący cały program, cel domyślny,
clean
czyszczący kod po kompilacji
install
instalujący program po kompilacji
Często występuje też cel dist powodujący stworzenie archiwum kodu.

Należy teraz stworzyć ulepszoną wersję pliku. Starą wersję należy zachować pisząc:

$ cp Makefile Makefile-4

Aby uzupełnić plik Makefile proszę dopisać do zmiennych na początku pliku:

MANIFEST= *.c *.h Makefile*
DESTDIR = $(HOME)/bin
VER     = 0.1
a następnie do celów na końcu:
install: all
	sh -c "if [ ! -d $(DESTDIR) ] ; then mkdir $(DESTDIR) ; fi"
	cp $(EXEC) $(DESTDIR)/$(EXEC)
	echo hello: zainstalowane!

dist: clean
	tar cvzf ../$(EXEC)-$(VER).tar.gz $(MANIFEST)

Jakie są prerekwizyty celów? Dlaczego?

10.11 Ćwiczenie: Podsumowanie i przydatne opcje Make

Podsumowując, można stwierdzić, iż udało się stworzyć plik Makefile dla projektu programistycznego pozwalający na kompilację i instalację oprogramowania:

$ make
$ make install
a także na tworzenie archiwum - przydatne, gdyby się np. zmodyfikowało kod i chciało go komuś przekazać.
$ make dist

make można też uruchomić w trybie silent w którym nie wyświetla poleceń które wykonuje, a jedynie komunikaty wypisywane przez echo. W tym celu należy podac opcję -s

Przydatna jest też opcja -k która próbuje zrealizować jak najwięcej celów nawet w przpadku napotkania błędów, a także opcja -j N pozwalająca na równoległe uruchomienie N instancji make i zrównoleglenie procesu kompilacji, co jest przydatne w przypadku maszyn wieloprocesorowych.

Zadając opcję -f plik można podać plik o innej niż Makefile nazwie.

10.12 Ćwiczenie: Dokumentacja kodu

Proszę oglągnąć stronę domową Doxygen, http://www.doxygen.org.

Proszę prześledzić komentarze w kodzie przykłady sformatowane zgodnie z zasadami Doxygena.

Dokumentację można wygenerować przy pomocy Doxygena (o ile jest dostępny) lub wywołać go przez Make (jak?).

UWAGA: Doxygen nie obowiązuje na kolokwium ;)

DO_PRZYGOTOWANIA

Samodzielnie należy przed tym laboratorium przygotować:

Literatura o kompilatorze GCC i narzędziach:

Dla zaawansowanych:

Informacje o Make:

Dla zaawansownaych plecane są źródła dotyczące narzędzi wspomagających pracę z Make:



Grzegorz J. Nalepa 2007-04-19