Для чого у графічних бібліотеках використовується компонувальник. Посібник новачка з експлуатації компонувальника. Найменування складових елементів: що всередині Сі файлу

У типовій системі одночасно виконується безліч програм. Робота кожної програми залежить від багатьох функцій, деякі з яких входять до складу "стандартної" Си-бібліотеки (наприклад,printf() , malloc() , write()і т.д.).

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

Статичне компонування Термін "статично скомпонований" (statically linked) означає, що програма та деяка бібліотека були об'єднані за допомогою компонувальника під час процесу компонування. Таким чином, зв'язок між програмою та бібліотекою є фіксованим та встановлюється під час процесу компонування, тобто до того, як програма буде працювати. Крім іншого, це також означає, що можна змінити цей зв'язокінакше, як за допомогою перекомпонування програми з новою версією бібліотеки.

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

Статично скомпоновані програми компонуються з архівами об'єктів (бібліотеками ), які зазвичай мають розширення a. Прикладом такого набору об'єктів є стандартна бібліотека libc.a.

Динамічна компоновка Термін "динамічно скомпонований" (dynamically linked) означає, що програма та деяка бібліотекане були об'єднані за допомогою компонувальника під час процесу компонування. Натомість, компонувальник поміщає інформацію у виконуваний файл, який, у свою чергу, повідомляє завантажувачу, в якому об'єктному модулі, що розділяється, розташований код і який динамічний компонувальник (runtime linker) повинен використовуватися для пошуку і компонування посилань. Це означає, що зв'язок між програмою і об'єктом, що розділяється, встановлюється під час виконання програми, а саме, на самому початку виконання проводиться пошук і компонування необхідних об'єктів, що розділяються.

Такий тип програм називаєтьсячастково пов'язаним виконуваним файлом (partially bound executable) , оскільки у них дозволені в повному обсязі посилання, т. е. компонувальник у процесі компонування не пов'язав всі згадані ідентифікатори (referenced symbols) у програмі з відповідним кодом з бібліотеки. Замість цього, компонувальник вказує,якому саме об'єкті, що розділяються, знаходяться функції, викликані програмою. В результаті сам процес компонування здійснюється згодом, вже в момент виконання програми.

Динамічно скомпоновані програми компонуються з об'єктами, що розділяються, з розширенням so. Прикладом такого об'єкта є стандартна Си-бібліотека libc.so.

Для того, щоб повідомити комплект інструментів про те, який тип компоновки застосовується - статичний або динамічний - використовується відповідна опція командного рядкаутиліти QCC. Ця опція потім визначає розширення (a або so).

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

Розглянемо приклад роботи драйвера диска. Драйвер запускається, тестує обладнання та виявляє жорсткий диск. Потім драйвер динамічно завантажує модуль io-blk, призначений обробки дискових блоків, т.к. було виявлено блокорієнтований пристрій. Після того як драйвер отримує доступ до диска на блоковому рівні, він виявляє на диску два розділи: розділ DOS та розділ QNX4. Щоб не збільшувати розмір драйвера жорсткого диска, до нього взагалі не включаються драйвери файлових систем. Під час роботи системи драйвер може виявити ці два розділи (DOS та QNX4) і лише після цього завантажити відповідні модулі файлових систем fs-dos.so та fs-qnx4.so.

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

Як використовуються об'єкти, що розділяються Щоб зрозуміти, як програма використовує об'єкти, що розділяються, необхідно спочатку розглянути формат виконуваного модуля, а потім послідовність тих стадій, через які програма проходить при запуску. Формат ELF В ОС QNX Neutrino використовується так званий двійковий формат модулів, що виконуються і компонуються (Executable and Linkable Format, ELF), який в даний час прийнятий в системах SVR4 Unix. Формат ELF не тільки спрощує створення бібліотек, що розділяються, але також розширює можливості динамічного завантаження модулів під час роботи програми.

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

У процесі компонування програма або бібліотека будується за допомогою злиття секцій, що мають однакові атрибути, та перетворення їх на сегменти. Як правило, всі секції, що містять дані, призначені для виконання або "тільки для читання", компонуються в один сегментtext, а дані таBSS компонуються в сегментdata. Ці сегменти називаютьсязавантажувальними сегментами (load segments) тому що вони повинні бути завантажені в пам'ять при створенні процесу. Інші секції, як, наприклад, інформація про ідентифікатори та налагоджувальна інформація, об'єднуються у т.з.сегменти, що не завантажуються (nonload segments) .

