Чтобы исходный код мог выполняться, его сначала необходимо преобразовать в двоичный машинный код. Это задача компилятора.
Например, рассмотрим следующий исходный код (предположим, что имя файла — test.c).
#include <stdio.h>
int main(void)
{
fputs("Hello, world!\n", stdout);
return 0;
}
Прежде чем он сможет выполняться, его сначала должен обработать компилятор.
$ gcc test.c $ ./a.out Hello, world!
Для сложных проектов процесс компиляции необходимо разделить на три этапа.
$ ./configure $ make $ make install
Что именно делают эти команды? Большинство книг и материалов не дают ясного ответа на этот вопрос, просто говоря «вот так и компилируйте», без дальнейших пояснений.
В этой статье будет рассказано, как работает компилятор, то есть какие задачи выполняет каждая из трёх вышеуказанных команд. В основном я опираюсь на статью Алекса Смита «Building C Projects». Следует отметить, что эта статья в основном ориентирована на компилятор gcc, то есть C и C++, и может не применяться к компиляции других языков.

Шаг 1: Configure
Прежде чем компилятор начнёт работу, ему нужно знать текущее системное окружение, например, где находится стандартная библиотека, где расположена папка установки программного обеспечения, какие компоненты нужно установить, и так далее. Это связано с тем, что на разных компьютерах разные системные окружения. Указывая параметры компиляции, компилятор может гибко адаптироваться к окружению и создавать машинный код, который может выполняться в различных окружениях. Этот этап определения параметров компиляции называется «configure».
Эта информация о конфигурации хранится в файле конфигурации, обычно это файл сценария с именем configure. Обычно он создаётся инструментом autoconf. Компилятор получает параметры компиляции, запуская этот сценарий.
Скрипт configure уже пытается учитывать различия между системами и предоставляет значения по умолчанию для различных параметров компиляции. Если системное окружение пользователя необычно или имеются особые требования, пользователю необходимо вручную передать параметры компиляции скрипту configure.
$ ./configure --prefix=/www --with-mysql
Приведённый выше код — это конфигурация компиляции исходного кода PHP. Пользователь указывает, что установленные файлы сохраняются в каталоге www, и что при компиляции включается поддержка модуля mysql.
Шаг 2: Определение местоположения стандартной библиотеки и заголовочных файлов
Исходный код неизбежно использует функции стандартной библиотеки и заголовочные файлы. Они могут храниться в любом каталоге системы. Компилятор не может автоматически определить их местоположение; он может узнать его только из файла конфигурации.
Второй шаг компиляции — узнать из файла конфигурации местоположение стандартной библиотеки и заголовочных файлов. Обычно файл конфигурации предоставляет список нескольких конкретных каталогов. Во время компиляции компилятор ищет в этих каталогах по порядку, чтобы найти нужные цели.
Шаг 3: Определение зависимостей
В больших проектах часто существуют зависимости между исходными файлами. Компилятору необходимо определить порядок компиляции. Предположим, файл A зависит от файла B; компилятор должен обеспечить выполнение следующих двух условий:
(1) Файл A может быть скомпилирован только после того, как скомпилирован файл B.
(2) При изменении файла B файл A будет перекомпилирован.
Порядок компиляции хранится в файле с именем makefile, в котором перечислено, какие файлы компилируются раньше, а какие позже. Файл makefile генерируется при запуске configure, поэтому перед компиляцией необходимо сначала запустить configure.
Определяя зависимости, компилятор также определяет, какие заголовочные файлы будут использоваться во время компиляции.
Шаг 4: Предкомпиляция заголовочных файлов
Разные исходные файлы могут ссылаться на один и тот же заголовочный файл (например, stdio.h). Во время компиляции заголовочные файлы также должны компилироваться. Чтобы сэкономить время, компилятор компилирует заголовочные файлы перед компиляцией исходного кода. Это гарантирует, что каждый заголовочный файл компилируется только один раз, а не перекомпилируется при каждом использовании.
Однако не всё содержимое заголовочных файлов подвергается предварительной компиляции. Директива #define, используемая для объявления макросов, не подвергается предварительной компиляции.
Шаг 5: Предварительная обработка
После предварительной компиляции компилятор начинает заменять заголовочные файлы и макросы в исходном коде. Возьмём в качестве примера исходный код из начала этой статьи: он содержит заголовочный файл stdio.h, и после замены он выглядит так:
extern int fputs(const char *, FILE *);
extern FILE *stdout;
int main(void)
{
fputs("Hello, world!\n", stdout);
return 0;
}
Для удобочитаемости приведённый выше код показывает только те части заголовочного файла, которые связаны с исходным кодом, то есть объявления fputs и FILE, опуская остальную часть stdio.h (поскольку она очень длинная). Кроме того, заголовочный файл в приведённом выше коде не был предварительно скомпилирован; фактически в исходный код вставляется результат предварительной компиляции. На этом шаге компилятор также удаляет комментарии.
Этот шаг называется «предварительной обработкой», потому что после его завершения начинается настоящая обработка.
Шаг 6: Компиляция
После предварительной обработки компилятор начинает генерировать машинный код. У некоторых компиляторов есть промежуточный шаг: сначала преобразовать исходный код в ассемблерный код, а затем преобразовать ассемблерный код в машинный код.
Ниже приведён ассемблерный код, сгенерированный из исходного кода в начале этой статьи.
.file "test.c"
.section .rodata
.LC0:
.string "Hello, world!\n"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movq stdout(%rip), %rax
movq %rax, %rcx
movl $14, %edx
movl $1, %esi
movl $.LC0, %edi
call fwrite
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Debian 4.9.1-19) 4.9.1"
.section .note.GNU-stack,"",@progbits
Этот преобразованный файл называется объектным файлом.
Шаг 7: Компоновка
Объектный файл пока не может быть запущен; его нужно дополнительно преобразовать в исполняемый файл. Если внимательно посмотреть на результат предыдущего шага, вы заметите, что он ссылается на функцию stdout и функцию fwrite. То есть, чтобы программа нормально работала, помимо приведённого выше кода, она должна также содержать код функций stdout и fwrite, которые предоставляются стандартной библиотекой C.
Следующая задача компилятора — добавить код внешних функций (обычно это файлы с суффиксами .lib и .a) в исполняемый файл. Это называется компоновкой. Этот способ добавления библиотек внешних функций в исполняемый файл путём копирования называется статической компоновкой. О динамической компоновке будет сказано позже.
Роль команды make состоит в том, чтобы начать с шага 4, предкомпиляции заголовочных файлов, и продолжать его до завершения этого шага.
Шаг 8: Установка
Компоновка на предыдущем шаге выполняется в памяти, то есть компилятор создаёт исполняемый файл в памяти. Затем исполняемый файл необходимо сохранить в каталог установки, заранее указанный пользователем.
На первый взгляд этот шаг прост: просто скопировать туда исполняемый файл (вместе с соответствующими файлами данных). Однако на практике этот шаг также включает создание каталогов, сохранение файлов, установку прав доступа и так далее. Весь этот процесс сохранения называется «Установка».
Шаг 9: Интеграция с операционной системой
После установки исполняемого файла нужно каким-то образом уведомить операционную систему о том, что программа доступна. Например, если мы устанавливаем программу для чтения текстовых файлов, мы часто хотим, чтобы программа автоматически запускалась при двойном щелчке по txt-файлу.
Для этого требуется зарегистрировать метаданные программы в операционной системе: имя файла, описание файла, связанные расширения файлов и так далее. В системах Linux эта информация обычно хранится в файлах .desktop в каталоге /usr/share/applications. Кроме того, в операционных системах Windows необходимо создать ярлык в меню «Пуск».
Эти задачи называются «Интеграция с операционной системой». Команда make install используется для выполнения этапов «Установка» и «Интеграция с операционной системой».
Шаг 10: Создание установочного пакета
На этом общий процесс компиляции исходного кода в основном завершён. Но лишь очень небольшое число пользователей готово терпеливо пройти весь этот процесс от начала до конца. На самом деле, если вы дадите пользователям только исходный код, они сочтут это недружелюбным. Большинство пользователей хотят получить бинарную исполняемую программу, которая может сразу запуститься. Для этого разработчикам нужно превратить исполняемый файл, созданный на предыдущем шаге, в распространяемый установочный пакет.
Поэтому компилятор также должен уметь создавать установочные пакеты. Обычно исполняемый файл (вместе с связанными файлами данных) сохраняется в виде сжатого архива с определённой структурой каталогов и передаётся пользователю.
Шаг 11: Динамическое связывание
Обычно к этому шагу программа уже способна работать. Что происходит во время выполнения — это не связано с компилятором. Однако разработчик может выбрать на этапе компиляции, как исполняемый файл связывается с внешними библиотеками функций: либо статическое связывание (связывание во время компиляции), либо динамическое связывание (связывание во время выполнения). Поэтому в завершение стоит также упомянуть, что означает динамическое связывание.
Как упоминалось ранее, статическая компоновка означает копирование внешних библиотек функций в исполняемый файл. Преимущество — широкая применимость: не нужно беспокоиться о том, что на машине пользователя отсутствует файл библиотеки. Недостаток — установочный пакет относительно большой, и несколько приложений не могут совместно использовать файлы библиотек. Динамическая компоновка — противоположность: внешние библиотеки функций не включаются в установочный пакет и только динамически упоминаются во время выполнения. Преимущество — установочный пакет меньше, и несколько приложений могут совместно использовать файлы библиотек. Недостаток — у пользователей должны быть предварительно установлены файлы библиотек, и как версия, так и место установки должны соответствовать требованиям; иначе программа не будет нормально работать.
На практике большинство программного обеспечения использует динамическую компоновку и разделяемые библиотечные файлы. Эти динамически разделяемые библиотечные файлы имеют расширение .so на платформе Linux, .dll на платформе Windows и .dylib на платформе Mac.
Оригинальный источник: http://www.ruanyifeng.com/blog/2014/11/compiler.html