C/Więcej o kompilowaniu: Różnice pomiędzy wersjami

Z Wikibooks, biblioteki wolnych podręczników.
< C
Usunięta treść Dodana treść
m poprawa nawigacji
→‎Wyrównywanie: wogóle -> w ogóle!
Linia 83: Linia 83:


===Wyrównywanie===
===Wyrównywanie===
Wyrównywanie jest pewnym zjawiskiem, na które w bardzo wielu podręcznikach, mówiących o C wogóle się nie wspomina. Ten rozdział ma za zadanie wyjaśnienie tego zjawiska oraz uprzedzenie programisty o pewnych faktach, które w późniejszej jego "twórczości" mogą zminimalizować czas na znalezienie pewnych informacji, które mogą wpływać na to, że jego program nie będzie działał poprawnie.
Wyrównywanie jest pewnym zjawiskiem, na które w bardzo wielu podręcznikach, mówiących o C w ogóle się nie wspomina. Ten rozdział ma za zadanie wyjaśnienie tego zjawiska oraz uprzedzenie programisty o pewnych faktach, które w późniejszej jego "twórczości" mogą zminimalizować czas na znalezienie pewnych informacji, które mogą wpływać na to, że jego program nie będzie działał poprawnie.
Często zdarza się, że kompilator w ramach optymalizacji "wyrównuje" elementy struktury tak, aby procesor mógł łatwiej odczytać i przetworzyć dane. Przyjrzyjmy się bliżej następującemu fragmentowi kodu:
Często zdarza się, że kompilator w ramach optymalizacji "wyrównuje" elementy struktury tak, aby procesor mógł łatwiej odczytać i przetworzyć dane. Przyjrzyjmy się bliżej następującemu fragmentowi kodu:

Wersja z 17:20, 29 paź 2006

< C

Ciekawe opcje kompilatora GCC

  • -E - powoduje wygenerowanie kodu programu ze zmianami, wprowadzonymi przez preprocesor
  • -S - zamiana kodu w języku C na kod asemblera (komenda: gcc -S plik.c spowoduje utworzenie pliku o nazwie plik.s, w którym znajdzie się kod asemblera)
  • -c - kompilacja bez łączenia z bibliotekami
  • -Ikatalog - ustawienie domyślnego katalogu z plikami nagłówkowymi na katalog
  • -lbiblioteka - wymusza łączenie programu z podaną biblioteką (np. -lGL)

Program make

Dość często może się zdarzyć, że nasz program składa się z kilku plików źródłowych. Jeśli tych plików jest mało (np. 3-5) możemy jeszcze próbować ręcznie kompilować każdy z nich. Jednak jeśli tych plików jest dużo, lub chcemy pokazać nasz program innym użytkownikom musimy stworzyć elegancki sposób kompilacji naszego programu. Właśnie po to, aby zautomatyzować proces kompilacji powstał program make. Program make analizuje pliki Makefile i na ich podstawie wykonuje określone czynności.

Budowa pliku Makefile

Uwaga: poniżej został umówiony Makefile dla GNU Make. Istnieją inne programy make i mogą używać innej składni. Na Wikibooks został też obszernie opisany program make firmy Borland.


Najważniejszym elementem pliku Makefile są zależności oraz reguły przetwarzania. Zależności polegają na tym, że np. jeśli nasz program ma być zbudowany z 4 plików, to najpierw należy skompilować każdy z tych 4 plików, a dopiero później połączyć je w jeden cały program. Zatem zależności określają kolejność wykonywanych czynności. Natomiast reguły określają jak skompilować dany plik. Zależności tworzy się tak:

co: od_czego
  reguły...

Dzięki temu program make zna już kolejność wykonywanych działań oraz czynności, jakie ma wykonać. Aby zbudowac "co" należy wykonać polecenie: make co. Pierwsza reguła w pliku Makefile jest regułą domyślną. Jeśli wydamy polecenie make bez parametrów, zostanie zbudowana właśnie reguła domyślna. Tak więc dobrze jest jako pierwszą regułę wstawić regułę budującą końcowy plik wykonywalny; zwyczajowo regułę tą nazywa się all.

