Szukaj…


Wprowadzenie

Język C jest tradycyjnie językiem skompilowanym (w przeciwieństwie do tłumaczonego). Standard C definiuje fazy tłumaczenia , a produktem ich zastosowania jest obraz programu (lub program skompilowany). W fazy są wymienione w §5.1.1.2.

Uwagi

Rozszerzenie nazwy pliku Opis
.c Plik źródłowy. Zwykle zawiera definicje i kod.
.h Plik nagłówka. Zwykle zawiera deklaracje.
.o Plik obiektowy. Skompilowany kod w języku maszynowym.
.obj Alternatywne rozszerzenie dla plików obiektowych.
.a Plik biblioteki. Pakiet plików obiektowych.
.dll Biblioteka Dynamic-Link w systemie Windows.
.so Obiekt współdzielony (biblioteka) w wielu systemach uniksopodobnych.
.dylib Biblioteka Dynamic Link na OSX (wariant Unix).
.exe , .com Plik wykonywalny Windows. Utworzony przez połączenie plików obiektów i plików biblioteki. W systemach uniksowych nie ma specjalnego rozszerzenia nazwy pliku wykonywalnego.
Flagi kompilatora POSIX c99 Opis
-o filename Nazwa pliku wyjściowego, np. ( bin/program.exe , program )
-I directory szukaj nagłówków w direrctory .
-D name zdefiniuj name makra
-L directory wyszukaj biblioteki w directory .
-l name libname biblioteki biblioteki linków.

Kompilatory na platformach POSIX (Linux, mainframe, Mac) zwykle akceptują te opcje, nawet jeśli nie są nazywane c99 .

Flagi GCC (kolekcja kompilatora GNU) Opis
-Wall Włącza wszystkie komunikaty ostrzegawcze, które są powszechnie akceptowane, aby były przydatne.
-Wextra Włącza więcej komunikatów ostrzegawczych, może być zbyt głośny.
-pedantic Wymuś ostrzeżenia, gdy kod narusza wybrany standard.
-Wconversion Włącz ostrzeżenia o niejawnej konwersji, używaj ostrożnie.
-c Kompiluje pliki źródłowe bez łączenia.
-v Wyświetla informacje o kompilacji.
  • gcc akceptuje flagi POSIX i wiele innych.
  • Wiele innych kompilatorów na platformach POSIX ( clang , kompilatory specyficzne dla dostawców) również używa flag wymienionych powyżej.
  • Zobacz także Wywoływanie GCC, aby uzyskać więcej opcji.
Flagi TCC (kompilator Tiny C) Opis
-Wimplicit-function-declaration Ostrzegaj o niejawnej deklaracji funkcji.
-Wunsupported Ostrzegaj o nieobsługiwanych funkcjach GCC, które są ignorowane przez TCC.
-Wwrite-strings Ustaw stałe łańcuchowe typu const char * zamiast char *.
-Werror Przerwij kompilację, jeśli pojawią się ostrzeżenia.
-Wall Aktywuj wszystkie ostrzeżenia, z wyjątkiem -Werror , -Wunusupported i -Wwrite strings .

Linker

Zadaniem linkera jest połączenie kilku plików obiektowych (plików .o ) w binarny plik wykonywalny. Proces łączenia polega głównie na rozwiązywaniu adresów symbolicznych na adresy numeryczne . Wynikiem procesu łączenia jest zwykle program wykonywalny.

Podczas procesu łączenia linker pobierze wszystkie moduły obiektowe określone w wierszu poleceń, doda z przodu jakiś specyficzny dla systemu kod startowy i spróbuje rozwiązać wszystkie odwołania zewnętrzne w module obiektowym za pomocą definicji zewnętrznych w innych plikach obiektowych (plikach obiektowych może być określony bezpośrednio w wierszu poleceń lub może zostać domyślnie dodany przez biblioteki). Następnie przypisze adresy ładowania do plików obiektowych, to znaczy określa, gdzie kod i dane znajdą się w przestrzeni adresowej gotowego programu. Po uzyskaniu adresów ładowania może zastąpić wszystkie adresy symboliczne w kodzie obiektowym „rzeczywistymi” numerycznymi adresami w przestrzeni adresowej celu. Program jest teraz gotowy do uruchomienia.

