Перейти к основному содержимому

5.16. Преобразование кода в программу

Разработчику Архитектору

Преобразование кода в программу

От исходного кода к исполняемому файлу: этапы сборки

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

1. Препроцессор

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

#include <stdio.h>

Эта строка заменяется полным текстом заголовочного файла stdio.h, который содержит объявления стандартных функций ввода-вывода, таких как printf и scanf. Заголовочные файлы (с расширением .h) не содержат реализации функций — только их объявления: имена, типы возвращаемых значений и параметров. Это позволяет компилятору проверять корректность вызовов до того, как будет известно, как функция устроена внутри.

Другие часто используемые директивы:

  • #define — определяет макрос или константу;
  • #ifdef, #ifndef, #endif — позволяют условно включать или исключать части кода в зависимости от наличия определённых символов.

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

2. Компилятор

Компилятор принимает обработанный препроцессором код и преобразует его в ассемблерный код, специфичный для целевой архитектуры процессора (x86, ARM, RISC-V и другие). Ассемблер — это язык низкого уровня, где каждая инструкция почти напрямую соответствует одной машинной команде.

Например, выражение на Си:

int a = b + c;

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

mov eax, DWORD PTR [rbp - 8]   ; загрузить значение b в регистр eax
add eax, DWORD PTR [rbp - 12] ; прибавить значение c
mov DWORD PTR [rbp - 4], eax ; сохранить результат в a

Здесь используются регистры процессора (eax, rbp) и операции перемещения (mov) и сложения (add). Ассемблерный код читаем для человека, но уже близок к тому, что понимает процессор.

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

3. Объектный файл

Объектный файл (обычно с расширением .o в Unix-подобных системах или .obj в Windows) содержит машинный код, полученный из ассемблера, но ещё не готовый к выполнению. В нём находятся:

  • скомпилированные функции и глобальные переменные;
  • таблица символов — список имён функций и переменных, определённых в файле;
  • ссылки на внешние символы, которые будут разрешены позже (например, вызов printf).

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

4. Ассемблер

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

5. Компоновщик (линкер)

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

Например, если в одном файле вызывается функция calculate(), а её тело определено в другом, компоновщик свяжет эти два объектных файла. Аналогично он подключает стандартную библиотеку Си (libc), чтобы обеспечить работу printf, malloc и других функций.

Результат работы компоновщика — исполняемый файл (в Linux — без расширения или .out, в Windows — .exe), который операционная система может загрузить в память и запустить.

6. Исполняемый файл

Исполняемый файл содержит:

  • машинный код всех функций программы;
  • данные (глобальные переменные, строки и константы);
  • метаданные — точку входа (main), таблицу импорта/экспорта, информацию о сегментах памяти.

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


Заголовочные файлы и объявление функций

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

Объявление функции (function declaration) указывает компилятору:

  • имя функции;
  • тип возвращаемого значения;
  • количество и типы параметров.

Пример:

int add(int a, int b);

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

int add(int a, int b) {
return a + b;
}

Без объявления компилятор не сможет проверить корректность вызова add(5, 3). Заголовочный файл собирает такие объявления и делает их доступными через #include.

Хорошая практика — каждый .c-файл сопровождать соответствующим .h-файлом. Например, модуль math_utils.c имеет заголовок math_utils.h, который включают все, кто хочет использовать его функции.


Файл сборки

В простых проектах сборка может выполняться одной командой:

gcc main.c utils.c -o program

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

Традиционный инструмент — Makefile, используемый с утилитой make. Он описывает правила: из каких исходных файлов получаются объектные, как их связать, куда поместить результат.

Современные системы сборки включают CMake, Meson, Ninja. Они генерируют платформо-независимые файлы сборки и упрощают кросс-компиляцию, управление зависимостями и сборку под разные конфигурации (отладка, релиз).

Файл сборки — это «рецепт» превращения исходного кода в работающую программу. Он делает процесс воспроизводимым, автоматизированным и переносимым.


Этот взгляд «под капот» сборки помогает понять, почему программа на Си работает так, как работает. Каждый этап — от #include до запуска .exe — имеет своё назначение, свою логику и свою историю. Освоив эти механизмы, программист получает не только технические знания, но и глубокое уважение к элегантности и продуманности языка Си.