Tags Posts tagged with "kompilacja"

kompilacja

przez -
1 622
GNU - logo

>Paul Smith ogłosił wydanie GNU Make 4.0, programu powłoki systemowej, który automatyzuje proces kompilacji programów, na które składa się wiele zależnych od siebie plików. Aplikacja przetwarza plik reguł Makefile i na tej podstawie stwierdza, które pliki źródłowe wymagają kompilacji. Zaoszczędza to wiele czasu przy tworzeniu programu, ponieważ w wyniku zmiany pliku źródłowego kompilowane są tylko te pliki, które są zależne od tego pliku.

Najważniejsze zmiany:

  • Zintegrowano GNU Guile. Wspierane są wersje GNU Guile 1.8 i GNU Guile 2.0+
  • Dodano nowe komendy: --output-sync (-O), --trace, oraz nową opcję do komendy --debug: “n” (none)
  • Funkcje job server i .ONESHELL są teraz wspierane na Windows
  • Dodano nowy operator przypisania “!=“, który jest alternatywą dla funkcji $(shell …) i ma być kompatybilny z BSD Make
  • Dodano prosty operator przypisania “::=“, zdefiniowany w POSIX 2012
  • Dodano nową funkcję: $(file ...), która zapisuje do pliku
  • Dodano możliwość używania opcji: -r i -R wewnątrz MAKEFLAGS
  • Naprawiono ponad 80 błędów i dokonano pomniejszych zmian

Programowanie

Artykuł opisuje kolejne kroki kompilacji kodu w GCC. W prosty i przyjemny sposób oprowadzi Ciebie po procesie preprocesingu, kompilacji, optymalizacji oraz konsolidacji. Dzięki prostym przykładom zrozumiesz każdy etap przez jaki przechodzi kod źródłowy nim stanie się programem.

GCC jest podstawowym kompilatorem dla systemów uniksowych (posiada również port pod Windowsa), potrafi kompilować kod na wiele platform sprzętowych, takich jak x86, x86_64, ARM i PowerPC. Sama nazwa GCC jest nieco dwuznaczna: GNU C Compiler lub GNU Compiler Collection. Obecnie prawidłowe jest drugie rozwinięcie, mimo że sam gcc jako program służy do kompilowania kodu w C.

Ale do rzeczy. Może najpierw omówię proces kompilacji, który wielu kojarzy tylko z naciśnięciem przycisku “Build” w IDE. Potocznie na cały ten proces mówi się “kompilacja”, ale kompilacja to tylko jeden z procesów. Nie będę skupiał się na samych kodach źródłowych, ani działaniu bibliotek i ich dołączaniu. Omówię jak aplikacja jest budowana przy pomocy GCC.

Moim plikiem wejściowym będzie prosty algorytm liczenia sinusa w C.

#include <stdio.h>

#ifndef STEPS
	#define STEPS 20
#endif
#ifndef PRECISION
	#define PRECISION 30
#endif

double sin(double x){
	double w=x;
	double x2=x*x;
	double t=w;
	int i;
	for(i=2; i < PRECISION; i+=2){
		t=-t*((x2)/(i*(i+1)));
		w+=t;
	}
	return w;
}

int main(){
	double x=0;
	for(x=-1.570; x<=1.571; x+=3.14/STEPS)
		printf("sin(%lf)=%lf\n", x, sin(x));
	return 0;	
}

Oto kolejne etapy budowania aplikacji napisanej w C.

Preprocesor

Pierwszym krokiem jest przygotowanie kodu do kompilacji, robi to tzw. preprocesor. W tej fazie wykonywane są tzw. dyrektywy preprocesora – instrukcje poprzedzone znakiem “#”, m.in. #inlude, #define, #if itd. oraz wstawiane są definicje i makra. Innymi słowy: jeśli był gdziekolwiek jakiś #include, w te miejsce zostanie dosłownie wklejony podany plik. Jeśli jakiś warunek #if nie będzie spełniony, kod w tym warunku (tzn. do #endif) zostanie usunięty. W miejsca użycia nazw definicji zostaną wklejone dane definicje (lub makra).

Na uwagę zasługują właśnie makra. Trzeba pamiętać że w dane miejsce jego użycia makro zostanie wklejone dosłownie tak, jak było zdefiniowane:

#define SUMA(_v) _v+_v
int v=2*SUMA(2);

Po przejściu przez preprocesor w kodzie pozostanie:

int v=2*2+2;

Bezpieczniej jest takie makra wrzucić do nawiasu. Przykładowy plik wynikowy (na podstawie mojego sinus.c).

W następnym kroku omówię proces kompilacji i optymalizacji.

Kompilacja i optymalizacja

To najciekawszy etap budowania aplikacji w C, jest to proces zamieniający kod w danym języku programowania najpierw na: pośredni język kompilatora, później do Assemblera. W tym procesie kompilator usuwa takie elementy jak pętle zamieniając je na zwykłe warunki i instrukcje goto, a także przygotowuje kod do translacji na assemblera. Staje się też jasne, dlaczego switch działa akurat tak, a nie inaczej.

kod:

int main(){
	volatile int a=4; //volatile sprawia, że nie ma wykonywać optymalizacji względem tej zmiennej
	  //bez tego, kompilator usunąłby całego switch'a, bo byłby zbędny
	switch(a){
	case 1: printf("1");
	case 2: printf("2"); break;
	case 3 ... 4: printf("2..4");
	}
	return 0;
}

kod w trakcie kompilacji

;; Function int main() (main) (executed once)

int main() ()
{
  volatile int a;
  int retval.0;

<bb 2>:
  a ={v} 4;
  retval.0_1 ={v} a;
  switch (retval.0_1) <default: <L3>, case 1: <L0>, case 2: <L1>, case 3 ... 4: <L2>>

<L0>:
  __builtin_putchar (49);

<L1>:
  __builtin_putchar (50);
  goto <bb 6> (<L3>);

<L2>:
  __printf_chk (1, "2..4");

<L3>:
  return 0;

}

