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

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

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

6 ответов


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

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

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

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

предварительная обработка

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

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

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

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

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

сборник

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

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

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

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

на этом этапе сообщаются "Обычные" ошибки компилятора, такие как синтаксические ошибки или ошибки разрешения перегрузки.

связь

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

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

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


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

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

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

сборник

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

связь

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

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

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

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


на стандартном фронте:

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

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

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


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

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

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

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

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


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


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

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

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

Preprocessin с помощью языка Си препроцессор (ЧГК.exe), который включает в себя заголовки (#include) и разворачивает макросы (#define).

cpp Здравствуйте.c > Здравствуйте.я!--16-->

результирующий промежуточный файл " hello.я" содержит расширенный исходный код.

2. Сборник

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

ССЗ -с здравствуйте.я!--16-->

параметр-S указывает на создание кода сборки вместо кода объекта. Результирующий файл сборки - " hello.с."

3. Собрание

ассемблер (as.exe) преобразует код сборки в машинный код в объектном файле " hello.о."

as-o Здравствуйте.О привет.s

4. Линкер!--5-->

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

ЛД -О привет.ехе привет.о. ..библиотеки...