Comments 46
Круто! Как я сам не додумался до этого! Все гениальное просто!
Очень подробно написано. Не мой профиль, но даже я все поняла. Спасибо автору
Не хватает последнего шага - зарегистрировать новый тип исполняемых файлов через binfmt_misc.
Нафига все эти пляски с ручным вводом байтов кода в консоль? NASM умеет формировать bin-файлы (безо всяких заголовков), которые можно использовать напрямую, как, к примеру, COM-файлы MS-DOS.
Если уж очень хочется, то можно и заголовок прямо в NASM'е вставить инструкцией DB или DWORD, но надо понимать, что может поехать абсолютная адресация. Так что файл в этом случае в память надо загружать полностью с заголовком, чтобы адреса не сдвинулись.
А еще binfmt_misc может исполнимые файлы не только по сигнатуре регистрировать, но и по расширению (в DOS/Windows стиле), так что можно прям те же COM-файлы без заголовка в Linux возродить.
Понимаю, но, в рамках этой статьи, мне хотелось сделать все максимально просто и доступно. А усложнять и углубляться можно долго:)
Спасибо за комментарий!
Ну так тут как раз проще и получается: NASM генерирует конечный исполнимый файл, а приложение должно уметь только загружать его и исполнять.
Мне просто кажется, что это чуть больше будет похоже на черный ящик. Хотелось чуть больше "ручками" повзаимодействовать и меньше использовать готовые утилиты, или, по крайней мере, не сильно погружаясь в их особенности и функционал.
Но я согласен абсолютно — писать байты в консоль это жесть как муторно, зато наглядно! :D
А что дает эта наглядность? Код x86/x86-64 практически нечитаемый, там все на битах, длина команды от одного байта до 15. Это не PDP-11, который сходу в восьмеричной системе читается-пишется и все по 16-битным словам исключительно. К примеру, 1SSDD - команда MOV, где SS - источник, DD - приемник, первый разряд тип адресации, второй разряд номер регистра от 0 до 7, все единообразно. Следующие одно или два слова могут быть либо константными адресами либо константными данными (зависит от типа адресации). Если операция с байтом, а не словом, то устанавливается старший бит слова команды в единичку: 11SSDD. Вот здесь можно детали посмотреть. Вот где наглядность! А переписывать руками байты из HEX-редактора в консоль - это не наглядность.
Тоже об этом подумал, но с другой стороны, когда сам ручками вводишь байты, лучше чувствуешь что происходит
Это как учиться водить на механике, а не на автомате, для понимания основ - бесценно
Шестнадцатеричные цифры вводить - только ошибки лепить. Если уж хочется прям вводить, запустите DEBUG досовский под DOSBox, там как раз можно руками команды ассемблера вводить (адреса и константы таки придется шестнадцатеричными вбивать, а адреса еще и вычислять).
Есть более современные способы быстро получить байтики для инструкций, хотя бы llvm-mc.
Не совсем понял, при чем здесь это. Понятно, что любой компилятор будет работать быстрее и надежнее, чем ручной ввод. Тут люди хотят "наглядности" и "чувствовать".
Лучше всё таки привыкать к современным инструментам - тот же llvm-mc периодически использую для всяких мелочей. Дальше уже эти байтики запихиваем в программе в массивчик, даём права и исполняем.
Так в чем отличие от использования того же NASM для компиляции ассемблерного кода? Им и пользоваться проще, и с точки зрения самого ассемблера и компиляцию одной строкой вызвать.
NASM удобнее если действительно надо скомпилировать и получить готовый объектник. Если нужен hex для одной или нескольких инструкций, чтобы посмотреть глазами или вставить как константу в сишный код, придётся ещё какой нибудь objdump вызывать. В этом плане llvm-mc проще - дали инструкцию, получили hex.
Ну, тоже не сахар, в командной строке кучу обрамления писать. Плюс эта штука довольно нестандартный вид команд ассемблера требует, типа addl %eax, %ebx
Для быстрого ассемблирования есть онлайн инструменты типа этого.
довольно нестандартный вид команд ассемблера
Обычный AT&T синтаксис, objdump тоже его выдаёт. Но в целом да, кому что удобнее.
Если интересно как сделать теперь следующий шаг и добавить кроссплатформенности для SEF-файлов, могу подсказать - заодно выйдет еще одна понятная и интересная статья
Вот это была бы "пушка"! Представляем себе исполняемый файл для любой ОС!...Я балдею.
Чтобы "для любой ОС", надо ещё системные вызовы абстрагировать, это отдельная задача.
Всем, кому интересна эта тема предлагается связаться со мной в телеграмме @RigidusRigidus - сделаем группу и обсудим как мы можем совместить эти подходы (включая и https://justine.lol/ape.html)
Туда и двигаемся, можно сказать)
Конечно интересно, жду)
Вообще, этот SEF формат — быстрая импровизация на тему исполняемых файлов, в которой нет ничего особенно полезного. Цель была (и будет) в плавном введении в эту тематику, чтобы ребята, которые достаточно далеки от этого, смогли по-тихоньку собирать контекст из тучи непонятных слов:D
А сам формат мы постепенно будем развивать как минимум до введения экспортов, импортов и релокаций. Ещё и научимся PE переводить в SEF. Напишем более правильный и сложный загрузчик. Без исполнения массива никуда не денемся и будем использовать похожий сценарий для формирования функций-заглушек при отсутствующем импорте.
Короче, ещё много работы предстоит, поэтому я с удовольствием вбираю новые мысли и идеи!
Прочитав заголовок, подумал, что увижу информацию о том, как написать для линукса программу на Си/Си++, но внезапно увидел, что здесь ассемблер! Вау! Я такого ещё нигде не видел, спасибо огромное! Опять же, интересный кейс про регистрацию формата исполняемого файла, обязательно буду показывать своим студентам!
Теперь мы добавили выделение памяти с помощью mmap с модификаторами доступа RWX (Read, Write, Execute), после в эту память мы скопировали наш байт-код
mprotect(2) вроде может поменять RWX для существующей страницы, без дополнительного выделения и копирования. Про выравнивание и размер страниц только надо не забывать.
Если я не ошибаюсь, то mprotect не подойдет для нашего сценария, так как массив попадет в .data секцию и будет лишь её частью. А mprotect работает именно с целыми страницами, поэтому, как вы и сказали, потребуется выравнивание и учет размера страницы. Это приведет к изменению прав для гораздо большего диапазона адресов нежели нам нужно, что, я считаю, слишком излишним и более вредным, хоть это и требует меньше ресурсов.
Ну mmap ведь тоже отнюдь не 6 байт выделит, а ту же выровененную ~4кБ страницу целиком.
Единственная разница что mprotect также разрешит исполнение тому что уже рядом с program_data[]
(вроде вообще на стэке в данном конкретном примере, что делает это ещё более "безопасным") в пределах страницы оказалось.
Но при загрузке исполняемого кода с диска руками под него всё равно надо выделять память, так что да, нет смысла выделять её как попало и потом менять доступ на исполнение.
вот тут https://habr.com/ru/articles/746658/ тоже самое через mprotect только потому что память под сгенерированный исполняемый код выделялась снаружи и лезть туда менять аллокатор на свой было лень.
Согласен с вами, бесспорно использовать mprotect в данном контексте было бы неплохо (хоть и пришлось бы углубиться в управление оперативной памятью), но тут важно будет сказать, что с помощью mmap мы добиваемся отделения исполняемой области, что, на мой взгляд, более правильно.
Ну и если добавить контекст, то далее в статье идёт речь про отображение файла с помощью mmap, поэтому мне принципиально было задействовать этот системный вызов ранее, чтобы не добавлять новой информации и, тем самым, упростить статью:)
Перед исполнением динамически зачитанного/сгенерированного кода непохо ещё кеш инструкций сбросить (скажем, через __builtin___clear_cache). На x86 необязательно, а вот ARM может начать исполнять мусор.
Это круто!
Хорошая ламповая статья
Трюк с mmap и исполнением массива это прям обряд посвящения в системщину, cразу начинаешь понимать что код и данные по сути одно и то же. Спасибо что напомнили
< segmentation fault (core dumped)
Примечание: ELF файлу можно добавить exec-флаг для стэка и тогда такая программа должна заработать со стэка.
Варианты как это можно сделать:
во время линковки с помощью флага
-Wl,-zexecstack
(gcc);пропатчить готовый бинарник:
execstack --set-execstack main.out
Пример как посмотреть модификаторы доступа для стэка у ELF файла:
readelf --program-headers 3_exec_stack/test | grep -A1 GNU_STACK
# GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
# 0x0000000000000000 0x0000000000000000 RWE 0x10
# ↑↑↑
Если скомпилировать изначальную программу командой nasm -f bin test.asm (или просто nasm test.asm) то на выходе будет 66 B8 4A 00 00 00 C3
Откуда лишние 0x66 взялись?
Пишем и запускаем свой исполняемый файл на Linux