Obejmuje to zarówno pliki obiektowe, które kompilator utworzył z plików kodu źródłowego, jak i pliki obiektowe, które zostały wstępnie skompilowane i zebrane w pliki bibliotek. Pliki te mają nazwy, które kończą się na .a lub .so , i zwykle nie musisz o nich wiedzieć, ponieważ linker wie, gdzie znajduje się większość z nich, i w razie potrzeby automatycznie je połączy.

Domniemane wywołanie linkera

Podobnie jak preprocesor, linker jest osobnym programem, często nazywanym ld (ale Linux używa na przykład collect2 ). Podobnie jak preprocesor, linker jest wywoływany automatycznie podczas korzystania z kompilatora. Zatem normalny sposób korzystania z linkera jest następujący:

% gcc foo.o bar.o baz.o -o myprog

Ten wiersz informuje kompilator o połączeniu ze sobą trzech plików obiektowych ( foo.o , bar.o i baz.o ) w binarny plik wykonywalny o nazwie myprog . Teraz masz plik o nazwie myprog , który możesz uruchomić i mam nadzieję, że zrobi coś fajnego i / lub przydatnego.

Jawne wywołanie linkera

Możliwe jest bezpośrednie wywołanie linkera, ale rzadko jest to wskazane i zazwyczaj jest bardzo specyficzne dla platformy. Oznacza to, że opcje działające w systemie Linux niekoniecznie działają w systemach Solaris, AIX, macOS, Windows i podobnie w przypadku innych platform. Jeśli pracujesz z GCC, możesz użyć gcc -v aby zobaczyć, co jest wykonywane w twoim imieniu.

Opcje dla linkera

Linker bierze również kilka argumentów, aby zmodyfikować swoje zachowanie. Następujące polecenie powiedziałoby gcc, aby foo.o i bar.o , ale zawierało także bibliotekę ncurses .

% gcc foo.o bar.o -o foo -lncurses

Jest to faktycznie (mniej więcej) równoważne z

% gcc foo.o bar.o /usr/lib/libncurses.so -o foo

(chociaż libncurses.so może być libncurses.a , który jest tylko archiwum utworzonym za pomocą ar ). Zauważ, że powinieneś wymienić biblioteki (według nazwy ścieżki lub opcji -lname ) po plikach obiektów. W przypadku bibliotek statycznych kolejność ich określania ma znaczenie; często w przypadku bibliotek współdzielonych kolejność nie ma znaczenia.

Zauważ, że w wielu systemach, jeśli używasz funkcji matematycznych (z <math.h> matematyki.h <math.h> ), musisz podać -lm aby załadować bibliotekę matematyki - ale Mac OS X i macOS Sierra nie wymagają tego. Istnieją inne biblioteki, które są oddzielnymi bibliotekami w systemie Linux i innych systemach uniksowych, ale nie w macOS - wątki POSIX i POSIX w czasie rzeczywistym, a biblioteki sieciowe są przykładami. W związku z tym proces łączenia różni się w zależności od platformy.

Inne opcje kompilacji

To wszystko, co musisz wiedzieć, aby rozpocząć kompilację własnych programów C. Zasadniczo zalecamy również użycie opcji wiersza polecenia -Wall :

% gcc -Wall -c foo.cc

Opcja -Wall powoduje, że kompilator ostrzega o legalnych, ale wątpliwych konstrukcjach kodu i pomaga bardzo wcześnie wykryć wiele błędów.

Jeśli chcesz, aby kompilator wyświetlał w tobie więcej ostrzeżeń (w tym zmienne, które zostały zadeklarowane, ale nie zostały użyte, zapominając o zwróceniu wartości itp.), Możesz użyć tego zestawu opcji, ponieważ -Wall , pomimo nazwy, nie zmienia się wszystkie możliwe ostrzeżenia na:

% gcc -Wall -Wextra -Wfloat-equal -Wundef -Wcast-align -Wwrite-strings -Wlogical-op \
>     -Wmissing-declarations -Wredundant-decls -Wshadow …

Zauważ, że clang ma opcję -Weverything co naprawdę włącza wszystkie ostrzeżenia w clang .

Typy plików

