Запуск и завершение программы¶
Жизненный цикл процесса: от execve до main¶
Когда пользователь запускает программу (или вызывает execve), происходит длинная цепочка событий ещё до того, как
выполнится первая строка main.
execve("./prog", ...)
│
▼
┌─────────────────────────────────────────────────────┐
│ Ядро (kernel) │
│ • новое виртуальное адресное пространство │
│ • mmap PT_LOAD сегментов (.text, .data, ...) │
│ • читает .interp → загружает ld.so │
└──────────────────────────┬──────────────────────────┘
│ передаёт управление ld.so
▼
┌─────────────────────────────────────────────────────┐
│ Динамический линковщик (ld-linux-x86-64.so.2) │
│ • грузит DT_NEEDED библиотеки (рекурсивно) │
│ • выполняет релокации, заполняет GOT │
│ • вызывает .init_array каждой загруженной .so │
└──────────────────────────┬──────────────────────────┘
│ jmp _start
▼
┌─────────────────────────────────────────────────────┐
│ _start (из crt1.o) │
│ • выравнивание стека, извлечение argc/argv/envp │
│ • call __libc_start_main(main, argc, argv, ...) │
└──────────────────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ __libc_start_main (из libc.so) │
│ • регистрирует atexit-обработчики │
│ • вызывает .init_array бинаря │
│ (конструкторы глобальных C++ объектов) │
│ • call main(argc, argv, envp) │
└──────────────────────────┬──────────────────────────┘
│
▼
main() {…}
│
│ return / exit()
▼
┌─────────────────────────────────────────────────────┐
│ Завершение │
│ • atexit-функции (обратный порядок регистрации) │
│ • .fini_array бинаря │
│ (деструкторы глобальных C++ объектов) │
│ • .fini_array загруженных .so │
│ • syscall exit_group(код) │
└─────────────────────────────────────────────────────┘
1. Ядро¶
Ядро читает ELF-заголовок исполняемого файла и выполняет следующее:
- создаёт новый процесс и его виртуальное адресное пространство;
- отображает (mmap) сегменты с флагом
PT_LOAD—.text,.data,.bss, стек; - если ELF является динамическим (есть секция
.interp), ядро загружает в память динамический линковщик (обычно/lib64/ld-linux-x86-64.so.2) и передаёт управление ему, а не программе напрямую.
2. Динамический линковщик¶
Динамический линковщик (ld.so) выполняет:
- загружает все зависимые
.so(перечислены вDT_NEEDEDсекции.dynamic); - для каждой загруженной библиотеки рекурсивно загружает её зависимости;
- выполняет релокации — заполняет таблицу GOT корректными адресами;
- вызывает конструкторы библиотек (функции из их
.init_array); - передаёт управление в точку входа программы — функцию
_start.
3. _start и __libc_start_main¶
Функция _start из crt1.o — это реальная точка входа, записанная в ELF-заголовке. Она не является обычной C-функцией:
вызывается напрямую без стека вызовов.
_start выполняет:
- выравнивание стека и извлечение
argc,argv,envpиз начального состояния стека; - вызов
__libc_start_main(main, argc, argv, ...).
__libc_start_main из libc:
- регистрирует функции завершения через
atexit; - вызывает конструкторы глобальных и статических объектов (функции из
.init_arrayсамой программы); - вызывает
main(argc, argv, envp).
4. Возврат из main и завершение¶
После того как main завершается, управление возвращается в __libc_start_main, который:
- вызывает функции, зарегистрированные через
atexit(в обратном порядке регистрации); - вызывает деструкторы глобальных и статических объектов (функции из
.fini_array); - вызывает деструкторы библиотек;
- завершает процесс через системный вызов
exit_group.
Конструкторы и деструкторы глобальных объектов¶
Секции .init_array и .fini_array содержат массивы указателей на функции без аргументов. Компилятор автоматически
помещает туда конструкторы и деструкторы глобальных/статических объектов C++.
Порядок вызова конструкторов:
- конструкторы из
.so-зависимостей (в порядке загрузки); - конструкторы из
.init_arrayсамого бинаря.
Деструкторы вызываются в обратном порядке.
Также можно явно пометить функцию как конструктор или деструктор с атрибутами GCC:
__attribute__((constructor)) void before_main(void) {
// вызывается до main
}
__attribute__((destructor)) void after_main(void) {
// вызывается после main
}
Пользовательская точка входа¶
По умолчанию точка входа — _start из crt1.o. Можно указать другую функцию через опцию линковщика -e:
Реализация пользовательской точки входа:
#include <unistd.h>
extern "C" void my_start() {
// при желании можно явно вызвать main
int code = main(0, nullptr);
_exit(code); // ВАЖНО: не возвращаться через return
}
Почему нельзя делать return из _start¶
Функция _start вызывается ядром напрямую, без обычного стека вызовов C-функции. Перед _start на стеке нет адреса
возврата (или стоит мусор). Если пользовательская точка входа делает return:
- процессор берёт «адрес возврата» с неподготовленного стека;
- переходит по произвольному адресу;
- возникает
SIGSEGV.
Правильные способы завершить процесс из _start:
_exit(0); // немедленный системный вызов exit, без вызова atexit/деструкторов
exit(0); // вызывает atexit и деструкторы, затем exit_group
Завершение через exit vs _exit¶
| Функция | Что делает |
|---|---|
return из main |
Эквивалентно exit(код) |
exit(код) |
Вызывает atexit-функции, деструкторы глобальных объектов, fclose для всех FILE*, затем exit_group |
_exit(код) |
Немедленный системный вызов exit_group; atexit, деструкторы и буферы stdio не вызываются |
quick_exit(код) |
Вызывает только функции, зарегистрированные через at_quick_exit; деструкторы C++ не вызываются |
_exit используется после fork, если дочерний процесс не выполняет exec: вызов exit из дочернего процесса может
дважды сбросить буферы stdio и вызвать деструкторы глобальных объектов, что приводит к повреждению данных.
Связанные темы¶
- Формат ELF — секции
.init_arrayи.fini_arrayв ELF-файле - Линковка и библиотеки: основы — роли CRT-объектов (crt1.o, crti.o, crtn.o)
- Процессы: основы — создание процесса и
execve - fork и exec — почему после fork в дочернем процессе нужен
_exit - Системные вызовы: введение — как
_exitработает на уровне syscall
Источники¶
man 3 atexit— регистрация функций завершенияman 3 exit— полное завершение процессаman 2 exit_group— системный вызов завершенияman 2 execve— системный вызов запуска программыman ld.so— динамический линковщик- How programs get run: Linux — подробное описание загрузки ELF ядром
- stackoverflow: crtbegin, crtend, crt1, crti, crtn