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


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

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

6 342

6 ответов:

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

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

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

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

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

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

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

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

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

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

сборник

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

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

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

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

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

связь

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

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

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

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

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

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

сборник

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

связь

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

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

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

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

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

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

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

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

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

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

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

компоновщик-это то, что позволяет компилировать блоки кода отдельно. Это может ускорить общий процесс построения кода, и позволяет некоторую гибкость с тем, как блоки позже используются, другими словами, они могут быть перемещены в памяти, например, добавив 1000 к каждому адресу, чтобы scoot блок до 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. Предварительная обработка

препроцессор через препроцессор GNU C (cpp.exe), который включает в себя заголовки (#include) и разворачивает макросы (#define).

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

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

2. Компиляция

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

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

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

3. Сборка

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

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

4. Линкер

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

ld-o Здравствуйте.exe Здравствуйте.о. ..библиотеки...