цієї статті.

Організація таблиці символьних імен в асемблері.

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

Послідовне асемблювання.

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

Сортування за іменами.

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

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

Кеш-кодування.

При цьому способі на основі вихідної таблиці будується кеш-функція, що відображає імена в цілі числа в проміжку від K-1 (рис. 5.2.1, а). Кеш-функцією може бути, наприклад, функція перемноження всіх розрядів імені, представленого кодом ASCII або будь-яка інша функція, яка дає рівномірний розподіл значень. Після цього створюється кеш-таблиця, яка містить рядки (слоти). У кожному рядку розташовуються (наприклад, в алфавітному порядку) імена, що мають однакові значення кеш-функції (рис. 5.2.1 б), або номер слота. Якщо в кеш-таблиці міститься п символьних імен, то середня кількість імен у кожному слоті становить n/k. При n = k знаходження потрібного символьного імені загалом знадобиться лише одне пошук. Шляхом зміни можна варіювати розмір таблиці (число слотів) і швидкість пошуку. Зв'язування та завантаження. Програму можна як сукупність процедур (підпрограм). Асемблер по черзі транслюютьодну процедуру за іншою, створюючи об'єктні модуліта розміщуючи їх у пам'яті. Для отримання виконуваного двійкового коду повинні бути знайдені та пов'язанівсі відтрансльовані процедури.

Функції зв'язування та завантаження виконують спеціальні програми, які називаються компонувальниками, що зв'язують завантажувачами, редакторами зв'язківабо лінкерами.


Таким чином, для повної готовності до виконання вихідної програми потрібно два кроки (рис. 5.2.2):

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

● зв'язування об'єктних модулів, що виконується компонувальником для отримання двійкового коду. Окрема трансляція процедур викликана можливими помилкамиабо необхідністю зміни процедур. У цих випадках потрібно знову зв'язати всі об'єктні модулі. Оскільки зв'язування відбувається набагато швидше, ніж трансляція, виконання цих двох кроків (трансляції і зв'язування) заощадить час при доопрацюванні програми. Це особливо важливо для програм, які містять сотні чи тисячі модулів. В операційних системах MS-DOS, Windows і NT об'єктні модулі мають розширення ".obj", а двійкові програми, що виконуються, - розширення ".ехе". У системі UNIX об'єктні модулі мають розширення «.о», а двійкові програми, що виконуються, не мають розширення.

Функції компонувальника.

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

Мета компонування -створити точне відображення віртуального адресного простору програми, що виконується всередині компонувальника і розмістити всі об'єктні модулі за відповідними адресами.


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

Тому, як показано на рис. 5.2.3, б програми починаються не з нульової адреси, а з адреси 100. Оскільки кожен об'єктний модуль на рис. 5.2.3, а займає окремий адресний простір, виникає проблема перерозподілу пам'яті. Усі команди звернення до пам'яті не будуть виконані через некоректну адресацію. Наприклад, команда виклику об'єктного модуля B (рис. 5.2.3 б), вказана в осередку з адресою 300 об'єктного модуля А (рис. 5.2.3, а), не виконається з двох причин:

● команда CALL B знаходиться в осередку з іншою адресою (300, а не 200); ● оскільки кожна процедура транслюється окремо, асемблер не може визначити, яку адресу вставляти в команду CALL В. Адреса об'єктного модуля не відома до зв'язування. Така проблема називається проблемою зовнішнього заслання.Обидві причини усуваються за допомогою компонувальника, який зливає окремі адресні простори об'єктних модулів в єдиний лінійний адресний простір, для чого:

● будує таблицю об'єктних модулів та їх довжин;

● на основі цієї таблиці приписує початкові адреси кожному об'єктному модулю;

до пам'яті,і додає до кожної з них константу переміщення, яка дорівнює початковій адресі цього модуля (в даному випадку 100);

