Kompilacja programu C ++ obejmuje trzy kroki:
Przetwarzanie wstępne: preprocesor pobiera plik kodu źródłowego C ++ i zajmuje się #include
s, #define
si innymi dyrektywami preprocesora. Wyjściem tego kroku jest „czysty” plik C ++ bez dyrektyw przedprocesowych.
Kompilacja: kompilator pobiera dane wyjściowe procesora i tworzy z niego plik obiektowy.
Łączenie: konsolidator pobiera pliki obiektów wygenerowane przez kompilator i tworzy bibliotekę lub plik wykonywalny.
Przetwarzanie wstępne
Preprocesor obsługuje dyrektywy preprocesora , takie jak #include
i #define
. Jest niezależny od składni języka C ++, dlatego należy go używać ostrożnie.
To działa na jednym pliku źródłowym C ++ w czasie, poprzez zastąpienie #include
dyrektyw z treścią odpowiednich plików (zwykle jest to tylko deklaracje), robi wymianę makr ( #define
), a następnie wybierając różne fragmenty tekstu w zależności od #if
, #ifdef
i #ifndef
wskazówki.
Preprocesor działa na strumieniu tokenów przetwarzania wstępnego. Makropodstawienie definiuje się jako zastępowanie tokenów innymi tokenami (operator ##
umożliwia scalenie dwóch tokenów, gdy ma to sens).
Po tym wszystkim preprocesor wytwarza pojedyncze wyjście, które jest strumieniem tokenów wynikających z transformacji opisanych powyżej. Dodaje także specjalne znaczniki, które informują kompilator, skąd pochodzi każda linia, aby mógł używać tych znaków do generowania rozsądnych komunikatów o błędach.
Na tym etapie można popełnić pewne błędy dzięki sprytnemu zastosowaniu dyrektyw #if
i #error
.
Kompilacja
Etap kompilacji wykonywany jest na każdym wyjściu preprocesora. Kompilator analizuje czysty kod źródłowy C ++ (teraz bez dyrektyw preprocesora) i konwertuje go na kod asemblera. Następnie wywołuje bazowe zaplecze (asembler w toolchain), które składa ten kod w kod maszynowy, tworząc rzeczywisty plik binarny w jakimś formacie (ELF, COFF, a.out, ...). Ten plik obiektowy zawiera skompilowany kod (w formie binarnej) symboli zdefiniowanych na wejściu. Symbole w plikach obiektowych są określane według nazwy.
Pliki obiektowe mogą odnosić się do symboli, które nie są zdefiniowane. Dzieje się tak, gdy używasz deklaracji i nie podajesz jej definicji. Kompilatorowi to nie przeszkadza i chętnie utworzy plik obiektowy, o ile kod źródłowy jest poprawnie sformułowany.
Kompilatory zwykle pozwalają zatrzymać kompilację w tym momencie. Jest to bardzo przydatne, ponieważ dzięki niemu możesz skompilować każdy plik kodu źródłowego osobno. Zaletą tego jest to, że nie trzeba ponownie kompilować wszystkiego, jeśli zmienisz tylko jeden plik.
Wytworzone pliki obiektowe można umieścić w specjalnych archiwach zwanych bibliotekami statycznymi, aby ułatwić ich późniejsze użycie.
Na tym etapie zgłaszane są „zwykłe” błędy kompilatora, takie jak błędy składniowe lub błędy rozwiązywania przeciążenia.
Łączenie
Linker jest tym, co wytwarza ostateczne dane wyjściowe kompilacji z plików obiektowych utworzonych przez kompilator. To wyjście może być biblioteką współdzieloną (lub dynamiczną) (i chociaż nazwa jest podobna, nie mają wiele wspólnego z bibliotekami statycznymi wspomnianymi wcześniej) ani plikiem wykonywalnym.
Łączy wszystkie pliki obiektowe, zastępując odwołania do niezdefiniowanych symboli poprawnymi adresami. Każdy z tych symboli można zdefiniować w innych plikach obiektowych lub w bibliotekach. Jeśli są zdefiniowane w bibliotekach innych niż biblioteka standardowa, musisz o nich powiedzieć linkerowi.
Na tym etapie najczęstszymi błędami są brakujące definicje lub duplikaty definicji. To pierwsze oznacza, że albo definicje nie istnieją (tzn. Nie są zapisane), albo że pliki obiektów lub biblioteki, w których się znajdują, nie zostały przekazane linkerowi. To ostatnie jest oczywiste: ten sam symbol został zdefiniowany w dwóch różnych plikach obiektowych lub bibliotekach.
Temat ten jest omawiany w CProgramming.com:
https://www.cprogramming.com/compilingandlinking.html
Oto, co napisał tam autor:
źródło
Na standardowym froncie:
jednostka tłumaczenie jest kombinacja plików źródłowych, zawartych nagłówków i pliki źródłowe pomniejszonych o liniach źródłowych pominiętych przez włączenie warunkowego dyrektywy preprocesora.
standard określa 9 etapów tłumaczenia. Pierwsze cztery odpowiadają przetwarzaniu wstępnemu, następne trzy to kompilacja, następne to tworzenie szablonów ( tworzenie jednostek tworzenia ), a ostatnie to łączenie.
W praktyce ósma faza (tworzenie szablonów) jest często wykonywana podczas procesu kompilacji, ale niektóre kompilatory opóźniają ją do fazy łączenia, a niektóre rozkładają na dwie części.
źródło
Chude jest to, że procesor ładuje dane z adresów pamięci, przechowuje dane na adresy pamięci i wykonuje instrukcje sekwencyjnie poza adresami pamięci, z pewnymi warunkowymi skokami w sekwencji przetwarzanych instrukcji. Każda z tych trzech kategorii instrukcji wymaga obliczenia adresu do komórki pamięci, która ma być użyta w instrukcji maszyny. Ponieważ instrukcje maszynowe mają zmienną długość w zależności od konkretnej instrukcji, a ponieważ podczas tworzenia naszego kodu maszynowego łączymy je ze sobą o zmiennej długości, proces obliczania i budowania adresów wymaga dwuetapowego procesu.
Najpierw ustalamy przydział pamięci najlepiej, jak potrafimy, zanim będziemy mogli dowiedzieć się, co dokładnie dzieje się w każdej komórce. Rozumiemy bajty, słowa lub cokolwiek, co tworzy instrukcje, literały i wszelkie dane. Po prostu zaczynamy przydzielać pamięć i budować wartości, które będą tworzyć program w miarę upływu czasu, i zanotuj miejsce, w którym musimy wrócić i naprawić adres. W tym miejscu umieszczamy manekina, aby po prostu wstawić lokalizację, abyśmy mogli nadal obliczać rozmiar pamięci. Na przykład nasz pierwszy kod maszynowy może zająć jedną komórkę. Następny kod maszynowy może zająć 3 komórki, w tym jedną komórkę kodu maszynowego i dwie komórki adresowe. Teraz naszym wskaźnikiem adresu jest 4. Wiemy, co dzieje się w komórce maszyny, która jest kodem operacyjnym, ale musimy poczekać, aby obliczyć, co idzie w komórkach adresowych, aż będziemy wiedzieć, gdzie te dane będą znajdować się, tj.
Gdyby istniał tylko jeden plik źródłowy, kompilator mógłby teoretycznie wytwarzać w pełni wykonywalny kod maszynowy bez linkera. W procesie dwuprzebiegowym może obliczyć wszystkie rzeczywiste adresy do wszystkich komórek danych, do których odwołuje się dowolne obciążenie maszyny lub instrukcje przechowywania. I może obliczyć wszystkie adresy bezwzględne, do których odnoszą się instrukcje bezwzględnego skoku. Tak działają prostsze kompilatory, jak ten w Forth, bez linkera.
Linker to coś, co pozwala na osobne kompilowanie bloków kodu. Może to przyspieszyć cały proces budowania kodu i pozwala na pewną elastyczność przy późniejszym użyciu bloków, innymi słowy można je przenieść do pamięci, na przykład dodając 1000 do każdego adresu, aby przeskoczyć blok o 1000 komórek adresu.
Tak więc to, co generuje kompilator, to nieobrobiony kod maszynowy, który nie jest jeszcze w pełni zbudowany, ale jest tak ułożony, abyśmy znali rozmiar wszystkiego, innymi słowy, abyśmy mogli zacząć obliczać, gdzie będą znajdować się wszystkie adresy bezwzględne. kompilator wyświetla również listę symboli, które są parami nazwa / adres. Symbole odnoszą się do przesunięcia pamięci w kodzie maszynowym w module o nazwie. Przesunięcie jest bezwzględną odległością do miejsca pamięci symbolu w module.
Tam dochodzimy do linkera. Linker najpierw uderza wszystkie te bloki kodu maszynowego razem od końca do końca i zapisuje, gdzie zaczyna się każdy z nich. Następnie oblicza adresy, które mają zostać naprawione, sumując względne przesunięcie w module i bezwzględną pozycję modułu w większym układzie.
Oczywiście uprościłem to, abyś mógł to zrozumieć, i celowo nie użyłem żargonu plików obiektowych, tablic symboli itp., Co jest dla mnie częścią zamieszania.
źródło
GCC kompiluje program C / C ++ w plik wykonywalny w 4 krokach.
Na przykład
gcc -o hello hello.c
przeprowadza się w następujący sposób:1. Wstępne przetwarzanie
Przetwarzanie wstępne za pomocą GNU C Preprocessor (
cpp.exe
), który obejmuje nagłówki (#include
) i rozwija makra (#define
).Wynikowy plik pośredni „hello.i” zawiera rozszerzony kod źródłowy.
2. Kompilacja
Kompilator kompiluje wstępnie przetworzony kod źródłowy w kod zestawu dla określonego procesora.
Opcja -S określa wytwarzanie kodu asemblera zamiast kodu obiektowego. Wynikowy plik zestawu to „hello.s”.
3. Zgromadzenie
as.exe
Asembler ( ) konwertuje kod asemblera na kod maszynowy w pliku obiektowym „hello.o”.4. Linker
Na koniec linker (
ld.exe
) łączy kod obiektu z kodem biblioteki, aby utworzyć plik wykonywalny „hello”.źródło
Spójrz na adres URL: http://faculty.cs.niu.edu/~mcmahon/CS241/Notes/compile.html
Kompletny proces kompilacji C ++ został wyraźnie przedstawiony w tym adresie URL.
źródło