C jest językiem średniego poziomu i potrzebuje kompilatora do przekształcenia go w kod wykonywalny, tak aby program mógł być uruchomiony na naszej maszynie.
Jak skompilować i uruchomić program w języku C?
Poniżej przedstawiamy kroki, które wykonujemy na komputerze Ubuntu z kompilatorem gcc.
- Najpierw tworzymy program w języku C za pomocą edytora i zapisujemy plik jako filename.c
$ vi filename.c
- Schemat po prawej stronie przedstawia prosty program do dodawania dwóch liczb.
- Potem skompiluj go używając poniższej komendy.
$ gcc –Wall filename.c –o filename
- Opcja -Wall włącza wszystkie komunikaty ostrzegawcze kompilatora. Opcja ta jest zalecana w celu wygenerowania lepszego kodu.
Opcja -o służy do określenia nazwy pliku wyjściowego. Jeśli nie użyjemy tej opcji, to zostanie wygenerowany plik wyjściowy o nazwie a.out.
- Po kompilacji zostanie wygenerowany plik wykonywalny, który uruchamiamy za pomocą poniższej komendy.
$ ./filename
Co dzieje się wewnątrz procesu kompilacji?
Kompilator przekształca program C w plik wykonywalny. Istnieją cztery fazy, w których program C staje się plikiem wykonywalnym:
- Preprocessing
- Kompilacja
- Assembly
- Linking
Wykonując poniższe polecenie, otrzymamy wszystkie pliki pośrednie w bieżącym katalogu wraz z plikiem wykonywalnym.
$gcc –Wall –save-temps filename.c –o filename
Następujący zrzut ekranu pokazuje wszystkie wygenerowane pliki pośrednie.
Po kolei zobaczmy, co zawierają te pliki pośrednie.
Pre-processing
Jest to pierwsza faza, przez którą przechodzi kod źródłowy. Faza ta obejmuje:
- Usuwanie komentarzy
- Rozszerzanie makr
- Rozszerzanie dołączonych plików.
- Kompilacja warunkowa
Przetworzone wstępnie dane wyjściowe są przechowywane w nazwie pliku.i. Zobaczmy, co jest wewnątrz filename.i: using $vi filename.i
W powyższym wyjściu plik źródłowy jest wypełniony mnóstwem informacji, ale na końcu nasz kod jest zachowany.
Analiza:
- printf zawiera teraz a + b zamiast add(a, b) to dlatego, że makra zostały rozszerzone.
- Komentarze są usunięte.
- #include<stdio.h> brakuje zamiast tego widzimy dużo kodu. Więc pliki nagłówkowe zostały rozszerzone i dołączone do naszego pliku źródłowego.
Kompilacja
Kolejnym krokiem jest skompilowanie pliku filename.i i wyprodukowanie pośredniego skompilowanego pliku wyjściowego filename.s. Ten plik jest w instrukcjach poziomu asemblera. Przejrzyjmy ten plik używając $vi filename.s
Zrzut pokazuje, że jest on w języku asemblera, który asembler może zrozumieć.
Assembly
W tej fazie plik filename.s jest brany jako dane wejściowe i zamieniany przez asembler w plik filename.o. Plik ten zawiera instrukcje poziomu maszynowego. Plik ten zawiera instrukcje poziomu maszynowego. W tej fazie, tylko istniejący kod jest konwertowany na język maszynowy, wywołania funkcji jak printf() nie są rozwiązywane. Obejrzyjmy ten plik używając $vi filename.o
Linkowanie
Jest to ostatnia faza, w której wykonywane jest łączenie wywołań funkcji z ich definicjami. Linker wie, gdzie wszystkie te funkcje są zaimplementowane. Linker wykonuje również dodatkową pracę, dodaje do naszego programu dodatkowy kod, który jest wymagany podczas uruchamiania i kończenia programu. Na przykład, istnieje kod, który jest wymagany do ustawienia środowiska, jak na przykład przekazywanie argumentów linii poleceń. Zadanie to można łatwo sprawdzić za pomocą $size filename.o oraz $size filename. Dzięki tym komendom wiemy, że w jaki sposób plik wyjściowy zwiększa się z pliku obiektowego do pliku wykonywalnego. Dzieje się tak z powodu dodatkowego kodu, który linker dodaje do naszego programu.