● знаходить усі команди, які звертаються до процедур,та вставляє в них адресу цих процедур.
Нижче наведено таблицю об'єктних модулів (табл. 5.2.6), побудовану першому кроці. У ній дається ім'я, довжина та початкова адреса кожного модуля. Адресний простір після виконання компонувальником всіх кроків показано у табл. 5.2.6 та на рис. 5.2.3, ст. структура об'єктного модуля. Об'єктні модулі складаються з таких частин:

ім'я модуля,деяка додаткова інформація(наприклад, довжини різних частин модуля, дата асемблювання);

список визначених у модулі символів(символьні імена) разом з їх значеннями. До цих символів можуть звертатися інші модулі. Програміст мовою асемблера з допомогою директиви PUBLIC вказує, які символьні імена вважаються точками входу;

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

машинні команди та константи;

словник переміщень.До команд, які містять адреси пам'яті, має додаватися константа переміщення (див. мал. 5.2.3). Компонувальник сам не може визначити, які слова містять машинні команди, а які – константи. Тому в цій таблиці міститься інформація про те, які адреси необхідно перемістити. Це може бути бітова таблиця, де на кожен біт доводиться потенційно переміщувану адресу, або явний список адрес, які потрібно перемістити;

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

● спочатку зчитуються всі об'єктні модулі та будується таблиця імен та довжин модулів, а також таблиця символів, що складається з усіх точок входу та зовнішніх посилань;

● потім модулі ще раз зчитуються, переміщуються у пам'яті та зв'язуються. Про переміщення програм. Проблема переміщення пов'язаних і які у пам'яті програм обумовлена ​​тим, що їх переміщення які у таблицях адреси стають помилковими. Для ухвалення рішення про переміщення програм необхідно знати момент часу фінального зв'язування символічних іменз абсолютними адресами фізичної пам'яті.

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

При зв'язуванні можна виділити два етапи:

першийетап, на якому символічні іменазв'язуються з віртуальними адресами.Коли компонувальник пов'язує окремі адресні простори об'єктних модулів в єдиний лінійний адресний простір, він створює віртуальний адресний простір;

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

● розбиття на сторінки. Адресний простір, зображений на рис. 5.2.3, в містить віртуальні адреси, які вже визначені і відповідають символічним іменам А, В, С і D. Їх фізичні адреси залежатимуть від змісту таблиці сторінок. Тому для переміщення програми в основній пам'яті достатньо змінити лише її таблицю сторінок, але не саму програму;

