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

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

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

Ответы

Ответ 1

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

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

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

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

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

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

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

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

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

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

Компиляция

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

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

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

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

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

Связь

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

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

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

Ответ 2

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

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

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

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

Ответ 3

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

Составление

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

Связь

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

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

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

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

Ответ 4

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

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

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

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

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

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

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