Na tym etapie kod jest również optymalizowany krok po kroku, poszczególne kroki można zobaczyć dodając -fdump-tree-all do polecenia kompilującego. Kompilator zna wiele sposobów przyspieszenia kodu, na powyższym przykładzie widać, jak z pozoru ciężkiego printfa zamienia na proste wypisanie pojedynczego znaku, gdzie to jest możliwe. Obserwując kody generowane w trakcje optymalizacji zobaczyć można przekonwertowane liczby rzeczywiste do ich postaci w notacji wykładniczej, a przy tym: utratę dokładności: na tym poziomie staje się jasne dlaczego nie należy porównywać wartości zmiennoprzecinkowych operatorem ==.

Przykładowy plik pośredni, uzyskany podczas optymalizacji kodu przed ostateczną translacją do assemblera: sinus-optimized.i. Jak widać nie ma tu już pętel, są zastąpione zrozumiałymi dla procesora warunkami if i poleceniami goto. Widać również jak wartość -1.570 jest przetłumaczona jako: (<bb 3>, linijka 68):

-1.5700000000000000621724893790087662637233734130859375e+0(2)

Na tym samym poziomie wstawiane są wszystkie stałe wartości, jeśli jakaś funkcja jest const i użyjemy jej gdziekolwiek w kodzie z argumentami również const (znanymi w czasie kompilacji), kompilator może ją obliczyć na poziomie kompilacji i podstawić gotowy wynik. Przykładowo wyrażenie 3.14/20 zostało obliczone na poziomie kompilacji, nie wykonania – to efekt optymalizacji. Widać to w tym miejscu (<bb 5>, linijka 94):

x_7 = x_35 + 1.570000000000000006661338147750939242541790008544921875e-1;

e-1 na końcu świadczy o tym, że mamy do czynienia z wartością 0.157, czyli 3.14/20. Dzięki temu nie wykonuje tego działania na dwóch stałych znanych już w czasie kompilacji podczas wykonywania pętli.

Niektórzy pewnie zadają sobie pytane: a gdzie się podział warunek pętli for i wartość 1.571? Tak właściwie, to ich nie ma! A przynajmniej nie w takiej formie, w jakiej były w kodzie źródłowym. Na drodze optymalizacji kompilator stwierdził, że można go uprościć, bo zarówno wartość początkowa, końcowa, jak i wielkość kroku są stałe, więc pętla może wykonać się określoną ilość razy: 21. Operacje na typach całkowitych są w końcu szybsze. Wartość ta jest trzymana w zmiennej ivtmp.27, widać to w tej linijce (<bb 3>, linijka 69):

# ivtmp.27_25 = PHI <ivtmp.27_46(5), 21(2)>

Jeśli ktoś nie wierzy, może zmienić wartość STEPS i przy takich samych opcjach kompilacji zobaczyć jeszcze raz (dokładne kroki podam później). Obsługa pętli for jak wspomniałem opiera się na poleceniach goto, a tak wygląda końcowa część (instrukcja + warunek, <bb 5>, od linijki 95):

ivtmp.27_46 = ivtmp.27_25 - 1;
  if (ivtmp.27_46 != 0)
    goto &lt;bb 3&gt;;
  else
    goto &lt;bb 6&gt;;

Widać tu też, że kompilator usuwa na tym etapie wyrażenia takie jak x=x-1, zamieniając je na np. x_2=x_1-1, stąd te sufiksy.

W trakcie kompilacji kod jest również dostosowywany do wybranej architektury i środowiska, tzn. czy ma być na maszyny 32, czy 64-bitowe, oraz konkretne technologie procesora. Procesory mają swoje nazwy własne nie nadaremno: poza szybkością zegara, ilością rdzeni i pamięcią cache różnią się jeszcze zaimplementowanymi technologiami, które można przejrzeć zaglądając do pliku cpuinfo (cat /proc/cpuinfo), lub programem CPU-Z na Windowsie.

Podczas zamiany tego kodu pośredniego na assembler, używa dostępnych na danej architekturze instrukcji procesora. Można określić architekturę poprzez przełączniki -march=cpu i -mtune=cpu, gdzie za cpu podstawia się nazwę procesora (listę obsługiwanych można zobaczyć w podręczniku lub w man gcc) lub np. dla tej maszyny: -march=native. Kod wynikowy może się różnić w zależności od wybranej architektury procesora. Wg. podręcznika przełącznik -march wywołuje też -mtune, więc nie trzeba używać ich obu.

Po tym etapie kod jest tłumaczony do Assemblera. Plik wynikowy dla mojego sinus.i:

main:
.LFB1:
	.cfi_startproc
	pushq	%rbx
	.cfi_def_cfa_offset 16
	.cfi_offset 3, -16
	movl	$21, %ebx
	subq	$32, %rsp
	.cfi_def_cfa_offset 48
	movsd	.LC1(%rip), %xmm3
	movsd	.LC0(%rip), %xmm2

Pełen plik wynikowy kompilacji i optymalizacji: sinus.s.

Assemblacja

Dopiero w tym procesie kod jest tłumaczony na kod maszynowy, ale nadal nie nadaje się du uruchomienia: nie posiada potrzebnych nagłówków oraz może posiadać tzw. niezdefiniowane referencje (zwane też w niektórych kompilatorach: unsolved symbols – nierozwiązane symbole). To znaczy, że jeśli np. załączyliśmy plik funkcje.h z samymi predeklaracjami funkcji, assembler nie musi jeszcze wiedzieć gdzie ta funkcja jest, byle ma informację, że taka funkcja gdzieś jest, tak się nazywa i ma daną sygnaturę.

Wynikiem tej fazy jest tzw. obiekt (rozszerzenie .o), fragment programu. Nie ma sensu zamieszczać jego treści.

Konsolidacja, czyli linkowanie

Ostatnia faza budowania aplikacji. Polega na zebraniu wszystkich obiektów do razem i złączeniu ich w wykonywalny program. Konsolidator wymaga już skojarzenia symboli (nazw), musi wiedzieć gdzie znajduje się dana funkcja, dany obiekt zewnętrzny, musi je powiązać (zlinkować). Rozwiązań symboli (czy też: definicji referencji) może także szukać w bibliotekach dzielonych (.so, samego pliku nie dołączy, jedynie utworzy odnośnik) i statycznych (.a, ten plik już zostanie dołączony do aplikacji statycznie). Na tym etapie wśród linkowanych obiektów musi znaleźć się funkcja main.

Na tym etapie wywala błędy typu undefined reference to... – nie potrafi przypisać jakiejś (pre)deklaracji do definicji. Błąd ten jest wyrzucany tylko dla użytych symboli, a nie wszystkich zadeklarowanych (jeśli mamy kilka predeklaracji funkcji, a używamy tylko jedną z nich, błąd wystąpi tylko dla tej używanej). Jeżeli ten błąd występuje, to znaczy że albo pominęliśmy jakąś bibliotekę zewnętrzną, albo jest literówka w którymś nagłówku.

Aby dolinkować bibliotekę, trzeba dodać do argumentów wywołania nazwę biblioteki, która znajduje się w folderze domyślnym bibliotek dla kompilatora (np. -lGL, domyślnym katalogiem w Linuksie jest /usr/lib i /usr/local/lib) i jeśli to konieczne, podać dodatkowe lokalizacje gdzie tych bibliotek ma szukać (-L/sciezka/do/katalogu). Zwykle pliki Readme lub podręczniki bibliotek zawierają informację jakie biblioteki trzeba załączyć do linkera.

Czy gcc robi to wszystko?

Zarówno tak, jak i nie, program gcc jest tylko interfejsem, sterownikiem. GCC (jako pakiet) składa się z wielu programów, gcc jest jednym z nich, w prosty sposób zarządza on całym procesem od początku do końca. Każda faza budowania ma przydzieloną osobną aplikację. Oczywiście wszystkie flagi i przełączniki można mu przekazać, gcc będzie wiedział co z nimi zrobić, gdzie ich użyć. Te programy to m.in.:

  • Preprocesor: cpp (C PreProcessor), tworzy plik gotowy do kompilacji (-E)
  • Kompilator: cc (lub cc1, C Compiler), kompiluje do assemblera (-S)
  • Assembler: as, assembluje kod do kodu maszynowego platformy docelowej (-c)
  • Konsolidator: ld – tworzy końcowy program z utworzonych obiektów

(W nawiasach podane przełączniki, które powodują że na tym etapie budowanie ma się zakończyć pozostawiając pliki powstałe w danym procesie)

Dla C++ są takie same nazwy programów, tyle że trzeba poinformować, że chodzi o język c++: -x c++. Standardowo gcc rozpoznaje pliki źródłowe po rozszerzeniach i powinien wiedzieć co z nimi zrobić (ale skompiluje program jako C ze zlinkowanymi bibliotekami do C, więc kod w C++ może się nie zbudować). Przykładowo .c i .cpp zostaną poddane całemu procesowi. .i (C) i .ii (C++) nie zostaną poddane preprocesorowi, tylko od razu kompilacji. .s zostanie od razu zassemblowany. Produkty .o zostaną poddane konsolidacji. Cały ten proces jest wykonywany przez gcc (i g++ dla C++), nie trzeba wykonywać tych faz ręcznie.

Ręczna kompilacja z pominięciem GCC wygląda mniej więcej tak:

#prekompilacja
cpp sinus.c &gt; sinus.i

#kompilacja
cc sinus.i -S #argument -S, by na tym zakończył, bez tego ruszyłby z procesem dalej assemblując i próbując zlinkować

#assemblacja
as sinus.s -o sinus.o

I to jeszcze nie wszystko. Kolejnym krokiem jest konsolidacja, która jest inna na każdej platformie i wersji kompilatora. Znajomość procesu ręcznej kompilacji może się przydać przy dogłębszej zabawie z kompilatorem lub przy niestandardowym jego wykorzystaniu (np. mechanizm konkursowy topcoder.com , gdzie w C++ pisze się jedynie klasę z metodą rozwiązującą zadanie, a nie cały program), jednak końcowe pliki .o najlepiej potraktować gcc:

gcc *.o -o program

C++ wymaga dodatkowo zlinkowania biblioteki stdc++. Znacznie łatwiej jest uruchamiać wszystko za pośrednictwem gcc i g++, one wiedzą co zrobić dla danego języka, żeby nie było problemów.

Jak więc kompilować?

Dobry programista powinien znać swój kompilator, sposób kompilacji oraz umieć obsługiwać go z poziomu konsoli, znam osoby będące święcie przekonane że kompilator C# jest aplikacją okienkową (pomijam już powszechne mylenie kompilatora z IDE).

Przede wszystkim kompilator trzeba mieć zainstalowany. W dystrybucjach Linuksowych (i ogólnie uniksowych) jest on zazwyczaj w repozytoriach pod nazwą właśnie gcc, albo w postaci meta-pakietu build-essential (wersja zalecana). Pod Windowsem trzeba ręcznie ściągnąć pakiet MinGW z GCC i zainstalować go.

Następnie trzeba go mieć całe te środowisko (tzn. gcc i wszystkie podprogramy) w konsoli pod ręką, tzn. w zmiennej środowiskowej PATH. W systemach uniksowych (w tym i Linuksie) po zainstalowaniu gcc przez menedżer pakietów jest on tam domyślnie (/usr/bin), na Windowsie trzeba go sobie ręcznie dodać (poprzez ustawienie zmiennej PATH). Następnie mając uruchomioną konsolę ze ścieżką do aplikacji kompilatora w PATH, przechodzimy do folderu ze źródłem, które chcemy skompilować i wpisujemy:

