Что получается в итоге компиляции?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Что получается в итоге компиляции 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
Компоновщик делает:
- Разрешение символов — найти определения функций и переменных
- Релокация — исправить адреса вызовов и доступа к данным
- Объединение секций — слить все .text, .data, .bss в одно
- Создание таблицы символов (если нужна динамическая линковка)
// Если есть 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
Этот машинный код хранится в исполняемом файле и выполняется процессором.
Итоговая схема
В итоге компиляции получается:
-
Исполняемый файл (бинарник) содержит:
- Машинный код (инструкции CPU)
- Данные программы
- Таблица символов для динамической компоновки
- Заголовок ELF/PE с информацией о загрузке
- Отладочная информация (если компилировано с -g)
-
Размер и формат зависят от:
- Уровня оптимизации (-O0, -O2, -O3)
- Типа компоновки (статическая, динамическая)
- Платформы (x86-64, ARM, MIPS)
- Наличия отладочной информации
-
При запуске:
- ОС загружает бинарник в память
- Если динамическая компоновка — загружает зависимости .so
- Процессор начинает выполнять машинный код из entry point (обычно main)
Это базовое понимание критично для debug, оптимизации и понимания того, как код реально работает на iron.