Как работает процесс компиляции / компоновки? - PullRequest
381 голосов
/ 07 июня 2011

Как работает процесс компиляции и компоновки?

(Примечание: это должна быть запись в FAQ по C ++ для Stack Overflow . Если вы хотите критиковать идею предоставления FAQ в этой форме, тогда публикация в мета, которая началась все это было бы местом для этого. Ответы на этот вопрос отслеживаются в C ++ чат-комнате , где идея FAQ возникла в первую очередь, поэтому ваш ответ, скорее всего, будет прочитан теми, кто придумал эту идею.)

Ответы [ 6 ]

505 голосов
/ 07 июня 2011

Компиляция программы на C ++ включает три этапа:

  1. Предварительная обработка: препроцессор берет файл исходного кода C ++ и обрабатывает #include s, #define s и другие директивы препроцессора. Результатом этого шага является «чистый» файл C ++ без директив препроцессора.

  2. Компиляция: компилятор принимает выходные данные препроцессора и создает из него объектный файл.

  3. Связывание: компоновщик берет объектные файлы, созданные компилятором, и создает либо библиотеку, либо исполняемый файл.

Препроцессирование

Препроцессор обрабатывает директивы препроцессора , такие как #include и #define. Он не зависит от синтаксиса C ++, поэтому его следует использовать с осторожностью.

Работает с одним исходным файлом C ++ одновременно, заменяя директивы #include содержимым соответствующих файлов (обычно это просто объявления), заменяя макросы (#define) и выбирая различные части текста. в зависимости от директив #if, #ifdef и #ifndef.