gcc plik.c

To najprostsze wywołanie kompilatora. Spowoduje to zbudowanie aplikacji z jednego pliku .c do końcowego pliku a.out (lub a.exe, a.out nazwa starego formatu plików wykonywalnych, raczej pozostałość, bo obecnie linuksowe binarki to ELFy). Do sprecyzowania wynikowej nazwy pliku (w tym przypadku pliku wykonywalnego) należy użyć -o nazwa_programu, wtedy zamiast a.out będzie dana nazwa (nazwa wyjściowa musi być zaraz po spacji za -o, inaczej zostanie uznana za nazwę pliku wejściowego). Wynikowy plik od razu ma prawo do wykonywania. Takie wywołanie spowoduje wykonanie wszystkich tych kroków, jakie podałem wyżej: od preprocesora do konsolidacji. Co ciekawe, ta prosta konstrukcja może być użyta do kompilacji wielu plików:

gcc plik.c funkcje.c -o program

Skompiluje to oba pliki do jednego programu. Ale to nie wszystko: obsługiwane są wyrażenia regularne w nazwach! Przykładowo mając pliki: c1.c, c2.c, c3.c. Wywołanie gcc c[1-2].c skompiluje jedynie pliki c1.c i c2.c! Przydaje się konstrukcja gcc *.c by skompilować wszystkie pliki .c w danym folderze, gdy uczy się pracy na wielu plikach źródłowych, a nie chce się robić bałaganu projektami. Funkcja main musi wystąpić tylko raz, jeśli nie będzie, gcc (a raczej ld, bo to jego działka) wypisze undefined reference to `main', a gdy będzie więcej: multiple definition of `main' (podobnie z innymi funkcjami o takich samych sygnaturach).

Kompilacja wielkich projektów może trwać godziny, zatem czy każda zmiana wymaga ponownej kompilacji? Tak, ale nie całego projektu. Menedżery projektów zwykle kompilują pojedyncze pliki .c do plików obiektów .o (nie zlinkowanych). Te zaś, nie są rekompilowane, gdy nie muszą. Załóżmy że mamy pliki p1.c, p2.c i p3.c. Po pierwszym zbudowaniu poza wynikowym programem tworzone są pośrednie pliki obiektów: p1.o, p2.o i p3.o. Jeśli zmienimy cokolwiek w pliku p2.c, przy ponownym budowaniu skompilowany zostanie tylko ten jeden, a końcowy program zostanie zbudowany ze starych p1.o i p3.o i nowego p2.o. W ten sposób oszczędza się czas budowania wielo-plikowych projektów.

A jak to zrobić z poziomu konsoli? Służy do tego przełącznik -c, który kończy proces bez przeprowadzania konsolidacji, wystarczy dodać go do argumentów:

gcc p1.c -c

Nie trzeba podawać nazwy pliku końcowego (ale można), będzie taka sama nazwa jak pliku wejściowego, tyle że z rozszerzeniem .o (przy wielu plikach wejściowych nie można samemu określić nazwy pliku wyjściowego). Gdy mamy już skompilowane pliki źródłowe do obiektów, przystępujemy do konsolidacji:

gcc *.o -o program

Tutaj przydaje się wyrażenie regularne, by nie podawać nazw pojedynczo. W jednym (i tylko jednym, jak wspominałem) obiekcie musi być funkcja main.

Podsumowując, aby zbudować prostą aplikację jedno-plikową, wystarczy prosta konstrukcja:

gcc plik.c -o program

Dla programów wielo-plikowych, można skompilować wszystkie pliki (w folderze) jednocześnie poprzez użycie wyrażenia regularnego lub wypisanie każdego osobno.

Jest jeszcze jedno, o czym nie wspomniałem: GCC jest również cross-compilerem. Pozwala kompilować na różne platformy docelowe, nie tylko x86 i x86_64 i nie tylko na obecną. Mając 32-bitowy system i zainstalowane 32-bitowe biblioteki można skompilować aplikację na 32-bitową platformę używając flagi -m32, gcc dopasuje cały proces do 32-bitowej platformy docelowej. Kompilacja na inne platformy procesorowe (AVR, ARM) to już inny temat.

Flagi optymalizacji

Są to flagi służące do włączenia optymalizacji kodu: kompilator znajduje fragmenty dla których zna szybsze rozwiązanie i zamienia je. Najczęściej wystarczy przełącznik -O2, lub -O3. Przykład użycia:

gcc *.c -O2 -o program

Kolejność argumentów nie ma znaczenia. Jest to optymalizacja drugiego poziomu, umiarkowana, są jeszcze -O0 (domyślnie, brak optymalizacji), -O1 (słaba optymalizacja), -O3 (silna, agresywna, przez wielu uważana za zbyt ryzykowną) oraz -Os (optymalizacja rozmiaru). Flaga -O2 zostanie przekazana dopiero kompilatorowi, bo to on zajmuje się optymalizacją. Na stronie gcc.gnu.org znajduje się opis poszczególnych profili optymalizacji.

Pozostaje jeszcze przełącznik architektury procesora. GCC ma kilka szablonów pod popularne modele, każde z nich włącza dodatkowe opcje kompilacji, które mogą przyspieszyć działanie programu, obserwując różnice kodów assemblera często można zauważyć że czasem kod z -march=native (wykrywa procesor i ustawia flagi dla niego) jest krótszy, często wykonuje się szybciej.

Zachęcam do przeczytania krótkiego manuala GCC, który jest dostępny online na stronie http://linux.die.net/man/1/gcc. Dla skrótu, kilka ciekawych poleceń:

gcc -c -Q -O2 --help=optimizers # wyświetla włączone flagi optymalizacji kodu
gcc -c -Q -O2 --help=target # wyświetla włączone flagi ustawień platformy docelowej

Standardy i ostrzeżenia

-std=

