Что происходит до main?
Рассмотрим простую программу:
#include <iostream>
#include <random>
int a;
int b;
int main() {
a = rand();
b = rand();
std::cout << (a + b);
}
Все очень просто. Объявляем две глобальные переменные, в main() присваиваем им значения и выводим их сумму на экран.
Скомпилировав эту программу, мы сможем посмотреть ее ассемблер и увидеть просто набор меток, соответствующих разным сущностям кода(переменным a и b, функции main). Но вы не увидите какого-то "скрипта". Типа как в питоне. Если питонячий код не оборачивать в функции, то мы точно будем знать, что выполнение будет идти сверху вниз. Так вот, такой простыни ассемблера вы не увидите. Код будет организован так, как будто бы им кто-то будет пользоваться.
И это действительно так! Убирая сложные детали, можем увидеть вот такое:
a:
.zero 4
b:
.zero 4
main:
push rbp
mov rbp, rsp
call rand
...
call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
mov eax, 0
pop rbp
ret
Суть программы состоит из меток. Метки нужны, чтобы обращаться к сущностям программы. Да, они и внутри основного кода используются. Но то, что на главной функции стоит метка, говорит нам о том, что ее кто-то вызывает!
Но даже до того, как начнет работу сущность, которая вызывает main, нужно проделать большую работу по подготовке программы к исполнению. Давайте просто перечислю, что должно быть сделано:
💥 Программа загружается в оперативную память.
💥 Аллокация памяти для стека. Для исполнения функций и хранения локальных переменных обязательно нужен стек.
💥 Аллокация памяти для кучи. Для программы нужна дополнительная память, которую она берет из кучи.
💥 Инициализация регистров. Там их большое множество. Например, нужно установить текущий указатель на вершину стека(stack pointer), указатель на инструкции(instruction pointer) и тд.
💥 Замапить виртуальное адресное пространство процесса. Процессы не работают с железной памятью напрямую. Они делают это через абстракцию, называемую виртуальная память.
💥 Положить на стек аргументы argc, argv(мб envp). Это аргументы для функции main.
💥 Загрузка динамических библиотек. Программа всегда линкуется с разными динамическими либами, даже если вы этого явно не делаете)
💥 Вызов всякий преинициализирующих функций.
Важная оговорка, что это все суперсильное упрощение. В реале все намного сложнее. Не претендую на полноту изложения и правильность порядка шагов. К тому же я говорю только про эквайромент полноценных ОС типа окон и пингвина. В эмбеде могут быть сильные отличия. Обязательно оставляйте свои дополнения процесса старта программы в комментариях.
В этих полноценных осях всю
эту грязную работу на себя берет загрузчик программ.
После того, как эти шаги выполнены, загрузчик может вызывать ту самую функцию
_start(название условное, зависит от реализации).
Она уже выполняет более прикладные чтоли вещи:
👉🏿 Статическая инициализация глобальных переменных. Это и недавно обсуждаемая
zero-инициализация и
константная инициализация(когда объект инициализирован константным выражением). То есть инициализируется все, что можно было узнать на этапе компиляции.
👉🏿 Динамическая инициализация глобальных объектов. Выполняется код конструкторов глобальных объектов.
👉🏿 Инициализация стандартного ввода-вывода. Об этом мы говорили
тут.
👉🏿 Инициализация еще бог знает чего. Начальное состояние рандомайзера, malloc'а и прочего. Так-то это часть первых шагов, но привожу отдельно, чтобы вы не думали, что только ваши глобальные переменные инициализируются.
И только вот после этого всего, когда состояние программы приведено в соответствие с ожиданиями стандарта С++, функция _start вызывает main.
Так что, чтобы вы смогли выполнить свою программу, кому-то нужно очень мощно поднапрячься...
See what's underneath. Stay cool.
#OS #compiler