
Комментарии 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.”
На тему “чего добавить”, можно добавить реализацию всего рассмотренного вручную, своим кодом. То есть парсинг эльфа, выделение памяти, подгрузка библиотек и т.д. Правда, не уверен, что это удастся сделать простыми средствами.
Ох, ну это очень сложная и глубокая тема. По сути, необходимо реализовать часть ядра (даже мини-ОС) и свой динамический компоновщик.
Боюсь этот материал не уберется даже в рамки одной статьи.
Но идея классная: максимальное погружение в тему загрузки программы в память для выполнения, через практическую реализацию этого механизма.
Такое глубокое понимание точно может пригодиться реверс-инженерам и разработчикам прикладного ПО (в меньшей степени).
Спасибо за идею, добавлю в заметки. Возможно, вернусь к этому позднее, когда сам буду более готовым.
В общем, готов подискутировать на эту тему, если неправильно вас понял)
Ну, мне, как любителю контроллеров, такое развитие вашей темы кажется вполне логичным. Вот у нас есть статическая прошивка (запущенная с максимальными правами). Как бы разрешить юзеру выполнять свой код. И ведь даже компилятор изначально генерирует не hex, а обычный elf. Собственно, тут это, насколько я понимаю, решается вполне прозрачно. Но вот получится ли подобное воспроизвести на х86 из юзерспейса в существующей ОС не уверен.
необходимо реализовать часть ядра
Зачем? Практически вся логика живёт в 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 можно просто игнорировать, поскольку работу интерпретатора будем делать сами).
то нам всё равно не обойтись без системных вызовов ядра (
mmap,mprotectи т.д.).
Естественно, без вызовов ядра не обойдётся никакая прикладная программа, делающая что-то полезное. Но эти функции - просто базовый функционал работы с фалйами и памятью, а не часть логики загрузчика. Самое интересное там в разрешении зависимостей.
Но если все же речь про голое железо контроллеров
Я имел в виду независимую реализацию на обычной операционке.
Большое спасибо всем за обратную связь. Через месяц, когда закроется возможность комментирования, внесу сразу все уточнения и исправлю неточности, которые выявляются через наше обсуждение.
Как программа попадает в память: от execve до main