Jak wiemy (a przynajmniej powinno się wiedzieć) języki C i C++ to tylko język, a nie konkretny kompilator, właściwie to brak jakiejkolwiek oficjalnej implementacji, jak to jest w przypadku Javy, czy C#. Są różne implementacje i różne wersje standardów, każdy zapewne słyszał o C++0x (znany też jako C++11). Ale może skupmy się na C.

Dość częsty problem: konstrukcja pętli for. Czy w języku C w pętli for nie można bezpośrednio deklarować zmiennych? Oto przykładowa taka pętla:

for(int i=0; i<10; i++)

W przypadku standardów z 89 roku, czyli c89 i gnu89 (domyślny) taka konstrukcja nie jest dozwolona, otrzymamy komunikat:

for.c:6:2: error: ‘for’ loop initial declarations are only allowed in C99 mode
for.c:6:2: note: use option -std=c99 or -std=gnu99 to compile your code

GCC od razu sugeruje użycie standardu c99 lub gnu99. Jeśli ten sam kod skompiluje się z dopiskiem -std=c99, błąd nie wystąpi. Do dyspozycji wg. podręcznika mamy:

  • c89, gnu89, -ansi
  • c90, gnu90
  • c99, gnu99
  • c11, gnu11 (c1x, gnu1x)
  • c++98, gnu++98, -ansi
  • c++11, gnu++11 (c++0x, gnu++0x)

W przypadku standardów gdzie są nawiasy, w nawiasach podane przestarzałe nazwy używane w wersjach do 4.7, w 4.7 i nowszych obowiązują te bez nawiasów. Te które mają dopisek -ansi, oznaczają że są to domyślne standardy, które można uzyskać poprzez użycie flagi -ansi (będą wtedy użyte w wariancie bez gnu, czyli c89/c++98, w zależności od języka).

Kompilatory oferują również często własne możliwości, aby odróżnić się od innych. Są one opcjonalne i często znacznie rozszerzają możliwości języka. Warianty standardów z GNU w nazwie włączają właśnie takie rozszerzenia. Dostępne w GCC rozszerzenia można znaleźć na tej stronie: http://gcc.gnu.org/onlinedocs/gcc/C-Extensions.html.

Przykładowe rozszerzenia:

  • zmienna przy deklarowaniu tablic
  • zakres case (case 1 … 5:)
  • typy _Decimal*, które redukują problemy dokładności (używa systemu dziesiętnego przy zapisie ułamków, nie posiadają obsługi wejścia/wyjścia)
  • 128-bitowy integer i float (nie posiadają obsługi wejścia/wyjścia)
  • funkcje osadzone
  • liczby zespolone z zapisem w postaci 1+1j

-pedantic, -Wall

GCC posiada swoje rozszerzenia i udogodnienia, ale nie jest egoistyczny. Może ostrzec programistę, że używa specyficznej dla tego kompilatora i dialektu konstrukcji. Przykładem jest użycie zmiennej do określania wielkości tablicy:

int n;
scanf("%d", &n);
int tab[n];

Taka konstrukcja nie jest dopuszczona w standardzie C (ani C++), aczkolwiek w GCC przejdzie i będzie działać prawidłowo – na etapie optymalizacji kompilator będzie wiedział co z tym fantem zrobić. Przeniesienie takiego kodu do np. Visual C++ spowoduje nieco nieporozumień. O ile normalna kompilacja nie wyrzuci żadnego błędu, to dodanie flagi -pedantic, który sprawdza zgodność kodu z ISO C90/C99 już zarzuci:

plik.c:7:2: warning: ISO C90 forbids variable length array ‘tab’ [-Wvla]
plik.c:7:2: warning: ISO C90 forbids mixed declarations and code [-pedantic]

To są tylko ostrzeżenia, że dany kod jest niezgodny ze standardem ISO C90 i jest tym samym nieprzenośny na inne kompilatory. Najpierw jest podana nazwa pliku, następnie numer lini (w tym przypadku: 7).

Drugą przydatną flagą jest -Wall (nie, nie ściana, tylko Warning: all): włącza większość ostrzeżeń (można je włączyć ręcznie, dodając poszczególne flagi). Przykładowo informuje nas o zadeklarowanych, ale nieużywanych zmiennych:

warning: unused variable ‘zmienna’ [-Wunused-variable]

Innym ważnym ostrzeżeniem, jakie włącza -Wall, jest przypisywanie w miejscach, gdzie spodziewane są warunki, np. if(n=4), poinformuje:

plik.c:8:2: warning: suggest parentheses around assignment used as truth value [-Wparentheses]

Jak widać, włączenie tej flagi może ułatwić szukanie tak prostych błędów. Co prawda ten komunikat informuje o tym, że wyrażenie powinno być w nawiasach dla czytelności, ale często się sprawdza. Czasem w takich miejscach stawia się podstawianie (np. w pętli while, gdzie coś się robi dopóki jakaś funkcja coś zwraca), ale trzeba to robić świadomie (poprzez np. umieszczenie w nawiasach).

Dla ciekawostki dopowiem, że kompilator clang dodaje notkę, że to mogło być niedopatrzenie i powinien być w tym miejscu znak porównania ==, a nawiasy by te ostrzeżenie wyciszyły.

Definicje dla prekompilatora

Niektórzy mogli sobie zadać pytanie: po co w pliku sinus.c definicje są wsadzone do warunku. gdyby nie były, nie byłoby możliwości (właściwie sensu, bo wartość będzie nadpisana) takiego wywołania:

gcc *.c -O3 -o sinus -D STEPS=40

W ten sposób na start przekazuje się prekompilatorowi zdefiniowaną wartość. Przydatne, gdy w wielu miejscach używa się definicji i chce się je modyfikować w trakcie kompilacji, np. w celu dopasowania optymalnej wartości (w tym przypadku mógłbym dopasowywać definicję PRECISION dopasowując ilość wyrazów ciągu taylora).

Podsumowanie

