Як працює процес компіляції / зв’язування?


416

Як працює процес компіляції та зв’язування?

(Примітка. Це означає, що це запис до C ++ FAQ Stack Overflow . Якщо ви хочете критикувати ідею надання поширених запитань у цій формі, то тут слід зробити публікацію про мета, яка почала все це . Відповіді на це питання відстежується в кімнаті чатів C ++ , де ідея FAQ задається в першу чергу, тому велику ймовірність отримати відповідь ті, хто придумав цю ідею.)

Відповіді:


554

Складання програми C ++ включає три етапи:

  1. Попередня обробка: препроцесор бере файл вихідного коду C ++ і має справу з директивами #includes, #defines та іншими препроцесорами. Результатом цього кроку є "чистий" C ++ файл без директив попереднього процесора.

  2. Компіляція: компілятор бере висновок попереднього процесора і виробляє з нього об’єктний файл.

  3. Зв'язування: лінкер бере файли об'єктів, створені компілятором, і створює або бібліотеку, або виконуваний файл.

Попередня обробка

Препроцесор обробляє директиви препроцесора , як #includeі #define. Це агностик синтаксису C ++, тому його потрібно використовувати обережно.

Він працює на одному C ++ вихідного файлу , в той час, замінивши #includeдирективи з вмістом відповідних файлів (які, як правило , тільки декларація), роблячи заміну макросів ( #define), і вибираючи різні частини тексту , в залежності від #if, #ifdefі #ifndefдирективи.