Препроцессор работает с потоком токенов предварительной обработки. Подстановка макросов определяется как замена токенов другими токенами (оператор ## позволяет объединить два токена, когда это имеет смысл).

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

На этом этапе могут возникать некоторые ошибки при грамотном использовании директив #if и #error.

Компиляция

Этап компиляции выполняется на каждом выходе препроцессора. Компилятор анализирует чистый исходный код C ++ (теперь без каких-либо директив препроцессора) и преобразует его в ассемблерный код. Затем вызывает базовый сервер (ассемблер в инструментальной цепочке), который собирает этот код в машинный код, создавая настоящий двоичный файл в некотором формате (ELF, COFF, a.out, ...). Этот объектный файл содержит скомпилированный код (в двоичном виде) символов, определенных во входных данных. Символы в объектных файлах называются по имени.

Объектные файлы могут ссылаться на символы, которые не определены. Это тот случай, когда вы используете декларацию, а не даете ей определение. Компилятор не возражает против этого и с удовольствием создаст объектный файл, если исходный код правильно сформирован.

Компиляторы обычно позволяют вам остановить компиляцию на этом этапе. Это очень полезно, потому что с его помощью вы можете скомпилировать каждый файл исходного кода отдельно. Преимущество этого в том, что вам не нужно перекомпилировать все , если вы изменяете только один файл.

Созданные объектные файлы могут быть помещены в специальные архивы, называемые статическими библиотеками, для более легкого последующего использования.

На этом этапе сообщается о «обычных» ошибках компилятора, таких как синтаксические ошибки или ошибки разрешения перегрузки.

Linking

Линкер - это то, что создает окончательный вывод компиляции из объектных файлов, созданных компилятором. Эти выходные данные могут быть либо общей (или динамической) библиотекой (и, хотя имя схоже, они не имеют много общего со статическими библиотеками, упомянутыми ранее), либо исполняемым файлом.

Связывает все объектные файлы, заменяя ссылки на неопределенные символы правильными адресами. Каждый из этих символов может быть определен в других объектных файлах или в библиотеках. Если они определены в библиотеках, отличных от стандартной библиотеки, вам необходимо сообщить о них компоновщику.

На этом этапе наиболее распространенными ошибками являются пропущенные определения или дубликаты определений. Первое означает, что либо определения не существуют (т.е. они не записаны), либо объектные файлы или библиотеки, в которых они находятся, не были переданы компоновщику. Последнее очевидно: один и тот же символ был определен в двух разных объектных файлах или библиотеках.

34 голосов
/ 18 ноября 2013

Эта тема обсуждается на CProgramming.com:
https://www.cprogramming.com/compilingandlinking.html

Вот что написал автор:

Компиляция - это не то же самое, что созданиеисполняемый файл!Вместо этого создание исполняемого файла представляет собой многоступенчатый процесс, разделенный на два компонента: компиляция и компоновка.В действительности, даже если программа «хорошо компилируется», она может не работать из-за ошибок на этапе компоновки.Общий процесс перехода от файлов исходного кода к исполняемому файлу лучше назвать сборкой.

Компиляция

Компиляция относится к обработке файлов исходного кода (.c, .cc,или .cpp) и создание файла объекта.Этот шаг не создает ничего, что пользователь может запустить.Вместо этого компилятор просто создает инструкции машинного языка, которые соответствуют скомпилированному файлу исходного кода.Например, если вы компилируете (но не связываете) три отдельных файла, у вас будет три объектных файла, созданных в качестве выходных данных, каждый с именем .o или .obj (расширение будет зависеть от вашего компилятора).Каждый из этих файлов содержит перевод файла исходного кода в файл машинного языка - но вы пока не можете запустить их!Вам нужно превратить их в исполняемые файлы, которые может использовать ваша операционная система.Вот тут и вступает компоновщик.

Связывание

Связывание относится к созданию одного исполняемого файла из нескольких объектных файлов.На этом этапе обычно компоновщик будет жаловаться на неопределенные функции (обычно саму main).Во время компиляции, если компилятор не может найти определение для определенной функции, он просто предполагает, что функция была определена в другом файле.Если это не так, компилятор не узнает об этом - он не смотрит на содержимое более чем одного файла за раз.Компоновщик, с другой стороны, может просматривать несколько файлов и пытаться найти ссылки на функции, которые не были упомянуты.

Вы можете спросить, почему существуют отдельные этапы компиляции и компоновки.Во-первых, возможно, так проще реализовать.Компилятор делает свое дело, а компоновщик делает свое дело - сохраняя функции раздельными, сложность программы уменьшается.Другое (более очевидное) преимущество заключается в том, что это позволяет создавать большие программы без необходимости повторять этап компиляции при каждом изменении файла.Вместо этого, используя так называемую «условную компиляцию», необходимо компилировать только те исходные файлы, которые были изменены;в остальном, объектные файлы являются достаточным входом для компоновщика.Наконец, это упрощает реализацию библиотек предварительно скомпилированного кода: просто создайте объектные файлы и свяжите их, как любой другой объектный файл.(Тот факт, что каждый файл компилируется отдельно от информации, содержащейся в других файлах, кстати, называется «отдельной моделью компиляции».)

Чтобы получить все преимущества компиляции условий, вероятно, проще получить программучтобы помочь вам, чем пытаться вспомнить, какие файлы вы изменили с момента последней компиляции.(Конечно, вы можете просто перекомпилировать каждый файл, у которого временная метка больше, чем у соответствующего объектного файла.) Если вы работаете с интегрированной средой разработки (IDE), она может позаботиться об этом за вас.Если вы используете инструменты командной строки, есть отличная утилита make, которая поставляется с большинством дистрибутивов * nix.Наряду с условной компиляцией, она имеет несколько других приятных функций для программирования, например, позволяет выполнять различные компиляции вашей программы - например, если у вас есть версия, предоставляющая подробный вывод для отладки.

Зная разницу между фазой компиляции и фазой линка, можно упростить поиск ошибок.Ошибки компилятора обычно имеют синтаксическую природу - пропущенная точка с запятой, лишние скобки.Связывание ошибок обычно связано с отсутствием или множеством определения. Если вы получили ошибку, что функция или переменная определено несколько раз из компоновщика, это хороший признак того, что ошибка в том, что два ваших файла исходного кода имеют одинаковую функцию или переменная.

23 голосов
/ 07 июня 2011

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

  • a единица перевода - это комбинация исходных файлов, включенных заголовков и исходных файлов за вычетом любых строк исходного текста, пропущенных директивой препроцессора условного включения.

  • Стандарт определяет 9 этапов перевода. Первые четыре соответствуют предварительной обработке, следующие три - это компиляция, следующие - создание экземпляров шаблонов (производящих единицы создания экземпляров ), а последний - связывание.

На практике восьмой этап (создание шаблонов) часто выполняется во время процесса компиляции, но некоторые компиляторы задерживают его на этапе компоновки, а некоторые распространяют его на два.

14 голосов
/ 10 января 2014

Сложным является то, что ЦП загружает данные с адресов памяти, сохраняет данные по адресам памяти и выполняет инструкции последовательно из адресов памяти с некоторыми условными переходами в последовательности обработанных инструкций. Каждая из этих трех категорий команд включает в себя вычисление адреса ячейки памяти, которая будет использоваться в машинной инструкции. Поскольку машинные инструкции имеют переменную длину в зависимости от конкретной задействованной инструкции, и поскольку мы собираем их переменную длину вместе, когда строим наш машинный код, в вычислении и построении любых адресов используется двухэтапный процесс.

Сначала мы выкладываем распределение памяти как можно лучше, прежде чем узнаем, что именно происходит в каждой ячейке. Мы выясняем байты, или слова, или что-либо, что формирует инструкции, литералы и любые данные. Мы просто начинаем выделять память и строим значения, которые создадут программу по ходу работы, и записываем все, что нам нужно, чтобы вернуться и исправить адрес. В этом месте мы помещаем пустышку, чтобы просто заполнить местоположение, чтобы мы могли продолжить вычислять объем памяти. Например, наш первый машинный код может занимать одну ячейку. Следующий машинный код может занять 3 ячейки, включая одну ячейку машинного кода и две ячейки адреса. Теперь наш адресный указатель равен 4. Мы знаем, что происходит в ячейке машины, которая является кодом операции, но нам нужно подождать, чтобы вычислить, что идет в ячейках адреса, пока мы не узнаем, где эти данные будут расположены, т.е. что будет машинный адрес этих данных.

Если бы был только один исходный файл, компилятор теоретически мог бы создать полностью исполняемый машинный код без компоновщика. В двухпроходном процессе он может вычислить все фактические адреса для всех ячеек данных, на которые ссылается любая инструкция загрузки или сохранения компьютера. И он может вычислить все абсолютные адреса, на которые ссылаются любые инструкции абсолютного перехода. Вот как работают более простые компиляторы, такие как в Forth, без компоновщика.

Компоновщик - это то, что позволяет компилировать блоки кода отдельно. Это может ускорить весь процесс создания кода и дает некоторую гибкость в отношении того, как блоки используются позже, другими словами, их можно перемещать в памяти, например, добавляя 1000 к каждому адресу, чтобы поднять блок на 1000 ячеек адреса.

Итак, что выводит компилятор, это грубый машинный код, который еще не полностью собран, но выложен так, чтобы мы знали размер всего, другими словами, чтобы мы могли начать вычислять, где будут находиться все абсолютные адреса. Компилятор также выводит список символов, которые являются парами имя / адрес. Символы связывают смещение памяти в машинном коде в модуле с именем. Смещение - это абсолютное расстояние до ячейки памяти символа в модуле.

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

Очевидно, что я слишком упростил это, так что вы можете попытаться понять это, и я сознательно не использовал жаргон объектных файлов, таблиц символов и т. Д., Что для меня является частью путаницы.

9 голосов
/ 20 марта 2014

Посмотрите на URL: http://faculty.cs.niu.edu/~mcmahon/CS241/Notes/compile.html
Полный процесс компиляции C ++ четко представлен в этом URL.

4 голосов
/ 13 августа 2018

GCC компилирует программу на C / C ++ в исполняемый файл за 4 шага.

Например, «gcc -o hello.exe hello.c» выполняется следующим образом:

1.Предварительная обработка

Препроцессор через препроцессор GNU C (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 привет.

4.Компоновщик

Наконец, компоновщик (ld.exe) связывает объектный код с кодом библиотеки для создания исполняемого файла "hello.exe".

ld -o hello.exe hello.o ... библиотеки ...

...