GCC jest bardzo prosty w obsłudze, mimo ogromnego manuala. Jedyne co trzeba wiedzieć, że jeśli chce się skompilować jakiś plik, wystarczy podać jego nazwę. Kolejność argumentów jest dowolna. Proces budowania można zakończyć w dowolnym momencie:

  • -c – nie konsoliduje (tworzy pliki .o)
  • -S – nie assembluje (pozostawia pliki .s)
  • -E – wyświetla (lub zapisuje do pliku, jeśli został podany plik wyjściowy, np. -o doKompilatora.i) kod po przejściu przez preprocesor

Flaga -fdump-tree-all pozostawia pliki z poszczególnymi krokami kompilacji i optymalizacji.

Najprościej jest użyć flagi -O2 do optymalizacji (jak wspomniałem: -O3 bywa ryzykowne). Kompilować można wiele plików jednocześnie. Nazwy plików wejściowych wpisuje się nie poprzedzając żadnym argumentem, każda nazwa, której gcc nie będzie umiał dopasować do któregoś argumentu (np. -o ), będzie uznana za nazwę pliku wejściowego. Kolejność argumentów nie ma znaczenia. Przy np. rozwiązywaniu zadań konkursowych najprościej wywołać kompilację poleceniem:

gcc z1.c -O2 -o z1

Jeśli nie podamy nazwy pliku wyjściowego (-o nazwa), użyta zostanie domyślna nazwa a.out (a.exe na Windowsie). Można też dodać -march=native, które dostosuje kod do danego procesora, co może (ale nie musi, co pokaże benchmark) przyspieszyć wykonywanie kodu wynikowego na danej platformie.

Dzięki temu, że gcc/g++ zwracają numery błędów, można ich użyć do pewnej automatyzacji, np. od razu po kompilacji uruchomić, jeśli się powiedzie:

gcc z1.c -O2 -o z1 && time ./z1&lt;z1.in

Jeśli kompilacja się powiedzie, skompilowany program zostanie uruchomiony z wejściem z pliku z1.in i zmierzony zostanie czas wykonywania. Jeśli kompilacja się nie powiedzie, program nie będzie uruchomiony.

Jeśli program składa się z kilku plików, można je razem skompilować:

gcc z1a.c z1b.c -O2 -o z1

Lub też za pomocą wyrażenia regularnego:

gcc *.c -O2 -o z1 #wszystkie w tym folderze
gcc z1*.c -O2 -o z1 #zaczynające się na z1

Aby zobaczyć pliki przejściowe optymalizacji:

gcc z1.c -fdump-tree-all -o z1

Aby gcc nie usuwał tymczasowych plików (poza tymi z optymalizacjami, zostawia pliki .i, .ii, .s i .o):

gcc z1.c -save-temps -o z1

Jeśli chcemy zakończyć proces budowania na np. preprocesorze (np. w celu zbadania poprawności kodu po wstawieniu makr), dodajemy -E i wyrzucamy do jakiegoś pliku:

gcc z1.c -E -o z1.i

Pliki z rozszerzeniem .i (lub .ii w przypadku C++) w rozszerzeniu są plikami które już przeszły przez preprocesor. Rozszerzenie .s oznacza że jest to plik dla assemblera. Pliki .o to obiekty tymczasowe, skompilowane, po assemblerze, ale nie zlinkowane. Dopiero konsolidator (linker) tworzy wykonywalny program.

Jeśli mając system 64-bitowy chcemy skompilować program dla systemu 32-bitowego, musimy mieć pobrane wszystkie biblioteki dla 32-bitowego systemu i dodać do flag kompilacji -m32:

gcc z1.c -m32 -o z1.i

Kolejność argumentów wywołania (w przypadku gcc i g++) jest dowolna, jednak są opcje dwuargumentowe, takie jak właśnie nazwa pliku wyjściowego: -o nazwa_pliku.

Jednak jeśli kompiluje się już większe projekty z użyciem zewnętrznych bibliotek, lepiej zaopatrzyć się w jakieś IDE. Ręczne wpisywanie poleceń kompilacji nie jest zbyt wygodne na dłuższą metę, zwłaszcza gdy używa się kilku bibliotek. Jednak używanie kompilatora z poziomu konsoli w wielu przypadkach o wiele lepiej się sprawdza niż używanie IDE, np. konkursy, zabawa z kompilatorem, pisanie prostych algorytmów i małych programów.

Znajomość całego procesu krok po kroku może się przydać przy robieniu bardziej zaawansowanych lub niskopoziomowych rzeczy takich jak tworzenie własnego menedżera projektów, mechanizm konkursowy, czy też debugowanie kodu na poziomie kodu przejściowego i assemblera. Znajomość flag optymalizacji pozwala lepiej je dopasować do konkretnych algorytmów.

Konferencje

Laboratorium BRAMA zaprasza na kolejne spotkanie z cyklu Linux w Bramie. W planach prelekcja o kompilacji jądra oraz… Palm Pre! Jak co miesiąc w ostatni piątek, 27.XI zapraszamy wszystkich zainteresowanych na linuksowe spotkanie w Laboratorium BRAMA, na Wydziale Elektroniki i Technik Informacyjnych Politechniki Warszawskiej (Warszawa, ul. Nowowiejska 15/19, piwnica, pokój 039).

W planie, poza standardowymi punktami (ogólne informacje o WiOO, pomoc w problemach czy instalacji, kawa i ciastka), w tym miesiącu Kamil “D3LLF” Izdebski poprowadzi wyczerpującą prelekcję o kompilacji jądra, a Andrzej “balrog” Zaborowski przyniesie i pokaże swojego Palma Pre.

Jak zawsze, czekamy na Was od 10:00 do 22:00. Możecie wziąć swój sprzęt, lub pobawić się pingwinem na sprzęcie Laboratorium. Jak zwykle, wieczorem (pewnie koło 20:00) – mecze OpenArena na naszym serwerze (zapraszamy też gości zdalnych!).