Препроцесор працює над потоком маркерів попередньої обробки. Заміна макросів визначається як заміна лексем на інші лексеми (оператор ##дозволяє об'єднати два лексеми, коли це має сенс).

Після всього цього препроцесор виробляє єдиний вихід, який представляє собою потік лексем, що є результатом перетворень, описаних вище. Він також додає деякі спеціальні маркери, які повідомляють компілятору, звідки походить кожен рядок, щоб він міг використовувати їх для створення розважливих повідомлень про помилки.

На цьому етапі можуть бути допущені деякі помилки при розумному використанні директив #ifта #errorдиректив.

Компіляція

Етап компіляції виконується на кожному виході препроцесора. Компілятор аналізує чистий вихідний код C ++ (тепер без будь-яких директив препроцесора) і перетворює його в код складання. Потім викликає базовий бек-енд (асемблер в ланцюжку інструментів), який збирає цей код у машинний код, створюючи фактичний бінарний файл у якомусь форматі (ELF, COFF, a.out, ...). Цей об'єктний файл містить скомпільований код (у двійковій формі) символів, визначених на вході. Символи в об'єктних файлах посилаються на ім'я.

Файли об'єктів можуть посилатися на символи, які не визначені. Це той випадок, коли ви використовуєте декларацію, а не надаєте визначення для неї. Компілятор цього не заперечує, і він із задоволенням видасть об’єктний файл, доки вихідний код буде добре сформований.

Зазвичай компілятори дозволяють зупинити компіляцію на цьому етапі. Це дуже корисно, оскільки за допомогою нього ви можете зібрати кожен файл вихідного коду окремо. Перевага, яке надає, полягає в тому, що вам не потрібно перекомпілювати все, якщо ви зміните лише один файл.

Отримані файли об'єктів можуть бути поміщені в спеціальні архіви, які називаються статичними бібліотеками, для подальшого подальшого використання.

Саме на цьому етапі повідомляються про "звичайні" помилки компілятора, такі як синтаксичні помилки або помилки роздільної помилки.

Зв’язування

Лінкер - це те, що виробляє кінцевий вихід компіляції з файлів об'єктів, які виробляв компілятор. Цей вихід може бути або спільною (або динамічною) бібліотекою (і хоча назва схожа, вони не мають багато спільного зі статичними бібліотеками, згаданими раніше), або виконувані файли.

Він пов'язує всі файли об'єктів, замінюючи посилання на невизначені символи правильними адресами. Кожен з цих символів може бути визначений в інших об’єктних файлах або в бібліотеках. Якщо вони визначені в інших бібліотеках, ніж у стандартній бібліотеці, вам потрібно повідомити про них лінкер.

На цьому етапі найпоширенішими помилками є відсутні дефініції або дублюючі визначення. Перший означає, що або визначення не існують (тобто вони не записуються), або що об'єктні файли або бібліотеки, де вони мешкають, не були надані лінкеру. Останнє очевидно: той самий символ був визначений у двох різних файлах об'єктів або бібліотеках.


39
Етап компіляції також викликає асемблер перед перетворенням у файл об'єкта.
манав мн

3
Де застосовуються оптимізації? На перший погляд здається, що це було б зроблено на етапі компіляції, але з іншого боку, я можу уявити, що належна оптимізація може бути здійснена лише після з'єднання.
Барт ван Хекелом

6
@BartvanHeukelom традиційно це робилося під час компіляції, але сучасні компілятори підтримують так звану "оптимізацію часу зв’язку", яка має перевагу в можливості оптимізувати трансляцію підрозділів.
Р. Мартіньо Фернандес

3
Чи мають C однакові кроки?
Кевін Чжу

6
Якщо лінкер перетворює символи, що відносяться до класів / методів у бібліотеках, в адреси, чи це означає, що бінарні бібліотеки зберігаються в адресах пам'яті, які ОС підтримує постійними? Я просто розгублений у тому, як лінкер дізнається точну адресу, скажімо, бінарного файлу stdio для всіх цільових систем. Шлях до файлу завжди буде однаковим, але точна адреса може змінитися, правда?
Ден Картер

42

Цю тему обговорюють на сайті CProgramming.com:
https://www.cprogramming.com/compilingandlinking.html

Ось що там написав автор:

Компіляція не зовсім така, як створення виконавчого файлу! Натомість створення виконуваного файлу - це багатоетапний процес, розділений на два компоненти: компіляція та зв’язування. Насправді, навіть якщо програма "складе штрафи", вона може насправді не працювати через помилки під час фази зв'язування. Загальний процес переходу від файлів вихідного коду до виконуваного файлу краще називати збіркою.

Компіляція

Під компіляцією розуміється обробка файлів вихідного коду (.c, .cc або .cpp) та створення файлу 'object'. Цей крок не створює нічого, що користувач може реально запустити. Натомість компілятор просто створює інструкції на машинній мові, які відповідають файлу вихідного коду, який був складений. Наприклад, якщо ви компілюєте (але не посилаєте) три окремі файли, у вас будуть створені три об’єктні файли як вихідні дані, кожен з іменем .o або .obj (розширення буде залежати від компілятора). Кожен із цих файлів містить переклад файлу вашого вихідного коду на файл машинної мови - але ви ще не можете їх запустити! Вам потрібно перетворити їх у виконувані файли, які може використовувати ваша операційна система. Ось де заходить лінкер.

Зв’язування

Посилання стосується створення одного виконавчого файлу з декількох файлів об'єкта. На цьому кроці звичайно, що лінкер скаржиться на невизначені функції (як правило, основний). Під час компіляції, якщо компілятор не зміг знайти визначення для певної функції, було б просто припустити, що функція була визначена в іншому файлі. Якщо це не так, компілятор не знає - він не переглядає вміст більше одного файлу одночасно. З іншого боку, лінкер може переглянути декілька файлів і спробувати знайти посилання на функції, які не згадувалися.

Ви можете запитати, чому існують окремі етапи компіляції та зв’язування. По-перше, можливо, простіше реалізувати речі таким чином. Компілятор робить свою справу, а лінкер робить свою справу - зберігаючи функції окремо, складність програми знижується. Ще одна (більш очевидна) перевага полягає в тому, що це дозволяє створювати великі програми без необхідності повторювати крок компіляції щоразу, коли файл змінюється. Натомість, використовуючи так звану "умовну компіляцію", потрібно компілювати лише ті вихідні файли, які змінилися; для решти, об'єктні файли є достатнім входом для лінкера. Нарешті, це спрощує реалізацію бібліотек заздалегідь складеного коду: просто створіть об’єктні файли та зв’яжіть їх так само, як і будь-який інший файл об'єкта.

Щоб отримати всі переваги компіляції умов, можливо, простіше отримати програму, яка допоможе вам, ніж спробувати і запам'ятати, які файли ви змінили з моменту останньої компіляції. (Ви, звичайно, можете просто перекомпілювати кожен файл, у якого часова мітка перевищує часову позначку відповідного об'єктного файлу.) Якщо ви працюєте з інтегрованим середовищем розробки (IDE), він може вже зайнятися цим. Якщо ви використовуєте інструменти командного рядка, є чудова утиліта під назвою make, яка постачається з більшістю * nix дистрибутивів. Поряд з умовною компіляцією, у неї є кілька інших приємних функцій програмування, наприклад, дозволяючи різні компіляції вашої програми - наприклад, якщо у вас є версія, що виробляє багатослівний вихід для налагодження.