● використання регістру переміщення.Цей регістр вказує на фізичну адресу початкупоточної програми, що завантажується операційною системою перед переміщенням програми. За допомогою апаратних засобів вміст регістру переміщення додається до всіх адрес пам'яті, перш ніж вони завантажуються в пам'ять. Процес переміщення є «прозорим» для кожної програми користувача. Особливість механізму: на відміну від розбиття на сторінки, повинна переміщатися вся програма повністю. Якщо є окремі регістри (або сегменти пам'яті як, наприклад, процесорах Intel) для переміщення коду і переміщення даних, то цьому випадку програму потрібно переміщати як два компоненти;

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

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

Динамічне зв'язування у системіMULTICS. За кожною програмоюзакріплений сегмент зв'язування,містить блок інформації кожної процедури (рис. 5.2.4).

Інформація включає:

● слово "Непряма адреса", зарезервоване для віртуальної адреси процедури;

● ім'я процедури (EARTH, FIRE та ін.), яке зберігається у вигляді ланцюжка символів. При динамічному зв'язуванні виклики процедур у вхідній мові транслюються до команд, які за допомогою непрямої адресації звертаються до слова «Непряма адреса» відповідного блоку (рис. 5.2.4). Компілятор заповнює це слово або недійсною адресою,або спеціальним набором біт,який викликає системне переривання (типу пастки).Після цього:

● компонувальник знаходить ім'я процедури (наприклад, EARTH) і приступає до пошуку власної директорії для скомпільованої процедури з таким ім'ям;

● знайденій процедурі приписується віртуальна адреса «Адреса EARTH» (зазвичай у її власному сегменті), яка записується поверх недійсної адреси, як показано на рис. 5.2.4;

● команда, яка викликала помилку, виконується заново. Це дозволяє програмі продовжувати роботу з місця, де вона перебувала до системного переривання. Всі наступні звернення до процедури EARTH будуть виконуватися без помилок, оскільки в сегменті зв'язування замість слова "Непряма адреса" тепер вказана дійсна віртуальна адреса "Адреса EARTH". Таким чином, компонувальник задіяний лише тоді, коли деяка процедура викликається вперше. Після цього викликати компонувальника не потрібно.

Динамічний зв'язування в системі Windows.

Для зв'язування використовуються бібліотеки, що динамічно підключаються (Dynamic Link Library - DLL), які містять процедури і (або) дані. Бібліотеки оформляються як файли з розширеннями «.dll», «.drv» (для бібліотек драйверів - driver libraries) і «.fon» (для бібліотек шрифтів - font libraries). Вони дозволяють свої процедури та дані розділяти між кількома програмами (процесами). Тому найпоширенішою формою DLL є бібліотека, що складається з набору процедур, що завантажуються в пам'ять, до яких мають доступ кілька програм одночасно. Як приклад на рис. 5.2.5 показано чотири процеси, які поділяють файл DLL, що містить процедури А, В, С та D. Програми 1 і 2 використовує процедуру А; програма 3 – процедуру D, програма 4 – процедуру В.
Файл DLL будується компонувальником із набору вхідних файлів. Принцип побудови подібний до побудови виконуваного двійкового коду. Відмінність у тому, що з побудові файла DLL компоновщику передається спеціальний прапор повідомлення про створення DLL. Файли DLL зазвичай конструюються з набору бібліотечних процедур, які можуть знадобитися кільком процесорам. Типовими прикладами файлів DLL є процедури сполучення з бібліотекою системних викликів Windowsта великими графічними бібліотеками. Використання файлів DDL дозволяє:

● заощадити простір у пам'яті та на диску. Наприклад, якщо якась бібліотека була пов'язана з кожною програмою, що її використовує, то ця бібліотека буде з'являтися в багатьох виконуваних двійкових програмах у пам'яті і на диску. Якщо ж використовувати файли DLL, то кожна бібліотека з'являтиметься один раз на диску та один раз у пам'яті;

●спростити оновлення бібліотечних процедур і, крім того, здійснити оновлення, навіть після того, як програми, що їх використовують, були скомпільовані та пов'язані;

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

● не може запускатися і працювати сам по собі, оскільки він не має провідної програми;

● містить іншу інформацію у заголовку;

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

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

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

При явному зв'язуванніне потрібні бібліотеки імпорту і не потрібно завантажувати файли DLL одночасно з програмою користувача. Натомість користувальницька програма:

● здійснює явний виклик прямо під час роботи, щоб встановити зв'язок із файлом DLL;

● потім здійснює додаткові дзвінки, щоб отримати адреси процедур, які їй потрібні;

● після цього програма здійснює фінальний виклик, щоб розірвати зв'язок із файлом DLL;

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

David Drysdale, Beginner's guide to linkers

(http://www.lurklurk.org/linkers/linkers.html).

Мета цієї статті - допомогти C і C++ програмістам зрозуміти сутність того, чим займається компонувальник. За останні кілька років я пояснив це великій кількості колег і нарешті вирішив, що настав час перенести цей матеріал на папір, щоб він став доступнішим (і щоб мені не довелося пояснювати його знову). [Оновлення в березні 2009: додано додаткову інформацію про особливості компонування у Windows, а також детальніше розписано правило одного визначення (one-definition rule).

Типовим прикладом того, чому до мене зверталися за допомогою, є така помилка компонування:

g++ -o test1 test1a.o test1b.o

test1a.o(.text+0x18): In function `main':

: undefined reference to `findmax(int, int)"

collect2: ld returned 1 exit status

Якщо Ваша реакція - "напевно забув extern "C"", то Ви, швидше за все, знаєте все, що наведено в цій статті.

  • Визначення: що знаходиться в файлі C?
  • Що робить C компілятор
  • Що робить компонувальник: частина 1
  • Що робить операційна система
  • Що робить компонувальник: частина 2
  • C++ доповнення картини
  • Динамічно завантажені бібліотеки
  • Додатково

Визначення: що знаходиться в файлі C?

Цей розділ - коротке нагадування про різні складові C файлу. Якщо все в лістингу, наведеному нижче, має для Вас сенс, то, швидше за все, Ви можете пропустити цей розділ і відразу перейти до наступного.

Спочатку треба зрозуміти різницю між оголошенням та визначенням.

Визначення пов'язує ім'я з реалізацією, що може бути кодом або даними:

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

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

Для змінних існує визначення двох видів:

  • глобальні змінні, які існують протягом усього життєвого циклу програми («статичне розміщення») та які доступні у різних функціях;
  • локальні змінні, які існують тільки в межах деякої функції, що виконується («локальне розміщення») і які доступні тільки всередині цієї самої функції.

При цьому під терміном «доступні» слід розуміти «можна звернутися на ім'я, асоційоване зі змінною в момент визначення».

Існує пара окремих випадків, які з першого разу не здаються очевидними:

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

Варто зазначити, що, визначаючи функцію статичної, легко скорочується кількість місць, у тому числі можна звернутися до цієї функції на ім'я.

Для глобальних і локальних змінних, можемо розрізняти ініціалізована змінна чи ні, тобто. чи буде простір, відведений для змінної пам'яті, заповнено певним значенням.

І нарешті, ми можемо зберігати інформацію в пам'яті, яка динамічно виділена за допомогою malloc або new. В даному випадку немає можливості звернутися до виділеної пам'яті на ім'я, тому необхідно використовувати покажчики - іменовані змінні, що містять адресу неіменованої області пам'яті. Ця область пам'яті може бути звільнена за допомогою free або delete . У цьому випадку ми маємо справу з динамічним розміщенням.

Підсумуємо:

Глобальні

Локальні

Динамічні

Неініціа-

Неініціа-

Оголошення

int fn(int x);

extern int x;

extern int x;

Визначення

int fn(int x)

{ ... }

int x = 1;

(область дії

Файл)

int x;

(область дії – файл)

int x = 1;

(область дії – функція)

int x;

(область дії – функція)

int* p = malloc(sizeof(int));

Ймовірно більше легкий шляхЗасвоїти – це просто подивитися на приклад програми.

/* Визначення неініціалізованої глобальної змінної */

int x_global_uninit;

/* Визначення ініціалізованої глобальної змінної */

int x_global_init = 1;

/* Визначення неініціалізованої глобальної змінної, до якої

static int y_global_uninit;

/* Визначення ініціалізованої глобальної змінної, до якої

* можна звернутися по імені лише в межах цього C файлу */

static int y_global_init = 2;

/* Оголошення глобальної змінної, яка визначена десь

* в іншому місці програми */

extern int z_global;

/* Оголошення функції, яка визначена де-небудь іншому місці

* програми (Ви можете додати попереду "extern", однак це

* не обов'язково) */

int fn_a(int x, int y);

/* Визначення функції. Однак, будучи позначеною як static, її можна

* викликати на ім'я тільки в межах цього C файлу. */

static int fn_b(int x)

Return x+1;

/* Визначення функції. */

/* Параметр функції вважається локальною змінною. */

int fn_c(int x_local)

/* Визначення неініціалізованої локальної змінної */

Int y_local_uninit;

/* Визначення ініціалізованої локальної змінної */

Int y_local_init = 3;

/* Код, який звертається до локальних та глобальних змінних,

* а також функцій на ім'я */

X_global_uninit = fn_a (x_local, x_global_init);

Y_local_uninit = fn_a (x_local, y_local_init);

Y_local_uninit + = fn_b (z_global);

Return (x_global_uninit + y_local_uninit);

Що робить C компілятор

p align="justify"> Робота компілятора C полягає в конвертуванні тексту, (звичайно) зрозумілій людині, в щось, що розуміє комп'ютер. На вихід компілятор видає об'єктний файл. На платформах UNIX ці файли зазвичай мають суфікс.o; у Windows – суфікс.obj. Зміст об'єктного файлу - по суті дві речі:

код, що відповідає визначенню функції у файлі C

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

Код і дані, у разі, матимуть асоційовані із нею імена - імена функцій чи змінних, із якими пов'язані визначенням.

Об'єктний код - це послідовність (відповідним чином складених) машинних інструкцій, які відповідають C інструкціям, написаних програмістом: всі ці if"и while"и і навіть goto. Ці заклинання повинні маніпулювати інформацією певного роду, а інформація має бути де-небудь - для цього нам і потрібні змінні. Код може також посилатися на інший код (зокрема інші C функції в програмі).

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

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

Фактично компілятор залишає порожні місця. Порожнє місце (посилання) має ім'я, але значення, що відповідає цьому імені, поки не відомо.

Враховуючи це, ми можемо зобразити об'єктний файл, що відповідає програмі, наведеній вище, так:

Аналіз об'єктного файлу

Досі ми розглядали все на найвищому рівні. Проте корисно подивитись, як це працює на практиці. Основним інструментом буде команда nm, яка видає інформацію про символи об'єктного файлу на платформі UNIX. Для Windows команда dumpbin із опцією /symbols є приблизним еквівалентом. Також є портовані під Windows інструменти GNU binutils, які включають nm.exe.

Давайте подивимося, що видає nm для об'єктного файлу, отриманого з прикладу вище:

Symbols from c_parts.o:

Name Value Class Type Size Line Section

fn_a | | U | NOTYPE | | |*UND*

z_global | | U | NOTYPE | | |*UND*

fn_b | 00000000 | t | FUNC | 00000009 | |.text

x_global_init | 00000000 | D | OBJECT | 00000004 | |.data

y_global_uninit | 00000000 | b | OBJECT | 00000004 | |.bss

x_global_uninit | 00000004 | C | OBJECT | 00000004 | |*COM*

y_global_init |00000004| d | OBJECT | 00000004 | |.data

fn_c | 00000009 | T | FUNC | 00000055 | |.text

Результат може виглядати трохи по-різному на різних платформах (зверніться до man", щоб отримати відповідну інформацію), але ключовими відомостями є клас кожного символу і його розмір (якщо є). Клас може мати різні значення:

  • Клас U означає невизначені посилання, ті самі «порожні місця», згадані вище. Для цього класу існує два об'єкти: fn_a та z_global. (Деякі версії nm можуть виводити секцію, яка була б *UND* або UNDEF у цьому випадку.)
  • Класи t та T вказують на код, який визначений; різницю між t і T у тому, чи є функція локальної (t) у файлі чи ні (T), тобто. була функція оголошена як static. Знову ж таки в деяких системах може бути показана секція, наприклад.
  • Класи d і D містять ініціалізовані глобальні змінні. У цьому статичні змінні належать класу d. Якщо є інформація про секції, то це буде.data.
  • Для неініціалізованих глобальних змінних ми отримуємо b, якщо вони статичні і B або C інакше. Секцією в цьому випадку буде швидше за все. bss або * COM *.

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

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

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

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

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

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

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

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

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

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

Однак завантажувач не завжди є складовою системи програмування, оскільки виконувані ним функції дуже залежать від цільової архітектури обчислювальної системи, В якій виконується результуюча програма, створена системою програмування. На перших етапах розвитку ОС завантажувачі існували у вигляді окремих модулів, які виконували трансляцію адрес та готували програму до виконання – створювали так званий “образ задачі”. Така схема була характерною для багатьох ОС (наприклад, для ОСРВ на ЕОМ типу СМ-1, ОС RSX/11 або RAFOS на ЕОМ типу СМ-4 тощо). Образ завдання можна було зберегти на зовнішньому носіїабо ж створювати його знову щоразу під час підготовки програми до виконання.

З розвитком архітектури обчислювальних засобівкомп'ютера з'явилася можливість виконувати трансляцію адрес безпосередньо у момент запуску програми на виконання. Для цього потрібно до складу файлу, що виконується, включити відповідну таблицю, що містить перелік посилань на адреси, які необхідно піддати трансляції. У момент запуску виконуваного файлу ОС обробляла цю таблицю та перетворювала відносні адреси на абсолютні. Така схема, наприклад, притаманна ОС типу MS-DOS. У цій схемі модуль завантажувача як такий відсутній (фактично він входить до складу ОС), а система програмування відповідальна лише за підготовку таблиці трансляції адрес – цю функцію виконує компонувальник.

У сучасних ОС є складні методи перетворення адрес, які працюють безпосередньо вже під час виконання програми. Ці методи засновані на можливостях, апаратно закладених в архітектуру обчислювальних комплексів. Методи трансляції адрес можуть бути засновані на сегментній, сторінковій та сегментно-сторінковій організації пам'яті. Тоді для виконання трансляції адрес у момент запуску програми мають бути підготовлені відповідні системні таблиці. Ці функції повністю лягають на модулі ОС, тому вони виконуються в системах програмування.

Статті до прочитання:

Як перейти на Miui 9 Stable Global із китайської прошивки? Розблокування завантажувача



препроцесор компілятор компонувальник (7)

Я хочу зрозуміти, на яку частину компілятора програми він дивиться і на що посилається лінкер. Тому я написав наступний код:

#include using namespace std; #include < class paramType >void FunctionTemplate (paramType val) (i = val)); test void Test :: DefinedCorrectFunction (int val) ( i = val ; ) void Test :: DefinedIncorrectFunction (int val ) ( i = val ) void main () ( Test testObject (1 ); //testObject.NonDefinedFunction(2);//testObject.FunctionTemplate (2); }

У мене є три функції:

  • DefinedCorrectFunction - це нормальна функція, оголошена та визначена правильно.
  • DefinedIncorrectFunction - ця функція оголошена правильно, але реалізація неправильна (відсутня;)
  • NonDefinedFunction - лише оголошення. Нема визначення.
  • FunctionTemplate - шаблон функції.

    Тепер, якщо я скомпілюю цей код, я отримую помилку компілятора для відсутнього ";" в DefinedIncorrectFunction.
    Припустимо, я виправити це, а потім прокоментувати testObject.NonDefinedFunction (2). Тепер я отримую помилку компонувальника. Тепер закоментуйте testObject.FunctionTemplate (2). Тепер я отримую помилку компілятора для відсутнього ";".

Для шаблонів функцій я розумію, що вони не чіпані компілятором, якщо вони не викликаються в коді. Отже, відсутні ";" не скаржиться компілятором, доки я не викликав testObject.FunctionTemplate (2).

Для testObject.NonDefinedFunction (2) компілятор не скаржився, але компонувальник робив це. Наскільки я розумію, весь компілятор повинен був знати, що оголошено функцію NonDefinedFunction. Він не дбав про здійснення. Потім лінкер скаржився, бо не міг знайти реалізацію. Все йде нормально.

Тому я не зовсім розумію, що робить компілятор і що робить компонувальник. Моє розуміння компонентів компонувальника посилань зі своїми викликами. Отже, коли NonDefinedFunction називається, він шукає скомпільовану реалізацію NonDefinedFunction і скаржиться. Але компілятор не дбав про реалізацію NonDefinedFunction, але це робилося для DefinedIncorrectFunction.

Я дуже вдячний, якщо хтось зможе пояснити це або дати деяке посилання.

Компілятор перевіряє, чи відповідає вихідний код мови та дотримується семантики мови. Висновок компілятора – це об'єктний код.

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

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

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

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

  1. Заголовки - інформація про файл
  2. Код об'єкта - код у машинній мові (цей код не може працювати сам по собі в більшості випадків)
  3. Інформація про переїзд. Яким частинам коду необхідно буде змінити адреси за фактичного виконання
  4. Символьна таблиця- Символи, на які посилається код. Вони можуть бути визначені в цьомукоді, імпортовані з інших модулів або визначені компонувальником
  5. Відлагоджувальна інформація - використовується відладчиками

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

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

Слід зазначити

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

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

Так що у вашому конкретному випадку -

  1. DefinedIncorrectFunction() - компілятор отримує визначення функції і починає компілювати його для створення об'єктного коду та вставки відповідного посилання в таблицю символів. Помилка компіляції через синтаксичну помилку, тому компілятор переривається з помилкою.
  2. NonDefinedFunction () - компілятор отримує декларацію, але не має визначення, тому він додає запис до таблиці символів і поміщає компонувальник для додавання відповідних значень (оскільки компонувальник обробляє купу об'єктних файлів, можливо, це визначення присутнє в якомусь іншому об'єктному файлі). У вашому випадку ви не вказуєте будь-який інший файл, тому компонувальник переривається з помилку undefined reference to NonDefinedFunctionтому що він не може знайти посилання на відповідний запис у таблиці символів.

Щоб зрозуміти це, ще раз скажемо, що ваш код структурований так:

#include #include class Test ( private : int i ; public : Test (int val ) ( i = val ;) void DefinedCorrectFunction (int val ); void DefinedIncorrectFunction (int val ); void NonDefinedFunction (int val ); template< class paramType >void FunctionTemplate (paramType val) (i = val;));

Файл try.cpp

#include "try.h" test void Test :: DefinedCorrectFunction (int val) ( i = val ; ) void Test :: DefinedIncorrectFunction (int val ) ( i = val ; ) int main () ( Test testObject (1 ); testObject . Невизначенафункція (2); //testObject.FunctionTemplate (2); return 0; )

Давайте спочатку тільки скопіюємо та зберіть код, але не зв'яжемо його

$ g ++ - c try. cpp-o try. o $

Цей крок протікає без жодних проблем. Таким чином, у вас є об'єктний код у try.o. Спробуємо зв'язати його

$ g ++ try . o try. o : In function ` main " : try . cpp :(. text + 0x52 ): undefined reference to ` Test : NonDefined

Ви забули визначити Test::NonDefinedFunction. Давайте визначимо його в окремомуфайл.

Файл-try1.cpp

#include "try.h" void Test :: NonDefinedFunction (int val) (i = val;)

Скомпілюємо його в об'єктний код

$ g ++ - c try1. cpp-o try1. o $

Знову ж таки, це успішно. Спробуємо зв'язати лише цей файл

$ g ++ try1 . o/usr/lib/gcc/x86_64-redhat-linux/4.4. 5 /../../../../ lib64 / crt1 . o : In function ` _start ": (. text + 0x20 ): undefined reference to ` main " collect2 : ld returned 1 exit status

Немає основної так виграної!

Тепер у вас є два окремі об'єктні коди, в яких є всі необхідні компоненти. Просто передайте обох з них у компонувальник, і нехай це зробить інше

$ g ++ try . o try1. o $

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

Скажіть, що ви хочете з'їсти якийсь суп, тому вирушайте в ресторан.

Ви шукаєте меню для супу. Якщо ви не знайдете його в меню, ви залишаєте ресторан. (на зразок компілятора, що скаржиться на те, що він не зміг знайти функцію). Якщо ви знайдете, що ви робите?

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

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

Компілятор шукає функцій для виклику там, де вони потрібні. Тут цього не потрібно, тому скарги немає. У цьому файлі немає помилки, оскільки навіть якщо це було необхідно, вона не може бути реалізована в цьому конкретному блоці компіляції. Компонент відповідає за збір різних блоків компіляції, тобто їх «зв'язування».

Те, що робить компілятор, і що робить компонувальник, залежить від реалізації: правова реалізація може просто зберігати токенований джерело в компіляторі і робити все в компонувальника. Сучасні реалізації Усебільше відкладають на компонувальник, для кращої оптимізації. І багато ранніх реалізації шаблонів навіть не дивилися код шаблону до тих пір, поки час посилання, крім відповідних фігурних дужок, не буде достатньо, щоб дізнатися, де шаблон закінчився. З погляду користувача вас більше цікавить, чи є помилка «необхідною діагностикою» (яка може бути обрана компілятором або компонувальником) чи є невизначеною поведінкою.

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

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

Це змінено в таких випадках, як вбудовані функції, де вам дозволено повторювати визначення в кожній одиниці перекладу і надзвичайно модифікувати шаблони, оскільки багато помилок не можуть бути виявлені до створення екземпляра. У випадку шаблонів стандарт залишає реалізації великою свободою: принаймні компілятор повинен аналізувати шаблон достатньо, щоб визначити, де шаблон закінчується. Стандарт додав такі речі, як typename , однак, щоб дозволити набагато більше розбору до створення екземпляра. Однак у залежних контекстах деякі помилки не можуть бути виявлені до створення екземпляра, що може мати місце під час компіляції або в час ранньої реалізації, що сприяв створенню моменту часу; компіляція часу домінує сьогодні і використовується VC ++ і g ++.

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

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

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

Я вважаю, що це ваше питання:

Там, де я заплутався, компілятор скаржився на DefinedIncorrectFunction. Він шукав реалізацію NonDefinedFunction, але пройшов через DefinedIncorrectFunction.

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

Операційні системи (ОС)