Na koniec zaś – informacyjnie: dostępne już są zdjęcia i relacja z Drugich Urodzin LwB, oraz wyniki Great Netbook Linux Smackdown. Zapraszamy!

Programowanie

Większość oprogramowania dla Linuksa jak i sam system dostępne jest w postaci kodu źródłowego. Program komputerowy jest zapisany w pewnym języku programowania, zazwyczaj jako tekst, ale też jako inne dane (np. litery, cyfry, symbole) w postaci czytelnej dla człowieka. W takiej postaci program jest zrozumiały dla człowieka (programisty znającego język programowania) jednakże bezpośrednio jest bezużyteczny dla maszyny np. komputera.

Kod źródłowy jest przetwarzany na kod maszynowy zrozumiały dla (procesora) maszyny, przez translator (kompilator lub asembler), lub jest analizowany i wykonywany przez specjalny program zwany interpreterem, może być też przetworzony na kod pośredni. W tym artykule zajmiemy się kompilacją programów pod Linuksem.

Pierwszym krokiem będzie oczywiście pobranie jakiejś aplikacji. Ogromną bazę programów można znaleźć na stronie SourceForge.net. Zamieszczone tam pliki najczęściej są w postaci archiwów tar.gz lub tar.bz2. W archiwach tych znajdują się pliki zawierające źródła, pliki Makefile, instrukcje jak kompilować dany program lub inne pliki zawierające informacje od autora (TODO, ChangeLog).

Pobraną aplikację kopiujemy do katalogu /usr/src, gdzie ją rozpakujemy. Operacje te powinniśmy wykonywać jako użytkownik root, gdyś zazwyczaj tylko on ma tam prawa do zapisu. W katalogu tym należy rozpakować dane archiwum. Możemy skorzystać z aplikacji graficznych takich jak Ark, czy file-roller. W przypadku konsoli pomoże nam Midnight Commander (polecenie mc) lub tar, gzip, bzip2. W przypadku archiwum tar.gz wydajemy polecenie: tar -xvzf archiwum.tar.gz np:

[root@muszelka src]# tar -xvzf ekg-current.tar.gz
ekg-20080114/
ekg-20080114/configure
ekg-20080114/config.sub
ekg-20080114/config.guess
ekg-20080114/examples/
ekg-20080114/examples/.cvsignore
ekg-20080114/examples/httphash.c
ekg-20080114/examples/conn-async.c
ekg-20080114/examples/register.c
...

Opis przełączników dla tar:

  • x – oznacza, żeby dany plik został rozpakowany
  • v – pokaże nam listę plików i szczegóły
  • z – filtrowanie pakietów przez gzip
  • f – oznacza, że plik archiwum jest plikiem lokalnym

W wyniku powyższych operacji otrzymaliśmy katalog, w którym znajdują się źródła programu, licencje oraz pliki informujące jak dany program skompilować i zainstalować. Najczęściej jest to plik INSTALL lub README. Warto je poczytać, gdyż znajdziemy tam szczegółowe informacje na temat kompilacji danej aplikacji. Typowa kompilacja rozpoczyna się od wydania polecenia ./configure w katalogu ze źródłami. Jest to prosty skrypt Configure, który sprawdza nasz system pod względem, czy posiada wszystkie potrzebne biblioteki, sprawdza dostępność kompilatorów, przygotowuje źródła do kompilacji w danym systemie. Cały proces wygląda mniej więcej tak:

[root@muszelka ekg-20080114]# ./configure
checking for gcc... gcc
checking for C compiler default output file name... a.out
checking whether the C compiler works... yes
checking whether we are cross compiling... no
checking for suffix of executables...
checking for suffix of object files... o
checking whether we are using the GNU C compiler... yes
checking whether gcc accepts -g... yes
checking for gcc option to accept ISO C89... none needed
checking how to run the C preprocessor... gcc -E
checking for a BSD-compatible install... /usr/bin/install -c
checking whether ln -s works... yes
checking for an ANSI C-conforming const... yes
checking for gmake... /usr/bin/gmake
checking for ar... ar
checking for strip... strip
checking for t_accept in -lnsl... no
checking for socket in -lsocket... no
checking for __inet_addr in -lbind... no
checking for inet_pton... yes
checking for pkg-config... pkg-config
...

Bardzo często zdarza się, że configure zatrzyma się w pewnym momencie i pokaże listę błędów, które uniemożliwiają nam skompilowanie programu:

checking for libgnutls-config... no
checking for libgnutls - version &gt;= 1.0.0... no
*** The libgnutls-config script installed by LIBGNUTLS could not be found
*** If LIBGNUTLS was installed in PREFIX, make sure PREFIX/bin is in
*** your path, or set the LIBGNUTLS_CONFIG environment variable to the
*** full path to libgnutls-config.
configure: WARNING:
***
*** libgnutls was not found. You may want to get it from
*** ftp://ftp.gnutls.org/pub/gnutls/

Jak widać powyżej brakuje nam biblioteki libgnutls w wersji większej lub równej 1.0.0. Configure również pokazało, gdzie należy szukać podanych plików. Po zainstalowaniu biblioteki odpalamy jeszcze raz ten skrypt i czytamy na rezultaty. Być może teraz znajdzie brak innych plików, pakietów, które również będzie trzeba doinstalować lub skompilować. Jeśli wszystkie zależności zostaną spełnione naszym oczom powinien pokazać się następujący efekt:

configure: creating ./config.status
config.status: creating src/Makefile
config.status: creating Makefile
config.status: creating examples/Makefile
config.status: creating config.h

configured options:
 - openssl: enabled
 - ioctld: disabled
 - python: disabled
 - zlib: enabled
 - pthread: disabled
 - libungif: disabled
 - libjpeg: enabled
 - ui-readline: disabled
 - ui-ncurses: enabled (default)
 - ui-gtk: enabled
 - aspell: disabled

Good - your configure finished. Start make now

Sama kompilacja ogranicza się do wydania polecenia make:

