Jak działa proces kompilacji / łączenia?

Jak działa proces kompilacji i łączenia?

(Uwaga: Jest to wpis do Stack Overflow ' S C++ FAQ . Jeśli chcesz krytykować pomysł dostarczenia FAQ w tej formie, to post na meta, który rozpoczął to wszystko , byłby miejscem, aby to zrobić. Odpowiedzi na to pytanie są monitorowane w C++ chatroom , gdzie pomysł FAQ zaczął się w pierwszej kolejności, więc Twoja odpowiedź jest bardzo prawdopodobne, aby przeczytać przez tych, którzy przyszli z pomysłem.)

6 answers

Kompilacja programu C++ składa się z trzech kroków:

  1. Preprocessor pobiera plik kodu źródłowego C++ i zajmuje się #includes, #define s i innymi dyrektywami preprocesora. Wynikiem tego kroku jest "czysty" plik C++ bez dyrektyw przed procesorem.

  2. Kompilacja: kompilator pobiera dane wyjściowe preprocesora i tworzy z niego plik obiektowy.

  3. Linkowanie: linker pobiera pliki obiektowe wyprodukowane przez kompilatora i tworzy bibliotekę lub plik wykonywalny.

Przetwarzanie wstępne

Preprocesor obsługuje dyrektywy preprocesora , takie jak #include i #define. Jest agnostykiem składni C++, dlatego należy go używać z ostrożnością.

Działa na jednym pliku źródłowym C++ naraz, zastępując dyrektywy #include zawartością odpowiednich plików( co zwykle jest tylko deklaracjami), wykonując zamianę makr (#define) i wybierając różne porcje tekstu w zależności od #if, #ifdef i #ifndef dyrektywy.

Preprocesor pracuje nad strumieniem tokenów wstępnego przetwarzania. Podstawianie makr jest definiowane jako zastępowanie tokenów innymi tokenami(operator ## umożliwia łączenie 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 też kilka specjalnych znaczników, które mówią kompilatorowi, gdzie każda linia pochodzi z tak, że może ich używać do generowania sensownych komunikatów o błędach.

Niektóre błędy mogą być popełniane na tym etapie dzięki sprytnemu wykorzystaniu dyrektyw #if i #error.

Kompilacja

Krok kompilacji jest wykonywany na każdym wyjściu preprocesora. Kompilator przetwarza czysty kod źródłowy C++ (obecnie bez dyrektyw preprocesora) i konwertuje go na kod asemblera. Następnie wywołuje bazowy back-end (asembler w toolchain), który montuje kod do kodu maszynowego produkującego rzeczywisty plik binarny w jakimś formacie (ELF, COFF, a .out,...). Ten plik obiektowy zawiera skompilowany kod (w postaci binarnej) symboli zdefiniowanych na wejściu. Symbole w plikach obiektowych są określane nazwą.

Pliki obiektowe mogą odnosić się do symboli, które nie są zdefiniowane. Ma to miejsce w przypadku, gdy używasz deklaracji i nie podajesz dla niej definicji. Kompilator nie ma nic przeciwko temu i z radością wytworzy plik obiektowy tak długo, jak długo kod źródłowy będzie dobrze uformowany.

Kompilatory zazwyczaj pozwalają na zatrzymanie kompilacji w tym momencie. Jest to bardzo przydatne, ponieważ za jego pomocą można skompilować każdy plik kodu źródłowego osobno. Zaletą tego rozwiązania jest to, że nie musisz przekompilowywać wszystkiego, jeśli zmieniasz tylko jeden plik.

Wytworzone pliki obiektowe mogą być umieszczane w specjalnych archiwach zwanych bibliotekami statycznymi, co ułatwia późniejsze ich ponowne użycie.

To na tym etapie "zwykłe" błędy kompilatora, takie jak błędy składni lub błędy rozdzielczości przeciążenia są zgłaszane.

Linkowanie

Linker jest tym, co tworzy ostateczne wyjście kompilacji z plików obiektowych, które kompilator wyprodukował. Wyjście To może być biblioteką współdzieloną (lub dynamiczną) (i chociaż nazwa jest podobna, nie mają one wiele wspólnego ze statycznymi bibliotekami wspomnianymi wcześniej) lub wykonywalną.

Łączy wszystkie pliki obiektowe, zastępując odniesienia do niezdefiniowanych symboli poprawnymi adresami. Każdy z symbole te mogą być definiowane w innych plikach obiektowych lub w bibliotekach. Jeśli są one zdefiniowane w bibliotekach innych niż biblioteka standardowa, musisz poinformować o nich linkera.

Na tym etapie najczęstszymi błędami są brakujące definicje lub zduplikowane definicje. Pierwsza z nich oznacza, że albo definicje nie istnieją (tzn. nie są zapisywane), albo że pliki obiektowe lub biblioteki, w których znajdują się, nie zostały przekazane linkerowi. To ostatnie jest oczywiste: ten sam symbol został zdefiniowany w dwóch różnych plikach obiektowych lub bibliotekach.

 448
Author: R. Martinho Fernandes,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-01-09 01:13:40

Ten temat jest omawiany na CProgramming.com:
https://www.cprogramming.com/compilingandlinking.html

Oto co napisał tam autor:

Kompilowanie to nie to samo co Tworzenie pliku wykonywalnego! Zamiast tego tworzenie pliku wykonywalnego jest wielostopniowym procesem podzielonym na dwa składniki: kompilacja i łączenie. W rzeczywistości, nawet jeśli program "dobrze kompiluje" może faktycznie nie działać z powodu błędów podczas Faza łączenia. Suma proces przechodzenia z plików kodu źródłowego aby plik wykonywalny mógł być lepiej określany jako build.

Kompilacja

Kompilacja odnosi się do przetwarzania plików kodu źródłowego (.c,. cc lub .cpp) oraz utworzenie pliku 'object'. Ten krok nie tworzy wszystko, co użytkownik może uruchomić. Zamiast tego kompilator jedynie tworzy instrukcje języka maszynowego, które odpowiadają plik kodu źródłowego, który został skompilowany. Na przykład, jeśli skompilujesz (ale nie Linkuj) trzy oddzielne pliki, będziesz miał trzy pliki obiektowe utworzony jako wyjście, każdy z nazwą .o or .obj (rozszerzenie będzie zależeć od kompilatora). Każdy z tych plików zawiera tłumaczenie pliku kodu źródłowego na komputer plik językowy - ale nie możesz ich jeszcze uruchomić! Musisz je obrócić. do plików wykonywalnych, z których może korzystać Twój system operacyjny. Tam wchodzi linker.

Linkowanie

Linkowanie odnosi się do tworzenia pojedynczego plik wykonywalny z wiele plików obiektowych. W tym kroku często zdarza się, że łącznik będzie narzekać na nieokreślone funkcje(zwykle same główne). Podczas kompilacji, jeśli kompilator nie mógł znaleźć definicji dla konkretnej funkcji, wystarczy założyć, że funkcja jest zdefiniowane w innym pliku. Jeśli tak nie jest, to nie ma mowy o kompilator by wiedział - nie patrzy na zawartość więcej niż jeden plik na raz. Z drugiej strony linker może wyglądać na wielu plików i spróbuj znaleźć odniesienia do funkcji, które nie wspominano.

Możesz zapytać, dlaczego istnieją oddzielne kroki kompilacji i łączenia. Po pierwsze, prawdopodobnie łatwiej jest zaimplementować rzeczy w ten sposób. Kompilator robi swoje, a linker robi swoje-zachowując funkcje oddzielne, złożoność programu jest zmniejszona. Inny (bardziej oczywistą) zaletą jest to, że pozwala to na tworzenie dużych programy bez konieczności ponownego krok kompilacji za każdym razem, gdy plik zmienił się. Zamiast tego, używając tzw. "kompilacji warunkowej", jest niezbędne do kompilacji tylko tych plików źródłowych, które uległy zmianie; Dla reszta, pliki obiektowe są wystarczającymi danymi wejściowymi dla linkera. Wreszcie, ułatwia to implementację bibliotek wstępnie skompilowanych kod: wystarczy utworzyć pliki obiektowe i połączyć je tak jak każde inne plik obiektowy. (Fakt, że każdy plik jest kompilowany oddzielnie od informacje zawarte w innych plikach, nawiasem mówiąc, nazywa się osobny model kompilacji.)

Aby uzyskać pełne korzyści z kompilacji stanu, prawdopodobnie łatwiej dostać program, który Ci pomoże, niż próbować i pamiętać, który pliki, które zostały zmienione od czasu ostatniej kompilacji. (Można oczywiście, wystarczy przekompilować każdy plik, który ma znacznik czasu większy niż znacznik czasu odpowiedniego pliku obiektowego.) Jeśli pracujesz z zintegrowane środowisko programistyczne (IDE) może już zająć się to dla Ciebie. Jeśli używasz narzędzi wiersza poleceń, jest sprytny narzędzie o nazwie make, które pochodzi z większości dystrybucji * nix. Along z kompilacją warunkową, posiada kilka innych fajnych funkcji dla programowania, np. umożliwienie różnych kompilacji programu -- na przykład, jeśli masz wersję produkującą szczegółowe dane wyjściowe do debugowania.

Poznanie różnicy między fazą kompilacji a łączem faza może ułatwić polowanie na błędy. Błędy kompilatora są zazwyczaj składnia w naturze - brak średnika, dodatkowy nawias. Błędy linkowania zwykle mają związek z brakiem lub wieloma definicje. Jeśli pojawi się błąd, że funkcja lub zmienna jest zdefiniowany wielokrotnie z linkera, to dobry znak, że błąd polega na tym, że dwa pliki kodu źródłowego mają tę samą funkcję lub zmienna.

 25
Author: neuronet,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-02-25 20:05:15

Na standardowym froncie:

  • Jednostka tłumaczenia jest kombinacją plików źródłowych, nagłówków i plików źródłowych pomniejszonych o dowolne linie źródłowe pominięte przez dyrektywę preprocesora z włączeniem warunkowym.

  • Standard definiuje 9 faz tłumaczenia. Pierwsze cztery odpowiadają przetwarzaniu wstępnemu, następne trzy to kompilacja, następna to instancja szablonów (produkująca jednostki instancyjne ), a ostatnia to linkowanie.

W praktyce ósma Faza (instancjacja szablonów) jest często wykonywana podczas procesu kompilacji, ale niektóre Kompilatory opóźniają ją do fazy łączenia, a niektóre rozprzestrzeniają ją w obu.

 22
Author: AProgrammer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-06-07 11:23:16

Skinny polega na tym, że procesor ładuje dane z adresów pamięci, przechowuje dane do adresów pamięci i wykonuje instrukcje kolejno z adresów pamięci, z pewnymi skokami warunkowymi w sekwencji przetwarzanych instrukcji. Każda z tych trzech kategorii instrukcji polega na obliczeniu adresu komórki pamięci, który ma być użyty w instrukcji maszynowej. Ponieważ instrukcje maszynowe mają zmienną długość w zależności od konkretnej instrukcji, a ponieważ ciągniemy zmienna długość ich razem podczas budowania naszego kodu maszynowego, istnieje dwuetapowy proces związany z obliczaniem i budowaniem dowolnych adresów.

Najpierw ustalamy przydział pamięci najlepiej jak potrafimy, zanim dowiemy się, co dokładnie dzieje się w każdej komórce. Rozgryzamy bajty, słowa, czy 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 notujemy w dowolnym miejscu, w którym musimy wróć i popraw adres. W tym miejscu umieszczamy atrapę, aby po prostu umieścić miejsce, abyśmy mogli kontynuować obliczanie wielkości pamięci. Na przykład nasz pierwszy kod maszynowy może przyjmować jedną komórkę. Następny kod maszynowy może przyjmować 3 komórki, obejmujące jedną komórkę kodu maszynowego i dwie komórki adresowe. Teraz nasz wskaźnik adresu to 4. Wiemy, co idzie do komórki maszyny, czyli kodu op, ale musimy czekać na obliczenie tego, co idzie do komórek adresowych, aż dowiemy się, gdzie te dane będą zlokalizowane, tzn. co będzie adresem maszyny tych danych.

Gdyby istniał tylko jeden plik źródłowy, kompilator mógłby teoretycznie wytworzyć w pełni wykonywalny kod maszynowy bez linkera. W dwujezdniowym procesie 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 odwołują się dowolne instrukcje skoku bezwzględnego. Tak prostsze Kompilatory, jak ten w pracy Forth, bez linker.

Linker jest czymś, co pozwala na oddzielną kompilację bloków kodu. Może to przyspieszyć ogólny proces budowania kodu i pozwala na pewną elastyczność z tym, jak bloki są później używane, innymi słowy mogą być przenoszone w pamięci, na przykład dodając 1000 do każdego adresu, aby zwiększyć blok o 1000 komórek adresowych.

Więc co kompilator wyjścia jest szorstki kod maszynowy, który nie jest jeszcze w pełni zbudowany, ale jest rozplanowany tak, że znamy rozmiar wszystkiego, w innymi słowy, możemy zacząć obliczać, gdzie będą znajdować się wszystkie adresy bezwzględne. kompilator wypisuje również listę symboli, które są parami nazwa / adres. Symbole odnoszą się do przesunięcia pamięci w kodzie maszynowym modułu z nazwą. Przesunięcie jest absolutną odległością od miejsca pamięci symbolu w module.

Tam dojdziemy do linkera. Linker najpierw zrzuca wszystkie te bloki kodu maszynowego razem od końca do końca i notuje w dół, gdzie każdy zaczyna się. Następnie oblicza adresy do ustalenia, dodając razem względne przesunięcie w module i absolutną pozycję modułu w większym układzie.

Oczywiście uprościłem to, abyś mógł spróbować to zrozumieć, i celowo nie użyłem żargonu plików obiektowych, tabel symboli itp. co dla mnie jest częścią zamieszania.

 12
Author: my username was hijacked here,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-01-10 11:13:57

Spójrz na URL: http://faculty.cs.niu.edu / ~mcmahon/CS241/Notes/compile.html
Kompletny proces compling C++ jest wyraźnie przedstawiony w tym adresie URL.

 8
Author: Charles Wang,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-03-20 09:24:41

GCC kompiluje program C / C++ do pliku wykonywalnego w 4 krokach.

Na przykład, a "gcc -o hello.exe hello.c" przeprowadza się w następujący sposób:

1. Obróbka wstępna

Preprocesor poprzez GNU C preprocesor (cpp.exe), który obejmuje nagłówki (#include) i rozszerza makra (#define).

cpp Witam.c > Witam.i

Wynikowy plik pośredni "hello.i " zawiera rozszerzony kod źródłowy.

2. Kompilacja

Kompilator kompiluje wstępnie przetworzony kod źródłowy do kodu Złożenia dla określonego procesora.

gcc-s Witam.i

Opcja-S określa, aby produkować kod złożenia, zamiast kodu obiektowego. Wynikowym plikiem montażowym jest " hello.s".

3. Assembly

Asembler (as.exe) konwertuje kod złożenia na kod maszynowy w pliku obiektowym " hello.o".

as-o Witaj.o Witam.s

4. Linker

Wreszcie, linker (ld.exe) łączy kod obiektowy z kodem biblioteki, tworząc plik wykonywalny " hello.exe".

ld-o Witam.exe Witam.o...biblioteki...

 2
Author: kaps,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-08-13 09:32:20