Знання різниці між фазою компіляції та фазою зв’язку може полегшити полювання на помилок. Помилки компілятора зазвичай мають синтаксичний характер - відсутня крапка з комою, додаткові дужки. Помилки посилання зазвичай пов'язані з пропущеними або декількома визначеннями. Якщо ви отримуєте помилку про те, що функція або змінна визначається кілька разів від лінкера, це хороший показник того, що помилка полягає в тому, що два файли вашого вихідного коду мають однакову функцію або змінну.


1
Я не розумію, що якщо препроцесор керує такими речами, як #includes, щоб створити один супер-файл, то похмуро після цього нічого не посилати?
binarysmacker

@binarysmacer Подивіться, чи те, що я написав нижче, має для вас сенс. Я намагався описати проблему зсередини.
Еліптичний вигляд

3
@binarysmacker Це занадто пізно коментувати це, але інші можуть вважати це корисним. youtu.be/D0TazQIkc8Q В основному ви включаєте файли заголовків, і ці файли заголовків, як правило, містять лише декларації змінних / функцій, а не визначення, дефініції можуть бути наявними в окремому вихідному файлі. Linker help.Ви пов'язуєте вихідний файл, який використовує змінну / функцію, з вихідним файлом, який їх визначає.
Karan Joisher

24

На стандартній передній панелі:

  • одиниця перекладу є комбінацією вихідних файлів, включених заголовків і вихідних файлів менш яких - або вихідних ліній пропускаються умовної директиви включення препроцесора.

  • стандарт визначає 9 фаз у перекладі. Перші чотири відповідають попередній обробці, наступні три - це компіляція, наступна - інстанція шаблонів (виробляючи одиниці інстанції ), а остання - посилання.

На практиці восьма фаза (інстанціювання шаблонів) часто виконується в процесі компіляції, але деякі компілятори затримують її до фази зв'язку, а деякі поширюють її на дві.


14
Чи можете ви перерахувати всі 9 фаз? Це було б приємне доповнення до відповіді. :)
jalf

@jalf: Зв'язаний: stackoverflow.com/questions/1476892 / ... .
sbi

@jalf, просто додайте екземпляр шаблону безпосередньо перед останньою фазою у відповідь, вказану @sbi. IIRC є тонкі відмінності в точному формулюванні в поводженні з широкими символами, але я не думаю, що вони виникають на мітках діаграм.
AProgrammer

2
@sbi Так, але це має бути питанням FAQ, чи не так? Тож чи не повинна ця інформація бути тут доступною ? ;)
джельф

3
@AProgrammmer: просто перерахування їх по імені буде корисним. Тоді люди знають, що шукати, якщо хочуть детальніше. У будь-якому разі, +1 відповів на вашу відповідь у будь-якому випадку :)
jalf

14

Худім є те, що процесор завантажує дані з адрес пам'яті, зберігає дані на адреси пам'яті та виконує інструкції послідовно із адрес пам'яті, з деякими умовними стрибками в послідовності оброблюваних інструкцій. Кожна з цих трьох категорій інструкцій включає обчислення адреси в комірці пам'яті, яка буде використана в машинній інструкції. Оскільки машинні інструкції мають різну довжину залежно від конкретної інструкції, і тому, що ми з'єднуємо змінну довжину їх разом, коли ми будуємо наш машинний код, є процес, що складається з двох етапів, що обчислюють та створюють будь-які адреси.