[root@muszelka ekg-20080114]# make
cd src &amp;&amp; /usr/bin/gmake all
gmake[1]: Wejście do katalogu `/usr/src/ekg-20080114/src'
gcc  -MM -I.. -g -O2 -Wall    -I/usr/include/gtk-2.0 -I/usr/lib/gtk-2.0/include -I/usr/include/atk-1.0 -I/usr/include/cairo -I/usr/include/pango-1.0 -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include   -DDATADIR=\"/usr/local/share/ekg\" -DSYSCONFDIR=\"/usr/local/etc\" stuff.c commands.c events.c themes.c vars.c dynstuff.c userlist.c ekg.c xmalloc.c mail.c msgqueue.c emoticons.c configfile.c simlite.c ../compat/strlcat.c ../compat/strlcpy.c ui-ncurses.c ui-gtk.c ui-gtk-maingui.c ui-gtk-xtext.c ui-gtk-chanview.c ui-gtk-palette.c ui-gtk-bindings.c ui-batch.c ui-none.c log.c comptime.c 1&gt; .depend
gcc  -I.. -g -O2 -Wall    -I/usr/include/gtk-2.0 -I/usr/lib/gtk-2.0/include -I/usr/include/atk-1.0 -I/usr/include/cairo -I/usr/include/pango-1.0 -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include   -DDATADIR=\"/usr/local/share/ekg\" -DSYSCONFDIR=\"/usr/local/etc\"   -c -o stuff.o stuff.c
gcc  -I.. -g -O2 -Wall    -I/usr/include/gtk-2.0 -I/usr/lib/gtk-2.0/include -I/usr/include/atk-1.0 -I/usr/include/cairo -I/usr/include/pango-1.0 -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include   -DDATADIR=\"/usr/local/share/ekg\" -DSYSCONFDIR=\"/usr/local/etc\"   -c -o commands.o commands.c</pre>
Wszystko w tym momencie powinno kompilować się bez problemu. Jeśli wystąpią jakieś problemy, moga one wynikać ze starszej wersji kompilatora jaką posiadamy lub błędnie napisanych źródeł. Warto wtedy poczytać instrukcje dołączone do porgramu, lub przeszukać Internet, aby znaleźć rozwiązanie. Z reguły kompilacja kończy się poprawnie. Na konsoli powinniśmy zobaczyć następujący wynik:
<pre>make[2]: Nie nic do roboty w `all-am'.
make[2]: Leaving directory `/usr/src/ekg2-20050812'
make[1]: Leaving directory `/usr/src/ekg2-20050812'

Nasz program jest skompilowany. Czas go zainstalować. Instalacja skompilowanych źródeł musi odbywać się z poziomu użytkownika root dlatego. Teraz wystarczy wydać polecenie: make install. Teraz system kopiuje odpowiednie pliki binarne do katalogów systemu. Proces instalacji wygląda mniej więcej tak:

...
Making install in tests
make[3]: Wejście do katalogu `/usr/src/pidgin-2.3.1/libpurple/tests'
make[4]: Wejście do katalogu `/usr/src/pidgin-2.3.1/libpurple/tests'
make[4]: Nie ma nic do zrobienia w `install-exec-am'.
make[4]: Nie ma nic do zrobienia w `install-data-am'.
make[4]: Opuszczenie katalogu `/usr/src/pidgin-2.3.1/libpurple/tests'
make[3]: Opuszczenie katalogu `/usr/src/pidgin-2.3.1/libpurple/tests'
...

Po całej tej operacji wystarczy w konsoli napisać nazwę programu i się on uruchomi. Jeśli program się nam znudzi i chcemy go usunąć to wystarczy wejść do katalogu ze źródłem programu (domyślnie /usr/src/nazwa_programu) i jako root wydajemy polecenie make uninstall.

przez -
0 455
Linux Tux

Rzeszowska Grupa Użytkowników Linuksa i Koło Naukowe Użytkowników Linuksa przy Politechnice Rzeszowskiej zapraszają na wykład poświęcony tematyce GNU/Linux. W trakcie wykładu przewidziano omówienie następujących tematów:

  • kompilacja jądra systemu (coś dla zaawansowanych użytkowników)
  • słuchanie wilka, czyli przedstawienie obsługi i możliwości odtwarzacza Amarok (coś dla nowych użytkowników Linuksa)

Wykład odbędzie się w środę 12 grudnia 2007 r. o godz. 18:00 w sali P9 budynku P Politechniki Rzeszowskiej (przy ulicy Poznańskiej). Wstęp oczywiście wolny.

przez -
0 758
Open Source

Instalacja programów ze źródeł może czasami sprawiać problemy początkującym użytkownikom systemu Linux. Na szczęście z pomocą przychodzi im program Kconfigure, który ukazał się właśnie w wersji 2.0. Aplikacja ułatwia konfigurowanie oraz instalację oprogramowania głównie dzięki temu, że posiada graficzny interfejs użytkownika. Jest wyjątkowo łatwa w użyciu – cała praca sprowadza się do kilku zaledwie kliknięć myszką.

Dodatkowo Kconfigure umożliwia wykorzystanie dodatkowych argumentów podczas kompilacji, zapis do logu przebiegu operacji, zawiera także wsparcie dla tworzenia i instalowania pakietów rpm/slack/deb. W najnowszej wersji – 2.0 – przebudowano graficzny interfejs użytkownika, dzięki czemu stał się on jeszcze bardziej przyjazny, dodano opcję kontroli, czy instalacja przebiegła pomyślnie (checkinstall), zaś pliki logów mogą być teraz zapisywane zarówno w formacie tekstowym, jak i w HTML. Więcej informacji

Polecane

Jesień Linuksowa

1 648
Polska Grupa Użytkowników Linuksa ma zaszczyt zaprosić na konferencję Jesień Linuksowa 2017, która odbędzie się w dniach 22 – 24 września 2017 roku. Jako...