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

417

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

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

неизвестно
источник

Ответы:

555

Компиляция программы на C ++ состоит из трех этапов:

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

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

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

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

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

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

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

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

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

компиляция

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

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

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

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

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

соединение

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

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

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

R. Martinho Fernandes
источник
39
Этап компиляции также вызывает ассемблер перед преобразованием в объектный файл.
Манав Мн
3
Где применяются оптимизации? На первый взгляд кажется, что это будет сделано на этапе компиляции, но с другой стороны, я могу себе представить, что правильную оптимизацию можно выполнить только после компоновки.
Барт ван Хейкелом
6
@BartvanHeukelom традиционно это делалось во время компиляции, но современные компиляторы поддерживают так называемую «оптимизацию во время компоновки», которая имеет преимущество в том, что она способна оптимизировать все единицы перевода.
Р. Мартиньо Фернандес
3
Есть ли у C такие же шаги?
Кевин Чжу
6
Если компоновщик преобразует символы, относящиеся к классам / методам в библиотеках, в адреса, означает ли это, что двоичные файлы библиотеки хранятся в адресах памяти, которые ОС поддерживает постоянными? Я просто не понимаю, как компоновщик может узнать точный адрес, скажем, двоичного файла stdio для всех целевых систем. Путь к файлу всегда будет одинаковым, но точный адрес может измениться, верно?
Дэн Картер
42

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

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

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

компиляция

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

соединение

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

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

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

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

нейросеть
источник
1
Чего я не понимаю, так это того, что если препроцессор управляет такими вещами, как #includes, чтобы создать один супер файл, то после этого нечем связать?
binarysmacker
@binarysmacer Посмотрите, имеет ли смысл то, что я написал ниже. Я пытался описать проблему изнутри.
эллиптический вид
3
@binarysmacker Слишком поздно комментировать это, но другие могут найти это полезным. youtu.be/D0TazQIkc8Q В основном вы включаете заголовочные файлы, и эти заголовочные файлы обычно содержат только объявления переменных / функций, а не определения, определения могут присутствовать в отдельном исходном файле. Так что препроцессор включает только объявления, а не определения, это где помогает компоновщик. Вы связываете исходный файл, который использует переменную / функцию, с исходным файлом, который их определяет.
Каран Джойшер
24

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

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

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

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

AProgrammer
источник
14
Не могли бы вы перечислить все 9 этапов? Я думаю, это было бы хорошим дополнением к ответу. :)
jalf
@jalf: Связано: stackoverflow.com/questions/1476892/… .
СБи
@jalf, просто добавьте создание шаблона непосредственно перед последней фазой в ответе, указанном @sbi. Во IIRC есть небольшие различия в точной формулировке обработки широких символов, но я не думаю, что они появляются в метках диаграммы.
AProgrammer
2
@ sbi да, но это должен быть вопрос FAQ, не так ли? Так не должна ли эта информация быть доступна здесь ? ;)
Джалф
3
@AProgrammmer: было бы полезно просто перечислить их по имени. Тогда люди знают, что искать, если они хотят больше деталей. В любом случае, +1 добавил ваш ответ в любом случае :)
jalf
14

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

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

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

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

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

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

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

мое имя пользователя было похищено здесь
источник
13

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

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

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

Предварительная обработка через GNU C Preprocessor ( 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 hello.s

4. Линкер

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

    ld -o привет hello.o ... библиотеки ...
Kaps
источник
9

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

Чарльз Ван
источник
2
Спасибо, что поделились этим, это так просто и понятно для понимания.
Марк
Хорошо, ресурс, не могли бы вы дать здесь базовое объяснение процесса, ответ помечается алгоритмом как низкое качество, потому что оно короткое и только URL.
JasonB
Хороший короткий учебник, который я нашел: calleerlandsson.com/the-four-stages-of-compiling-ac-program
Guy Avraham