Moduł 6

Materiały do zajęć z Systemów operacyjnych prowadzonych na Wydziale Matematyki i Informatyki Uniwersytetu im. Adama Mickiewicza w Poznaniu.

« Wróć do spisu materiałów

Materiał do tych i kolejnych ćwiczeń zakłada, że umiesz swobodnie pisać programy w języku ANSI C. Jeśli nie czujesz się w tym zbyt pewnie, powtórz sobie materiał z zajęć z Podstaw programowania lub poczytaj literaturę we własnym zakresie.

Proces kompilacji programu w języku C

Proces kompilacji programu w języku C składa się zwykle z czterech etapów:[1]

  1. Prekompilacja (preprocessing). W czasie tego etapu przetwarzane są instrukcje preprocesora związane z dołączaniem plików, kompilacją warunkową czy makrami.
  2. Kompilacja (compilation). W czasie tego etapu kod przetworzony przez preprocesor jest przekształcany na kod źródłowy w języku assemblera.
  3. Assemblacja (assembly). W czasie tego etapu tworzone są pliki obiektów, zbudowane z przetworzonego na język maszynowy kodu assemblera.
  4. Linkowanie/konsolidacja (linking). W czasie tego etapu pliki obiektów i bibliotek łączone są tak, że powstaje jeden wykonywalny plik, w którym odwołania do elementów zdefiniowanych w innych plikach obiektów mogą być realizowane.

W praktyce, wiele funkcji jest wspólnych dla różnych programów (rozważmy chociażby funkcję printf z biblioteki standardowej). Na szczęście współczesne systemy dobrze radzą sobie z tzw. dynamicznym linkowaniem, którego idea polega na tym, że niektóre obiekty nie są dołączane do programu w procesie konsolidacji (statyczne linkowanie), lecz dopiero w czasie jego uruchamiania.

[1] Zwięzłe, ale szczegółowe wyjaśnienie zagadnień związanych z kompilacją można znaleźć choćby na stronie http://www.tenouk.com/ModuleW.html.

W przeszłości do kompilowania programów w języku C wykorzystywano uniksowy kompilator języka C o nazwie cc. Dziś jednak o wiele większą popularność zdobywa pakiet gcc (ang. GNU Compiler Collection), umożliwiający kompilację programów w różnych językach. Z niego będziemy korzystać w czasie tych i kolejnych ćwiczeń. Okazuje się, że w większości instalacji systemu GNU/Linux polecenia cc i gcc odwołują się do tego samego programu. Warto jednak zdawać sobie sprawę z historii polecenia cc.

Utwórz plik źródłowy program.c o treści:

#include <stdio.h>

main(){
    printf("Hello World\n");
}

Skompiluj program, wykonując polecenie

$ gcc program.c

Następnie uruchom go, wykonując polecenie

$ ./a.out

Dlaczego ostatnie z poleceń rozpoczyna się od ciągu ./?

Sprawdź co się stanie, jeśli nie dołączysz do pliku źródłowego deklaracji zawartych w nagłówku stdio.h. Czy wówczas program się skompiluje?

Korzystając z opcji -o programu gcc, zapisz efekt działania kompilatora do pliku o nazwie aplikacja.

W przypadku języka C przyjęło się, że:

  • kod źródłowy programu przechowuje się w pliku o rozszerzeniu .c,
  • przetworzony przez preprocesor kod przechowuje się w pliku o rozszerzeniu .i,
  • kod assemblera przechowuje się w pliku o rozszerzeniu .s,
  • plik obiektu ma rozszerzenie .o.

Sprawdź efekt działania kompilatora gcc w poszczególnych etapach kompilacji. W tym celu użyj:

  • opcji -c, aby zakończyć działanie kompilatora na procesie assemblacji,
  • opcji -S, aby zakończyć działanie kompilatora na procesie kompilacji,
  • opcji -E, aby zakończyć działanie kompilatora na procesie prekompilacji.

Jakie są domyślne nazwy plików, do których kompilator zapisuje efekt swojej pracy w poszczególnych przypadkach? Sprawdź, czy da się wpłynąć na te nazwy korzystając z opcji -o. Czy zawsze kompilator zapisuje dane do pliku? Jeśli nie, to jak sobie z tym poradzić?

Zobacz odpowiedź

W przypadku opcji -E kompilator wypisuje dane na wyjściu. Można je przekierować do odpowiedniego pliku standardowym przekierowaniem strumienia danych. W przypadku skorzystania z opcji --save-temps efekty poszczególnych etapów kompilacji są zachowywane.

Usuń wszystkie pliki związane z kompilacją pliku program.c, poza nim samym. Następnie skompiluj plik program.c, wywołując program gcc z opcją --save-temps. Skomentuj efekty.

Utwórz pliki hw.c, gb.c oraz app.c o treściach, kolejno:

#include <stdio.h>

int helloworld(){
    printf("Hello World\n");
    return 0;
}
#include <stdio.h>

int goodbye(){
    printf("Good bye\n");
    return 0;
}
int helloworld();
int goodbye();

int main(){
    helloworld();
    goodbye();
    return 0;
}

Następnie wykonaj polecenia:

