Перейти к содержанию

Запуск и завершение программы

Жизненный цикл процесса: от 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++.

Порядок вызова конструкторов:

  1. конструкторы из .so-зависимостей (в порядке загрузки);
  2. конструкторы из .init_array самого бинаря.

Деструкторы вызываются в обратном порядке.

Также можно явно пометить функцию как конструктор или деструктор с атрибутами GCC:

__attribute__((constructor)) void before_main(void) {
    // вызывается до main
}

__attribute__((destructor)) void after_main(void) {
    // вызывается после main
}

Пользовательская точка входа

По умолчанию точка входа — _start из crt1.o. Можно указать другую функцию через опцию линковщика -e:

g++ main.cpp -Wl,-e,my_start -o prog

Реализация пользовательской точки входа:

#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 и вызвать деструкторы глобальных объектов, что приводит к повреждению данных.

Связанные темы

Источники

  • 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