Спочатку ми розкладаємо розподіл пам’яті якнайкраще, перш ніж зможемо дізнатися, що саме відбувається в кожній комірці. Ми розбираємо байти чи слова, або що інше, що утворюють інструкції, літерали та будь-які дані. Ми просто починаємо виділяти пам'ять і будувати значення, які створюватимуть програму, коли ми йдемо, і записуємо в будь-якому місці, де нам потрібно повернутися і виправити адресу. У цьому місці ми поміщаємо манекен, щоб просто зафіксувати місце, щоб ми могли продовжувати обчислювати розмір пам'яті. Наприклад, наш перший машинний код може містити одну клітинку. Наступний код машини може містити 3 комірки, включаючи одну коду машинного коду та дві адресні комірки. Тепер наш покажчик адреси становить 4. Ми знаємо, що відбувається в машинній комірці, що є кодом op, але нам доведеться чекати, щоб обчислити, що йде в адресні комірки, поки ми не дізнаємося, де будуть розташовані ці дані, тобто

Якби був лише один вихідний файл, компілятор теоретично міг би створити повністю виконуваний машинний код без лінкера. У процесі двох пропусків він міг обчислити всі фактичні адреси до всіх комірок даних, на які посилається будь-яке завантаження машини чи інструкції зберігання. І він міг обчислити всі абсолютні адреси, на які посилаються будь-які інструкції щодо абсолютного стрибка. Ось як простіші компілятори, як той, що працює у Forth, не мають лінкера.

Посилання - це те, що дозволяє блоки коду збирати окремо. Це може пришвидшити загальний процес побудови коду та дозволяє деяку гнучкість у використанні блоків пізніше, інакше кажучи, вони можуть бути переміщені в пам'ять, наприклад, додаючи 1000 до кожної адреси, щоб скасувати блок на 1000 адресних комірок.

Отже, що компілятор виводить - це грубий машинний код, який ще не повністю побудований, але він викладений, тому ми знаємо розмір всього, іншими словами, тому ми можемо почати обчислювати, де будуть розташовані всі абсолютні адреси. компілятор також виводить список символів, які є іменами / адресами. Символи відносять зміщення пам'яті в машинному коді в модулі з назвою. Зсувом є абсолютна відстань до місця пам'яті символу в модулі.

Ось тут ми потрапляємо до лінкера. Спочатку лінкер пров'язує всі ці блоки машинного коду разом від кінця до кінця і відзначає, з чого починається кожен. Потім він обчислює адреси, які потрібно виправити, додаючи разом відносне зміщення всередині модуля і абсолютне положення модуля у більшій компоновці.

Очевидно, що я спростив це, щоб ви могли спробувати його зрозуміти, і я навмисно не використовував жаргон об’єктних файлів, таблиць символів тощо, що для мене є частиною плутанини.


13

GCC компілює програму C / C ++ у виконуваний у 4 етапи.

Наприклад, gcc -o hello hello.cздійснюється наступним чином:

1. Попередня обробка

Попередня обробка за допомогою GNU C Preprocessor ( cpp.exe), що включає заголовки ( #include) і розширює макроси ( #define).

cpp hello.c > hello.i

Отриманий проміжний файл "hello.i" містить розгорнутий вихідний код.

2. Складання

Компілятор збирає попередньо оброблений вихідний код у код складання для конкретного процесора.

gcc -S hello.i

Параметр -S вказує на створення коду складання замість коду об'єкта. Отриманий файл складання "hello.s".

3. Збірка

Асемблер ( as.exe) перетворює код складання в машинний код у файлі об'єкта "hello.o".

as -o hello.o hello.s

4. Лінкер

Нарешті, linker ( ld.exe) пов'язує об'єктний код з кодом бібліотеки, щоб створити виконуваний файл "привіт".

    ld -o привіт hello.o ... бібліотеки ...

9

Подивіться на URL: http://facturing.cs.niu.edu/~mcmahon/CS241/Notes/compile.html
Повний процес компіляції C ++ чітко введений у цю URL-адресу.


2
Дякуємо, що поділилися цим, це так просто і просто зрозуміти.
Марк

Добре, ресурс, ви можете тут розмістити деякі основні пояснення процесу, відповідь позначена алгоритмом, оскільки низька якість b / c коротка і просто URL-адреса.
JasonB

Гарний короткий підручник, який я знайшов: calleerlandsson.com/the-four-stages-of-compiling-ac-program
Гай Аврахам
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.