$ gcc app.c
$ gcc -c app.c
$ gcc -c hw.c
$ gcc hw.o gb.c app.o

Które z poleceń nie zadziałały? Dlaczego? Zastanów się nad znaczeniem pozostałych.

Zobacz odpowiedź

Pierwsze polecenie zakończyło się niepoprawnie. Kompilator nie potrafił znaleźć definicji funkcji helloworld oraz goodbye.

Dołączanie bibliotek

W czasie pisania programu możemy korzystać z funkcji pochodzących z pewnych bibliotek, takich jak biblioteka standardowa (libc) czy biblioteka funkcji matematycznych (libm). Wymaga to dołączenia odpowiednich plików nagłówkowych (zwykle z rozszerzeniem .h), zawierających między innymi deklaracje funkcji bibliotecznych.[1]

Aby proces konsolidacji zakończył się powodzeniem, konieczne jest także poinformowanie kompilatora o tym, jakie biblioteki mają brać w nim udział. Kompilator gcc, o ile nie zdecydowano inaczej, zawsze dołącza w procesie konsolidacji bibliotekę libc, zawierającą definicje funkcji zadeklarowanych w wielu plikach nagłówkowych, np. stdlib.h oraz stdio.h.

Same biblioteki dzielimy jednak w ogólności na statyczne i dynamiczne. W przypadku tych pierwszych, wykorzystywane funkcje są dołączane na stałe do pliku wynikowego. Biblioteki dynamiczne umożliwiają współdzielenie swoich zasobów pomiędzy różnymi procesami, a tym samym zapewniają lepsze zarządzanie pamięcią. System operacyjny dołącza wówczas wymagane definicje do programu w momencie jego uruchamiania. Przyjęło się, że biblioteki statyczne są plikami o rozszerzeniu .a, a dynamiczne .so.

[1] Należy ostrożnie obchodzić się z pojęciami deklaracji i definicji zmiennych i funkcji. Pliki nagłówkowe zawierają deklaracje funkcji (a czasem też stałych), zaś biblioteki ich definicje.

Sprójrz na Ćwiczenie 6.1. Odwoływaliśmy się w nim do funkcji printf, a jednak nigdzie jej nie definiowaliśmy. Gdzie została zadeklarowana? Gdzie była zdefiniowana? Dlaczego udało się skompilować program bez dołączania dodatkowych bibliotek?

Aby dołączyć bibliotekę do programu, należy skorzystać z opcji -l programu gcc. Opcję tę można wywołać wielokrotnie, wskazując każdorazowo nazwę jednej biblioteki (z pominięciem przedrostka lib). Przyjęło się, że jeśli nazwa biblioteki jest jednoznakowa, to umieszcza się ją za opcją -l bez dodatkowego znaku odstępu, tworząc np. zbitkę -lm (dla biblioteki libm). Program gcc poprawnie interpretuje takie wywołania.

Utwórz plik źródłowy sin.c o następującej treści:

#include <stdio.h>
#include <math.h> // plik nagłówkowy math.h deklaruje stałe oraz
                  // funkcje zdefiniowane w bibliotece
                  // matematycznej (libm)

main(){
    int x = 5;
    printf("sin(%d)=%f\n", x, sin(x));
}

Spróbuj go skompilować, wykonując polecenia:

$ gcc -o sin sin.c
$ gcc -l m -o sin sin.c
$ gcc -o sin sin.c -l m
$ gcc -o sin sin.c -lm

Skomentuj efekty.

Korzystając z programu ldd sprawdź, z jakich bibliotek dynamicznych korzysta skompilowany przed chwilą program.

Wypełnij plik hellobye.c treścią:

#include <stdio.h>

int helloworld(){
    printf("Hello World\n");
    return 0;
}

int goodbye(){
    printf("Good bye\n");
    return 0;
}

Aby utworzyć z niego bibliotekę dynamiczną, wykonaj następujące polecenia:

$ gcc -c -fPIC hellobye.c
$ gcc -shared -o libhellobye.so hellobye.o

Utwórz plik hello.c o treści:

int helloworld();
int goodbye();

int main(){
    helloworld();
    goodbye();
    return 0;
}

i skompiluj go z wykorzystaniem utworzonej biblioteki dynamicznej libhellobye.so, wykonując polecenie:

$ gcc -L . hello.c -l hellobye

Za co odpowiada opcja -L? Dlaczego program nie uruchomi się? Zmodyfikuj wartość zmiennej systemowej LD_LIBRARY_PATH w taki sposób, aby możliwe było uruchomienie programu. Usuń plik biblioteki i sprawdź, czy program dalej będzie działać.

Zobacz odpowiedź

Program nie uruchomi się, bo poszukuje bibliotek dynamicznych w określonych katalogach. Katalog bieżący nie należy do tego zbioru. Aby możliwe było przeszukiwanie lokalnego katalogu, należy ustawić wartość zmiennej LD_LIBRARY_PATH na .. Zmienna ta pozwala ustalić, oddzielone przecinkami, ścieżki, które powinny być przeszukiwane przed standardowymi lokalizacjami. Opcja fPIC (ang. Position Independent Code) programu gcc wprowadza odpowiednie zarządzanie pamięcią.