Należy pamiętać, by sekcji "co" nie wcinać, natomiast "reguły" wcinać dwoma spacjami. Część "od_czego" może być pusta.


Plik Makefile umożliwia też definiowanie pewnych zmiennych. Nie trzeba tutaj się już troszczyć o typ zmiennej, wystarczy napisać:

nazwa_zmiennej = wartość

W ten sposób możemy zadeklarować dowolnie dużo zmiennych. Zmienne mogą być różne - nazwa kompilatora, jego parametry i wiele innych. Zmiennej używamy w następujący sposób: $(nazwa_zmiennej).

Komentarze w pliku Makefile tworzymy zaczynając linię od znaku hash (#).

Przykładowy plik Makefile

Dość tej teorii, teraz zajmiemy się działającym przykładem. Załóżmy, że nasz przykładowy program nazywa się test oraz składa się z czterech plików:

pierwszy.c
drugi.c
trzeci.c
czwarty.c

Odpowiedni plik Makefile powinien wyglądać mniej więcej tak:

# Mój plik makefile - wpisz 'make all' aby skompilować cały program
CC = gcc  

all: pierwszy.o drugi.o trzeci.o czwarty.o
  $(CC) pierwszy.o drugi.o trzeci.o czwarty.o -o test

pierwszy.o: pierwszy.c
  $(CC) pierwszy.c -c -o pierwszy.o

drugi.o: drugi.c
  $(CC)drugi.c -c -o drugi.o

trzeci.o: trzeci.c
  $(CC) trzeci.c -c -o trzeci.o

czwarty.o: czwarty.c
  $(CC) czwarty.c -c -o czwarty.o

Widzimy, że nasz program zależy od 4 plików z rozszerzeniem .o (pierwszy.o itd.), potem każdy z tych plików zależy od plików .c, które program make skompiluje w pierwszej kolejności, a następnie połączy w jeden program (test). Nazwę kompilatora zapisaliśmy jako zmienną, ponieważ powtarza się i zmienna jest sposobem, by zmienić ją wszędzie za jednym zamachem.

Zatem jak widać używanie pliku Makefile jest bardzo proste. Warto na koniec naszego przykładu dodać regułę, która wyczyści katalog z plików .o:

clean:
  rm -f *.o test

Ta reguła spodowuje usunięcie wszystkich plików .o oraz naszego programu jeśli napiszemy make clean.

Możemy też ukryć wykonywane komendy albo dopisać własny opis czynności:

clean:
  @echo Usuwam gotowe pliki
  @rm -f *.o test

Tak naprawdę jest to dopiero bardzo podstawowe wprowadzenie do używania programu make, jednak jest ono wystarczające, byś zaczął z niego korzystać. Wyczerpujące omówienie całego programu niestety przekracza zakres tego podręcznika.


Optymalizacje

Kompilator GCC umożliwia generację kodu zoptymalizowanego dla konkretnej architektury. Służą do tego opcje -march= i -mtune=. Stopień optymalizacji ustalamy za pomocą opcji -Ox, gdzie x jest numerem stopnia optymalizacji (od 1 do 3). Możliwe jest też użycie opcji -Os, która powoduje generowanie kodu o jak najmniejszym rozmiarze. Aby skompilować dany plik z optymalizacjami dla procesora Athlon XP, należy napisać tak:

gcc program.c -o program -march=athlon-xp -O3

Z optymalizacjami należy uważać, gdyż często zdarza się, że kod skompilowany bez optymalizacji działa zupełnie inaczej, niż ten, który został skompilowany z optymalizacjami.

Wyrównywanie

Wyrównywanie jest pewnym zjawiskiem, na które w bardzo wielu podręcznikach, mówiących o C w ogóle się nie wspomina. Ten rozdział ma za zadanie wyjaśnienie tego zjawiska oraz uprzedzenie programisty o pewnych faktach, które w późniejszej jego "twórczości" mogą zminimalizować czas na znalezienie pewnych informacji, które mogą wpływać na to, że jego program nie będzie działał poprawnie.

Często zdarza się, że kompilator w ramach optymalizacji "wyrównuje" elementy struktury tak, aby procesor mógł łatwiej odczytać i przetworzyć dane. Przyjrzyjmy się bliżej następującemu fragmentowi kodu:

typedef struct {
  unsigned char wiek; /* 8 bitów */
  unsigned short dochod; /* 16 bitów */
  unsigned char plec; /* 8 bitów */
} nasza_str;

Aby procesor mógł łatwiej przetworzyć dane kompilator może dodać do tej struktury jedno, ośmiobitowe pole. Wtedy struktura będzie wyglądała tak:

typedef struct {
  unsigned char wiek; /*8 bitów */
  unsigned char fill[1]; /* 8 bitów */
  unsigned short dochod; /* 16 bitów */
  unsigned char plec; /* 8 bitów */
} nasza_str;

Wtedy rozmiar zmiennych przechowujących wiek, płeć, oraz dochód będzie wynosił 64 bity - będzie zatem potęgą liczby dwa i procesorowi dużo łatwiej będzie tak ułożoną strukturę przechowywać w pamięci cache. Jednak taka sytuacja nie zawsze jest pożądana. Aby jej zapobiec w kompilatorze GNU GCC możemy użyć takiej oto linijki:

__attribute__ ((packed))

Dzięki użyciu tego atrybutu, kompilator zostanie "zmuszony" do braku ingerencji w naszą strukturę. Jest jednak jeszcze jeden, być może bardziej elegancki sposób na obejście dopełniania. Zauważyłeś, że dopełnienie, dodane przez kompilator pojawiło się między polem o długości 8-bitów (plec) oraz polem o długości 32-bitów (dochod). Wyrównywanie polega na tym, że dana zmienna powinna być umieszczona pod adresem będącym wielokrotnością jej rozmiaru. Oznacza to, że jeśli np. mamy w strukturze na początku dwie zmienne, o rozmiarze jednego bajta, a potem jedną zmienną, o rozmiarze 4 bajtów, to pomiędzy polami o rozmiarze 2 bajtów, a polem czterobajtowym pojawi się dwubajtowe dopełnienie. Może Ci się wydawać, że jest to tylko niepotrzebne mącenie w głowie, jednak niektóre architektury (zwłacza typu RISC) mogą nie wykonać kodu, który nie został wyrównany. Dlatego, naszą strukturę powinniśmy zapisać mniej więcej tak:

typedef struct {
  unsigned short dochod; /* 16 bitów */
  unsigned char wiek; /* 8 bitów */
  unsigned char plec; /* 8 bitów */
} nasza_str;

W ten sposób wyrównana struktura nie będzie podlegała modyfikacjom przez kompilator oraz będzie przenośna pomiędzy różnymi kompilatorami.

Wyrównywanie działa także na pojedyczych zmiennych w programie, jednak ten problem nie powoduje tyle zamieszania, co ingerencja kompilatora w układ pól struktury. Wyrównywanie zmiennych polega tylko na tym, że kompilator umieszcza je pod adresami, które są wielokrotnością ich rozmiaru

Kompilacja skrośna

Mając w domu dwa komputery, o odmiennych architekturach (np. i386 oraz Sparc) możemy potrzebować stworzyć program dla jednej maszyny, mając do dyspozycji tylko drugi komputer. Nie musimy wtedy latać do znajomego, posiadającego odpowiedni sprzęt. Możemy skorzystać z tzw. kompilacji skrośnej (ang. cross-compile). Polega ona na tym, że program nie jest kompilowany pod procesor, na którym działa kompilator, lecz na inną, zdefiniowaną wcześniej maszynę. Efekt będzie taki sam, a skompilowany program możemy bez problemu uruchomić na drugim komputerze.

Inne narzędzia

Wśród przydatnych narzędzi, warto wymienić równierz program objdump (zarówno pod Unix jak i pod Windows) oraz readelf (tylko Unix). Objdump służy do deasemblacji i analizy skompilowanych programów. Readelf służy do analizy pliku wykonywalnego w formacie ELF (używanego w większości systemów z rodziny Unix). Więcej informacji możesz uzyskać, pisząc (w systemach Unix):

man 1 objdump
man 1 readelf

Szablon:ProstaNawigacja