← Назад к вопросам

Что получается в итоге компиляции?

1.0 Junior🔥 121 комментариев
#Сборка и инструменты#Язык C++

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что получается в итоге компиляции C/C++

Этот вопрос проверяет понимание полного pipeline компиляции. Дам ответ с точки зрения практикующего backend разработчика.

Полный процесс компиляции

Компиляция C/C++ — это многостадийный процесс, который состоит из нескольких фаз:

Исходный код (.cpp, .h)
    ↓
[1] Препроцессор (preprocessor)
    ↓
Трансформированный исходный код
    ↓
[2] Компилятор (compiler)
    ↓
Объектный код (.o, .obj)
    ↓
[3] Компоновщик (linker)
    ↓
Исполняемый файл (executable) или библиотека (.a, .lib, .dll, .so)

Этап 1: Препроцессор

Препроцессор обрабатывает директивы препроцессора:

#include <iostream>      // Вставляет содержимое файла
#define MAX 100         // Замены текста
#ifdef DEBUG            // Условная компиляция
#pragma optimize(off)   // Инструкции компилятору

Результат: исходный код, готовый к компиляции.

# Посмотреть выход препроцессора
g++ -E main.cpp > preprocessed.i

Этап 2: Компилятор — получение объектного кода

Компилятор преобразует исходный код в объектный код (machine code):

# Получить объектный файл
g++ -c main.cpp -o main.o

Объектный файл (.o, .obj) содержит:

  • Машинный код — инструкции процессора (x86-64, ARM и т.д.)
  • Таблица символов — имена функций, глобальных переменных
  • Таблица релокаций — адреса, которые нужно исправить при компоновке
  • Отладочная информация — если скомпилировано с -g
  • Секции данных — инициализированные и неинициализированные данные
// Пример кода
int global_var = 42;      // Инициализированные данные (.data)
int uninit_var;            // Неинициализированные данные (.bss)

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

int main() {
  int result = add(5, 3);
  return 0;
}

Объектный файл содержит:

.text     (машинный код функций add, main)
.data     (global_var = 42)
.bss      (uninit_var)
.symtab   (таблица символов)
.reloc    (таблица релокаций для вызовов функций)

Этап 3: Компоновщик (Linker) — финальный исполняемый файл

Компоновщик объединяет:

  • Объектные файлы от нашего проекта
  • Библиотеки (статические .a/.lib или динамические .so/.dll)
  • Runtime функции (libc, libstdc++ и т.д.)
# Пример: g++ компилирует и компонует в одну команду
g++ main.cpp helper.cpp -o program

# Эквивалентно:
g++ -c main.cpp -o main.o
g++ -c helper.cpp -o helper.o
ld main.o helper.o -o program -lc -lm  # linker

Компоновщик делает:

  1. Разрешение символов — найти определения функций и переменных
  2. Релокация — исправить адреса вызовов и доступа к данным
  3. Объединение секций — слить все .text, .data, .bss в одно
  4. Создание таблицы символов (если нужна динамическая линковка)
// Если есть undefined reference
void undefined_function();  // Объявлено, но не определено

int main() {
  undefined_function();  // Ошибка компоновки!
}

// LD ERROR: undefined reference to `undefined_function'

Финальный результат

На Linux

# Исполняемый файл ELF (Executable and Linkable Format)
file ./program
# program: ELF 64-bit LSB shared object, x86-64, dynamically linked

# Структура бинарника
objdump -d ./program
# Показывает disassembly — машинный код в читаемом виде

# Размер секций
size ./program
# text     data     bss      dec      hex
# 1234     512      256      2002     7D2

На Windows

.exe файл (PE формат — Portable Executable)
- .text (код)
- .data (инициализированные данные)
- .rsrc (ресурсы — иконки, строки)
- .reloc (таблица релокаций для при загрузке)

Статическая vs динамическая компоновка

Статическая компоновка (-static)

g++ main.cpp -static -o program_static

Результат: большой файл (~5-10 MB), содержит все библиотеки внутри. Не нужны DLL при запуске.

ЭКСЕ = основной код + libc + libstdc++ + всё остальное

Динамическая компоновка (по умолчанию)

g++ main.cpp -o program_dynamic

Результат: небольшой файл (~100 KB), использует системные .so библиотеки при запуске.

ЭКСЕ = основной код
   При запуске ОС загружает: libc.so, libstdc++.so из системы
ldd ./program_dynamic
# libc.so.6 => /lib64/libc.so.6
# libstdc++.so.6 => /usr/lib64/libstdc++.so.6

Оптимизация при компиляции

# Без оптимизаций (debug)
g++ -O0 -g main.cpp -o program  # Медленнее, больше информации

# Оптимизация для скорости
g++ -O2 main.cpp -o program     # Быстро, хороший баланс
g++ -O3 main.cpp -o program     # Максимально быстро

# Оптимизация для размера
g++ -Os main.cpp -o program     # Самый маленький файл

Результат: машинный код одной и той же программы может различаться в 10 раз по скорости и размеру!

Практический пример: что находится в исполняемом файле

# Как выглядит бинарник изнутри
readelf -l ./program        # ELF segments (как ОС загружает)
readelf -S ./program        # Sections (внутренняя структура)
objdump -d ./program        # Машинный код в читаемом формате
strings ./program           # Строковые константы
nm ./program                # Таблица символов

# Пример вывода nm:
# 0000000000001000 T main              (функция main)
# 0000000000001050 T add               (функция add)
# 0000000000003000 d global_var        (глобальная переменная)

Конкретные примеры бинарника на x86-64

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

Машинный код (assembly):

0000000000001000 <add>:
    1000: 55                      push   %rbp
    1001: 48 89 e5                mov    %rsp,%rbp
    1004: 89 7d fc                mov    %edi,-0x4(%rbp)
    1007: 89 75 f8                mov    %esi,-0x8(%rbp)
    100a: 8b 45 fc                mov    -0x4(%rbp),%eax
    100d: 03 45 f8                add    -0x8(%rbp),%eax
    1010: 5d                      pop    %rbp
    1011: c3                      retq

Этот машинный код хранится в исполняемом файле и выполняется процессором.

Итоговая схема

В итоге компиляции получается:

  1. Исполняемый файл (бинарник) содержит:

    • Машинный код (инструкции CPU)
    • Данные программы
    • Таблица символов для динамической компоновки
    • Заголовок ELF/PE с информацией о загрузке
    • Отладочная информация (если компилировано с -g)
  2. Размер и формат зависят от:

    • Уровня оптимизации (-O0, -O2, -O3)
    • Типа компоновки (статическая, динамическая)
    • Платформы (x86-64, ARM, MIPS)
    • Наличия отладочной информации
  3. При запуске:

    • ОС загружает бинарник в память
    • Если динамическая компоновка — загружает зависимости .so
    • Процессор начинает выполнять машинный код из entry point (обычно main)

Это базовое понимание критично для debug, оптимизации и понимания того, как код реально работает на iron.

Что получается в итоге компиляции? | PrepBro