Kompilowanie programów w C wymaga pracy z pięcioma rodzajami plików:

  1. Pliki źródłowe : Te pliki zawierają definicje funkcji i mają nazwy, które kończą się na .c zgodnie z konwencją. Uwaga: .cc i .cpp są plikami C ++; nie pliki C.
    np. foo.c

  2. Pliki nagłówkowe : pliki te zawierają prototypy funkcji i różne instrukcje preprocesora (patrz poniżej). Służą do umożliwienia plikom kodu źródłowego dostępu do funkcji zdefiniowanych zewnętrznie. Pliki nagłówkowe kończą się na .h zgodnie z konwencją.
    np. foo.h

  3. Pliki obiektowe : Te pliki są tworzone jako dane wyjściowe kompilatora. Składają się z definicji funkcji w formie binarnej, ale same nie są wykonywalne. Pliki obiektowe kończą się na .o zgodnie z konwencją, chociaż w niektórych systemach operacyjnych (np. Windows, MS-DOS) często kończą się na .obj .
    np. foo.o foo.obj

  4. Binarne pliki wykonywalne : są tworzone jako dane wyjściowe programu o nazwie „linker”. Linker łączy ze sobą kilka plików obiektowych, aby utworzyć plik binarny, który można wykonać bezpośrednio. Binarne pliki wykonywalne nie mają specjalnego przyrostka w systemach operacyjnych Unix, chociaż zazwyczaj kończą się .exe w systemie Windows.
    np. foo foo.exe

  5. Biblioteki : Biblioteka jest skompilowanym plikiem binarnym, ale sama w sobie nie jest plikiem wykonywalnym (tzn. Nie ma funkcji main() w bibliotece). Biblioteka zawiera funkcje, z których może korzystać więcej niż jeden program. Biblioteka powinna być dostarczana z plikami nagłówkowymi, które zawierają prototypy dla wszystkich funkcji w bibliotece; do tych plików nagłówkowych należy się odwoływać (np. #include <library.h> ) w każdym pliku źródłowym korzystającym z biblioteki. Linker musi następnie zostać skierowany do biblioteki, aby program mógł się pomyślnie skompilować. Istnieją dwa typy bibliotek: statyczna i dynamiczna.

    • Biblioteka statyczna : Biblioteka statyczna (pliki .a dla systemów POSIX i .lib dla Windows - nie mylić z plikami bibliotek importu DLL , które również używają rozszerzenia .lib ) jest wbudowana statycznie w program. Biblioteki statyczne mają tę zaletę, że program dokładnie wie, która wersja biblioteki jest używana. Z drugiej strony, rozmiary plików wykonywalnych są większe, ponieważ wszystkie używane funkcje biblioteki są uwzględnione.
      np. libfoo.a foo.lib
    • Biblioteka dynamiczna : Biblioteka dynamiczna (pliki .so dla większości systemów POSIX, .dylib dla OSX i .dll dla Windows) jest dynamicznie łączona przez program w czasie wykonywania. Są one czasami nazywane bibliotekami współdzielonymi, ponieważ jeden obraz biblioteki może być współdzielony przez wiele programów. Biblioteki dynamiczne mają tę zaletę, że zajmują mniej miejsca na dysku, jeśli biblioteka korzysta z więcej niż jednej aplikacji. Umożliwiają także aktualizacje bibliotek (poprawki błędów) bez konieczności odbudowywania plików wykonywalnych.
      np. foo.so foo.dylib foo.dll

Preprocesor

Zanim kompilator C rozpocznie kompilację pliku kodu źródłowego, plik jest przetwarzany w fazie wstępnego przetwarzania. Ta faza może być wykonana przez osobny program lub być całkowicie zintegrowana w jednym pliku wykonywalnym. W każdym razie jest on automatycznie wywoływany przez kompilator przed rozpoczęciem właściwej kompilacji. Faza przetwarzania wstępnego przekształca kod źródłowy w inny kod źródłowy lub jednostkę tłumaczeniową poprzez zastosowanie zamienników tekstowych. Możesz myśleć o tym jako o „zmodyfikowanym” lub „rozszerzonym” kodzie źródłowym. To rozszerzone źródło może istnieć jako prawdziwy plik w systemie plików lub może być przechowywane w pamięci przez krótki czas przed dalszym przetwarzaniem.

Komendy preprocesora rozpoczynają się od znaku funta („#”). Istnieje kilka poleceń preprocesora; dwa najważniejsze to:

  1. Definiuje :

    #define służy głównie do definiowania stałych. Na przykład,

    #define BIGNUM 1000000
    int a = BIGNUM; 
    

    staje się

    int a = 1000000;
    

    #define jest używane w ten sposób, aby uniknąć konieczności jawnego zapisywania stałej wartości w wielu różnych miejscach w pliku kodu źródłowego. Jest to ważne w przypadku, gdy będziesz musiał później zmienić stałą wartość; jest o wiele mniej podatny na błędy, aby go zmienić raz w #define , niż trzeba go zmieniać w wielu miejscach rozsianych po całym kodzie.

    Ponieważ #define wykonuje po prostu wyszukiwanie zaawansowane i zamienia, możesz także deklarować makra. Na przykład:

    #define ISTRUE(stm) do{stm = stm ? 1 : 0;}while(0)
    // in the function:
    a = x;
    ISTRUE(a);
    

    staje się:

    // in the function:
    a = x;
    do {
        a = a ? 1 : 0;
    } while(0);
    

    Przy pierwszym przybliżeniu efekt ten jest mniej więcej taki sam, jak w przypadku funkcji wstawianych, ale preprocesor nie zapewnia sprawdzania typu makr #define . Wiadomo, że jest to podatne na błędy, a ich stosowanie wymaga dużej ostrożności.

    Zwróć też uwagę, że preprocesor zastąpiłby również komentarze spacjami, jak wyjaśniono poniżej.

  2. Obejmuje :

    #include służy do uzyskiwania dostępu do definicji funkcji zdefiniowanych poza plikiem kodu źródłowego. Na przykład:

     #include <stdio.h> 
    

    powoduje, że preprocesor wkleja zawartość <stdio.h> do pliku kodu źródłowego w miejscu instrukcji #include , zanim zostanie skompilowana. #include jest prawie zawsze używane do dołączania plików nagłówkowych, które są głównie plikami zawierającymi deklaracje funkcji i instrukcje #define . W tym przypadku używamy #include , aby móc korzystać z funkcji takich jak printf i scanf , których deklaracje znajdują się w pliku stdio.h . Kompilatory C nie pozwalają na korzystanie z funkcji, chyba że została wcześniej zadeklarowana lub zdefiniowana w tym pliku; Instrukcje #include są zatem sposobem na ponowne użycie wcześniej napisanego kodu w programach C.

  3. Operacje logiczne :

    #if defined A || defined B
    variable = another_variable + 1;
    #else
    variable = another_variable * 2;
    #endif
    

    zostanie zmieniony na:

    variable = another_variable + 1;
    

    jeśli A lub B zostały wcześniej zdefiniowane gdzieś w projekcie. Jeśli tak nie jest, oczywiście preprocesor to zrobi:

    variable = another_variable * 2;
    

    Jest to często używane w kodzie, który działa w różnych systemach lub kompiluje na różnych kompilatorach. Ponieważ istnieją definicje globalne, które są specyficzne dla kompilatora / systemu, możesz przetestować te definicje i zawsze pozwolić kompilatorowi po prostu użyć kodu, który skompiluje na pewno.

  4. Komentarze

    Preprocesor zastępuje wszystkie komentarze w pliku źródłowym pojedynczymi spacjami. Komentarze są oznaczone // do końca wiersza lub kombinacją nawiasów otwierających /* i zamykających */ komentarzy.

Kompilator

Po tym, jak preprocesor C uwzględni wszystkie pliki nagłówkowe i rozszerzy wszystkie makra, kompilator może skompilować program. Robi to, zamieniając kod źródłowy C w plik kodu obiektowego, który jest plikiem z rozszerzeniem .o który zawiera binarną wersję kodu źródłowego. Jednak kod obiektowy nie jest bezpośrednio wykonywalny. Aby zrobić plik wykonywalny, musisz również dodać kod do wszystkich funkcji bibliotecznych, które były #include d do pliku (to nie to samo, co #include deklaracji, co robi #include ). To jest praca linkera .

Zasadniczo dokładna sekwencja wywoływania kompilatora C zależy w dużej mierze od używanego systemu. W tym przypadku korzystamy z kompilatora GCC, choć należy zauważyć, że istnieje wiele innych kompilatorów:

% gcc -Wall -c foo.c

% to wiersz polecenia systemu operacyjnego. Informuje to kompilator, aby uruchomił procesor wstępny na pliku foo.c a następnie skompilował go w pliku kodu obiektowego foo.o Opcja -c oznacza skompilowanie pliku kodu źródłowego do pliku obiektowego, ale nie wywoływanie programu łączącego. Ta opcja -c jest dostępna w systemach POSIX, takich jak Linux lub macOS; inne systemy mogą używać innej składni.

Jeśli cały program znajduje się w jednym pliku kodu źródłowego, możesz zamiast tego zrobić:

% gcc -Wall foo.c -o foo

Mówi to kompilatorowi, aby uruchomił procesor wstępny na foo.c , skompilował go, a następnie połączył go, aby utworzyć plik wykonywalny o nazwie foo . Opcja -o stwierdza, że następnym słowem w wierszu jest nazwa binarnego pliku wykonywalnego (programu). Jeśli nie podasz -o , (jeśli wpiszesz gcc foo.c ), plik wykonywalny zostanie nazwany a.out z powodów historycznych.

Zasadniczo kompilator wykonuje cztery kroki podczas konwertowania pliku .c plik wykonywalny:

  1. przetwarzanie wstępne - tekstowo rozwija #include dyrektyw i #define makra w pliku .c
  2. kompilacja - konwertuje program na asembler (możesz zatrzymać kompilator na tym etapie, dodając opcję -S )
  3. wirtualny plik dziennika - konwertuje wirtualny plik dziennika na kod maszynowy
  4. linkage - łączy kod obiektu z bibliotekami zewnętrznymi, aby utworzyć plik wykonywalny

Zauważ też, że nazwa kompilatora, którego używamy, to GCC, co oznacza zarówno „kompilator GNU C”, jak i „zbiór kompilatorów GNU”, w zależności od kontekstu. Istnieją inne kompilatory C. W systemach operacyjnych typu Unix wiele z nich ma nazwę cc , oznaczającą „kompilator C”, który często jest dowiązaniem symbolicznym do innego kompilatora. W systemach Linux cc jest często aliasem dla GCC. W systemie macOS lub OS-X wskazuje na brzęk.

Standardy POSIX obecnie nakazują c99 jako nazwę kompilatora C - domyślnie obsługuje standard C99. Wcześniejsze wersje POSIX wymagały c89 jako kompilatora. POSIX nakazuje również, aby ten kompilator rozumiał opcje -c i -o , których użyliśmy powyżej.


Uwaga: Opcja -Wall obecna w obu przykładach gcc nakazuje kompilatorowi wydrukowanie ostrzeżeń o wątpliwych konstrukcjach, co jest zdecydowanie zalecane. Dobrym pomysłem jest również dodanie innych opcji ostrzegawczych , np. -Wextra .

Fazy tłumaczenia

Począwszy od standardu C 2011, wymienionego w § 5.1.1.2 Fazy tłumaczenia , tłumaczenie kodu źródłowego na obraz programu (np. Plik wykonywalny) jest wyświetlane w 8 uporządkowanych krokach.

  1. Dane wejściowe pliku źródłowego są odwzorowywane na źródłowy zestaw znaków (w razie potrzeby). W tym kroku zastępowane są trygrafy.
  2. Linie kontynuacji (linie kończące się na \ ) są łączone z następną linią.
  3. Kod źródłowy jest przetwarzany na białe znaki i tokeny przetwarzania wstępnego.
  4. Stosowany jest preprocesor, który wykonuje dyrektywy, rozwija makra i stosuje pragmy. Każdy plik źródłowy pobrany przez #include przechodzi fazy tłumaczenia od 1 do 4 (w razie potrzeby rekurencyjnie). Wszystkie dyrektywy związane z preprocesorem są następnie usuwane.
  5. Wartości źródłowego zestawu znaków w stałych znaków i literałach ciągu są odwzorowywane na zestaw znaków wykonania.
  6. Literały łańcuchowe sąsiadujące ze sobą są konkatenowane.
  7. Kod źródłowy jest przetwarzany na tokeny, które zawierają jednostkę tłumaczącą.
  8. Odwołania zewnętrzne są rozwiązane i tworzony jest obraz programu.

Implementacja kompilatora C może łączyć ze sobą kilka kroków, ale wynikowy obraz musi nadal zachowywać się tak, jakby powyższe kroki miały miejsce osobno w powyższej kolejności.



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow