Making our own executable packer
Все последние посты были про книги - этот будет про практику.
Невероятный гайд Making our own executable packer от Amos Wenger. Про то, как написать свой загрузчик и упаковщих ELF-файлов на Rust. По сути, наивная версия UPX. Идеальный повод погрузиться во внутренности Linux на Rust.
Сайт частенько был недоступен, поэтому я пользовался https://web.archive.org
По ходу гайда затрагивается много интересного из мира Linux:
mmapфайлов на Rust- сборка бинарей:
- ассемблер + линковка:
nasm,ld - C без libc:
syscallна asm,gcc -nostartfiles -nodefaultlibs - Rust без libc: приседаем с
no_std,build.rsиbuild-std - база по Makefile
- ассемблер + линковка:
- утилиты:
objdump,readelf,strace,dd,nm,ugdb,gdb - gdb-скрипты на Python + Rust toolkit для анализа mmap текущего pid - такого я еще не видел
- устройство релокаций в ELF и
.o - разница между
fPICиnon-fPIC - краткая история libc
Будет много unsafe-кода: работа с указателями, загрузка сегментов, ручная инициализация окружения. За собой заметил, что unsafe-блоки работают как задумано: каждый unsafe заставляет остановиться и подумать, а точно ли всё правильно. В C/C++ указатели для меня - обычный инструмент, о котором не задумываешься при использовании.
Для продакшена такой unsafe вряд ли будет полезен (если не писать драйверы или bare metal). Но как учебный опыт, почему бы и нет.
Долгая дорога
Легкой прогулкой это точно не назвать. Сам гайд писался два года. У меня повторение заняло около полутора месяцев. Результат выложил в свой «обучательный» репозиторий на GitHub. Идея выкладывать появилась не сразу, поэтому первый коммит начинается сразу со stage3 гайда.
Обычно я люблю гайды на C++ или Python, чтобы самому продумать портирование на Rust, как было с Ray Tracer. Здесь формат «сразу на Rust» зашёл. Задач для самостоятельного разбора хватало.
Решаем проблемы
Гайд написан шесть лет назад. Крейты старые, libc старая. Я же запускал всё в 2025 году — WSL, Ubuntu 22/24, ядро 6.x, glibc 2.35–2.39.
Уже в первой части пришлось переписывать код: nom v5.1 сильно отличается от nom v8. Но это цветочки, ягодки появились на 14 части туториала.
Ода ИИ-ассистенту
У автора в финале stage14 запускались ls и nano. У меня был стабильный segfault. Сравнивая обычный запуск и запуск через мой загрузчик, я понял, что не инициализирован _rtld_global. Временно прописал адрес вручную в регистр через gdb — дошёл до исполнения setlocale, где всё снова упало.
Дальше быстро продвинуться не вышло, и я отдал задачу ИИ-ассистенту (Cursor). Скормил ему пару последних частей туториала, код проекта, логи gdb. Попросил запустить nano.
Ассистент работал на Ubuntu 24, где всплыли ещё два типа релокаций и новых тегов (я вел разработку на Ubuntu 22). Это он поправил быстро. Потом упёрся в ту же проблему с _rtld_global.
С помощью серии вызовов gdb, nm, objdump, readelf и модификации кода проекта он разобрался, как хаками можно инициализировать _rtld_global и обойти эту проблему. Ассистент не может вызывать gdb в интерактивном режиме, поэтому он забавно делал это офлайн:
gdb -q --batch -ex 'set pagination off' -ex 'set debuginfod enabled off' -ex 'b "elk::process::jmp"'
-ex 'run' -ex 'autosym' -ex 'p/x $r14' -ex 'set $r14 = &_rtld_global' -ex 'c' -ex 'p/x $rdi'
-ex 'x/s $rdi' -ex 'bt' --args ./target/debug/elk run /bin/ls -- --help
Проблема с setlocale оказалась сложнее. После пары часов экспериментов ассистент предложил замокать setlocale и __ctype_*_loc, чтобы вернуть валидные указатели на таблицы в памяти.
Ассистент проверял свой код на запуске nano, он падал. Я в какой-то момент, решил запустить nano --help, и segfault’a не было. Тоже самое произошло чуть позже с ls --help. А когда были замоканы нормально локали, то уже заработал и просто ls.
После 3 часов такой работы я ещё час потратил на вычищение мусора, чтобы прийти к минимальному набору моков и хаков. Итог - я доволен, ls работает, nano --help работает. Полноценный nano не работает, как у автора, но по заверению ИИ дальше нужен другой подход, за рамками этого гайда.
ИИ - крутой инструмент. Позволяет в трудных ситуациях прорваться и решить проблему быстрее. Да, он не идеально все сделает, но доработку напильником можно проделать самому, главное сделать PoC. В PoCах ассистенты хороши.
За ИИ стоит следить: проверять команды, иногда править его gdb-скрипты, читать логи. Если хочешь научиться, а не просто «чтобы работало», нужно следить за ходом его мыслей, обоснованием вызова команд и изменений кода. За собой заметил, что после двух часов работы в таком режиме, я пустил дело на самотёк - перестал внимательно следить за действиями ассистента, просто нажимал далее. Т.е. когнитивная нагрузка высокая, и не получится 8 часов так работать. Нужно давать себе перерыв.
Главный вывод: ключевой навык программиста - декомпозиция. При хорошей декомпозиции и изолированности задач ИИ-ассистент дает плоды. Да, в больших кодовых базах могут быть проблемы из-за ограничения контекста. Но и у человека контекст не безграничен, и он также будет плавать.
Итог
Результат гайда опубликовал на GitHub. Самые интересные места:
Bonus
В ноябре поучаствовал в тренировках Яндекса: Тренировки. Забег по алгоритмам (8.0).
В этот раз ребята немного поленились и не стали записывать новые лекции. На каждую домашку дали по две темы из прошлых тренировок с ссылками на эти уроки. Это нисколько не делает тренировки хуже, я ими доволен.
Решил 36 из 40 задач - мой лучший результат. Одной задачи не хватило до топ-300, которых зовут на собес по упрощённому треку. Может, в следующий раз я попаду туда 😂