Обновить

Как программа попадает в память: от execve до main

Уровень сложностиСредний
Время на прочтение9 мин
Охват и читатели11K
Всего голосов 17: ↑17 и ↓0+20
Комментарии17

Комментарии 17

Можно упомянуть статические бинарники, когда ядро после загрузки сегментов просто передаёт управление на точку входа.

Обязательно добавлю про особенность загрузки статических бинарников. Их загрузка не такая интересная, но для полноты картины стоит их упомянуть. Спасибо за дополнение!

Годная статейка и годный курс - значит карме автора плюс! :-)))) Всегда было интересно, как же там мой любимый кормилец пэхапэ под капотом ворочается. Частично прояснили, спасибо … Напишите про многопоточность на уровне системы ещё.

Большое спасибо)

Думаю тему многопоточности лучше раскрывать в отдельной статье, а то эта и так перегружена.

Оболочка (bash) создала свою копию и вызвала системный вызов execve(), который заменяет текущий процесс новой программой

Тут неточность: этот exec делает не Bash, а сама /usr/bin/strace.

Да, вы правы. Мы же сейчас рассматриваем запуск программы «под strace». Вот так будет корректнее?

Когда вы запускаете strace ./empty_sleep, утилита strace создаёт свою копию с помощью fork(), а затем эта копия вызывает системный вызов execve() для запуска вашей программы. Если бы вы запускали программу напрямую (./empty_sleep), тогда execve() вызывала бы копия оболочки bash.

Все-таки strace это не штатный запуск. Обычно программы запускаются оболочкой. Возможно, лучше сформулировать как-то так: “Оболочка (bash) создает свою копию (системный вызов fork. Кстати, почему он не упомянут?), после чего вызывает execve(), который перезаписывает ее данные данными запускаемой утилиты. В нашем случае, когда мы запускаем empty_sleep под отладкой, роль оболочки берет на себя strace.”

Согласен с вами. Для полноты картины обязательно нужно упомянуть вызов fork. С него все начинается.

На тему “чего добавить”, можно добавить реализацию всего рассмотренного вручную, своим кодом. То есть парсинг эльфа, выделение памяти, подгрузка библиотек и т.д. Правда, не уверен, что это удастся сделать простыми средствами.

Ох, ну это очень сложная и глубокая тема. По сути, необходимо реализовать часть ядра (даже мини-ОС) и свой динамический компоновщик.

Боюсь этот материал не уберется даже в рамки одной статьи.

Но идея классная: максимальное погружение в тему загрузки программы в память для выполнения, через практическую реализацию этого механизма.

Такое глубокое понимание точно может пригодиться реверс-инженерам и разработчикам прикладного ПО (в меньшей степени).

Спасибо за идею, добавлю в заметки. Возможно, вернусь к этому позднее, когда сам буду более готовым.

В общем, готов подискутировать на эту тему, если неправильно вас понял)

Ну, мне, как любителю контроллеров, такое развитие вашей темы кажется вполне логичным. Вот у нас есть статическая прошивка (запущенная с максимальными правами). Как бы разрешить юзеру выполнять свой код. И ведь даже компилятор изначально генерирует не hex, а обычный elf. Собственно, тут это, насколько я понимаю, решается вполне прозрачно. Но вот получится ли подобное воспроизвести на х86 из юзерспейса в существующей ОС не уверен.

У меня совсем скромный опыт работы с контроллерами. Делал с нуля электронику для управляемой машинки через Wi-fi на базе ESP-07. Только чтобы получить базовые навыки)

Можете написать мне в ВК, обсудим) а то здесь не по теме уже получается)

необходимо реализовать часть ядра 

Зачем? Практически вся логика живёт в dl-linux; в ядре есть только минимальный парсинг эльфа, достаточный для запуска статических бинарников, но эта функциональность всё равно продублирована в dl-linux.

И да, разработчикам средств разработки (компиляторы с тулчейнами, профилировщики, бинарные оптимизаторы и т.д.) эти знания точно будут полезны.

Вы правы, создателям инструментов разработки эти знания пригодятся больше, чем разработчикам ПО.

Согласен с тем, что большая часть логики живёт в ld-linux. Но все же сначало работает ядро (парсит структуру, загружает программу и компоновщик).

Исходное предложение было написать свой код, который вручную парсит ELF, выделяет память, загружает библиотеки. И вот здесь без реализации «части ядра» действительно не обойтись, потому что:

  • именно ядро первым читает ELF, находит .interp и загружает ld-linux;

  • именно ядро создаёт адресное пространство процесса и выполняет начальные mmap для сегментов;

  • ld-linux работает уже внутри этого пространства, доводя загрузку до конца.

Так что если мы хотим написать свой «ручной» загрузчик в пользовательском пространстве (в существующей ОС), то нам всё равно не обойтись без системных вызовов ядра (mmap, mprotect и т.д.). А часть логики ядра (создание адресного пространства, загрузка интерпретатора) мы просто не можем взять на себя – это привилегия ядра.

Поэтому, соглашусь с вами, нам достаточно реализовать только компоновщик (ядро мы все равно не обойдем) в случае x86. Но если все же речь про голое железо контроллеров, то без реализации части ядра (небольшой ОС) уже не обойтись.

Или я вас неправильно понял?

создателям инструментов разработки эти знания пригодятся больше, чем разработчикам ПО

Средства разработки - такое же ПО )

именно ядро первым читает ELF, находит .interp и загружает ld-linux;

именно ядро создаёт адресное пространство процесса и выполняет начальные mmap для сегментов;

ld-linux делает всё то же самое для каждой нужной библиотеки (разве что сам себя не грузит - но в самописном загрузчике секцию .interp можно просто игнорировать, поскольку работу интерпретатора будем делать сами).

то нам всё равно не обойтись без системных вызовов ядра (mmapmprotect и т.д.).

Естественно, без вызовов ядра не обойдётся никакая прикладная программа, делающая что-то полезное. Но эти функции - просто базовый функционал работы с фалйами и памятью, а не часть логики загрузчика. Самое интересное там в разрешении зависимостей.

Но если все же речь про голое железо контроллеров

Я имел в виду независимую реализацию на обычной операционке.

Понял вас, спасибо за уточнения)

В любом случае, добавил в заметки, тема интересная.

Большое спасибо всем за обратную связь. Через месяц, когда закроется возможность комментирования, внесу сразу все уточнения и исправлю неточности, которые выявляются через наше обсуждение.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации