Итак, друзья, 1-е апреля прошло, пора раскрывать карты, что же такое "2B or not 2B" на самом деле. Это совместный текст от автора работы jin_x и уже знакомого вам деда unbeliever
Обязательно скачайте архив с работой на Pouet и прочитайте вводную (первоапрельскую) статью, а так же комментарии к ней. Посмотрите первое видео с практической демонстрацией того, как работает код из «двух байт» на x86. И вот уже потом попробуйте осилить весь текст ниже.
Да, 2B or not 2B это действительно среда для запуска различных sizecoding-работ, очень простая и, пожалуй, самая маленькая из существующих. При этом, она имеет свои требования и ограничения.
Если кто-то ещё не уловил, тул 2b.com запускается из-под DOS (DOSBox, FreeDOS, MS-DOS) и осуществляет прыжок в область командной строки (по смещению $82* сегмента PSP), запуская на исполнение код, который передан в командной строке в двоичном виде. Собственно говоря, код этот вполне может иметь вид строки, которую можно набрать на клавиатуре (т.е. состоять из ASCII-символов с кодами от 33 до 126), но об этом немного позже.
* шестнадцатеричные числа мы будем записывать в паскалевской нотации $XX, это удобно, и fasm позволяет это делать.
Что важно знать?
В-нулевых, в качестве компилятора мы рекомендуем использовать fasm, весь наш инструментарий написан именно под него.
Во-первых, основной код может иметь максимальный размер 125 байт (таковы ограничения длины командной строки) и будет стартовать как обычная COM-программа, только со смещения $82, а не $100, как обычно. Сразу после основного кода будет автоматически добавлен символ возврата каретки (CR) с кодом 13 ($0D), а по адресу $100 будет находиться команда jmp short $82
($EB, $80).
Во-вторых, поскольку запуск предполагается из BAT-файла (ну или из интерпретатора командной строки), код не должен содержать некоторые символы. Прежде всего это символы перенаправления ввода-вывода ("<", ">" и "|"), а также символ подстановки параметров и переменных окружения ("%"). В некоторых системах (в том числе Windows, поддерживающих запуск DOS-программ из-под V86) специальное значение имеют ещё и символы "&", "^". Спец-символы с кодами до 32 поддерживаются не всеми DOS, а некоторые не поддерживаются никакими или почти никакими (особенно скудный набор имеет DOSBox), поэтому все эти символы исключаем тоже.
В-третьих, стартовые значения всех регистров и флагов, такие же, как и при запуске COM-программы. В подавляющем большинстве DOS на старте будет: ax=bx=0 (почти всегда это так), cx=$FF, dx=cs=ds=es=ss, si=$100, di=sp=$FFFE (при достаточном количестве оперативной памяти), bp=$9XX (младший байт везде разный, но его старшая тетрада, то есть полубайт, обычно = 1), флаги cf=df=0. Использовать это или нет – дело ваше.
Больше всего здесь смущает пункт «во-вторых», не так ли?
Предположим, нам нужно написать:
mov ah,0
int $16
cmp al,27
je x
А тут аж сразу 5 запретных символов: 0 в mov ah,0
, $16 в int $16
, $3C (символ "<") и 27 ($1B) в cmp al,27
и некоторое число с кодом < 32 в je x
, если x
находится где-то неподалёку дальше по коду.
Что же делать? То, что можно заменить другими командами, заменяем:
- вместо
mov ah,0
пишемxor ah,ah
или дажеcbw
(когда это возможно); - вместо
cmp al,27
пишемnot al
+sub al,not 27
илиxor al,not 27
+inc al
, а ещё лучше (т.к. здесь нам нужно дождаться нажатия клавиши и сравнить полученный код с кодом клавиши ESC) –dec ah
.
С int $16
сложнее, но если подумать, то конструкцию xor ah,ah
+ int $16
можно заменить, например, на mov ah,$83
+ ror ah,1
+ int $21
.
Остаётся je $+10
. Тут есть как минимум 2 пути: либо сделать прыжок назад (на достаточное расстояние), а оттуда – вперёд. Либо произвести замену байта в коде. Например, здесь можно написать z: je ($*2+3)-x
, а где-то выше: not byte [si-($100-(z+1))]
.
В итоге получим:
not byte [si-($100-(z+1))] ; замена 2-го байта (смещения прыжка) je при si=$100
mov ah,$83
rol ah,1 ; ah=7
int $21 ; ожидаем нажатия клавиши, код будет в al
not al
sub al,not 27 ; cmp al,27
z: je ($*2+3)-x ; прыжок на метку x (после изменения второго байта)
Альтернативные решения
Конечно, в финальной intro на 100+ байт запретных символов может оказаться довольно много (например, 15-20 и даже больше), и каждый раз производить подобные манипуляции – занятие довольно муторное, к тому же, они нередко приводят к увеличению длины кода.
Поэтому можно прибегнуть к шифрованию. Либо всего кода, либо отдельных мест. В примере 2b_life.asm мы шифруем весь код, добавляя к каждому байту значение $AC. После первого шифрования у нас осталось порядка 4-х запретных символов, которые мы смогли проработать заменой на другие команды. Конечно, выбор метода шифрования (add, sub, xor, not и пр.), а также ключа тоже требует времени, но это меньшее из всех зол. Код дешифровщика занимает всего 8 байт – это вполне приемлемо в данной ситуации. Шифрование же происходит автоматически с помощью директив repeat
, load
и store
(т.е. мы получаем уже зашифрованный код).
Отдельные места шифруются в примере 2b_note.asm. Здесь, опять же, с помощью repeat
, load
и store
к некоторым байтам добавляется значение $3D, а список адресов этих байтов хранится отдельно (по 1 байту адреса на каждый такой байт). Итого мы шифруем 20 байт + 13 байт занимает дешифровщик. Да, первый способ был более экономичным :)
В начале статьи мы обещали рассказать про код, который может иметь вид строки, состоящий из ASCII-символов с кодами от 33 до 126 (чтобы его можно было, например, без особых трудностей набрать на клавиатуре). Такое возможно, например, если зашифровать код с помощью 16-ричных символов или подобным образом. Да, это расточительно, но если шифровать методом BASE64, расход может оказаться ещё больше, ведь и декодер должен состоять только из таких символов.
Инструменты
Для удобства написания кода под "2B or not 2B" было создано 4 файла:
- 2b.draft.asm – черновик для создания 128-байтовых интр в BAT-файле, в начало которого добавляются символы запуска программы 2b (однако тут шифрование, подмену или ещё какие-то трюки вам придётся выполнять самим, либо выдергивать из примеров). Читайте комменты, там всё довольно подробно описано.
- 2b.draft44.asm – черновик для создания 44-байтовых интр, код которых будет полностью состоять из ASCII-символов с кодами от 33 до 126. Здесь каждый байт основного кода шифруется двумя псевдо-шестнадцатеричными символами: сначала младшая тетрада + «A» (диапазон букв «A»...«P»), затем старшая тетрада + «K» (диапазон букв от «K» до «Z»). Размер дешифровщика – 37 байт (+ 2 байта на команды
pusha
+popa
, если вы хотите сохранить стартовые значения регистров). Итого на код остаётся: (125 — 37) / 2 = 44 байта (или 43, если добавитьpusha
+popa
). Зато о шифровании можно вообще не париться :). Примеры, основанные на этом черновике – 2b_snow.asm и 2b_hello.asm - 2b.check.inc – include-файл для проверки кода. В случае наличия запретных символов, либо при превышении допустимого размера, при компиляции будет выведено сообщение (с указанием смещения в сегменте кода и смещения от начала BAT-файла).
- 2b.debug.inc – include-файл для создания отладочных файлов (самостоятельно запускаемого COM-файла и BIN-файла с чистым кодом).
Ну и к чему это всё?
Набор существующих платформ, под которые пишутся интры, уже долгие годы практически не меняется, если не брать во внимание категорию Wild (АОН, паяльники, ватные палочки). Мы предлагаем вам… не то чтобы новую платформу, но по крайней мере, некоторое разнообразие, со своими ограничениями. Именно ограничения и их преодоление являются сутью демосцены как процесса. Было бы прикольно на ближайшем демопати узреть оком своим целый конкурс в рамках этой концепции, где разные авторы смогут попробовать свои силы и мастерство в «2B or not 2B compo» :)
---EOF---
И целого байта мало… (Часть #-1, пилот)
И целого байта мало… (Часть #0)
И целого байта мало… (Часть #1)
И целого байта мало… (Часть #2)
И целого байта мало… (Часть #3)
Развлекательный канал деда в Телеграм: teleg.run/bornded
Рядом с каналом есть чат. В нем можно попробовать поднять вопросы за демосцену, ассемблер, пиксель-арт, трекерную музыку и другие аспекты процессы. Вам могут ответить либо отправят в другие, более тематические чаты.
ТАК ПОБЕЖДАЛИ — ТАК ПОБЕДИМ!
Автор: Максим Мучкаев