В далеком 1998 году люди соревновались, кто напишет самую короткую игру для DOS. Со временем это хобби назвали «код-гольфингом». Я решил вернуться к такому кусочку кода (nibbles.asm) и превратить его в загрузочный образ дискеты, который бы умещался в твит, то есть в 140 символов.Вот как выглядит результат.
1 |
$ perl -E"say'swFoAKAHvqB9uBMAzRC/wPi5gAjzqqqBxz4Bc/jkYDxIchE8UHcNN5hISHIFLAJrwLCJwwHetIa2780VtEUmMCR72uv','B'x589,'Vao='"|base64 -D>boot.img |
В некоторых системах (например, Linux) ключ -D пишется в нижнем регистре ( base64 -d).
Это адаптированный перевод двух публикаций Алока Менхраджани: Bootloader + retro game in a tweet и Bootable CD + retro game in a tweet. Публикуется с разрешения автора. Перевел и адаптировал на русский язык Андрeй Письмeнный.
Образ загрузочной дискеты со «Змейкой»
Технически мой код — это не бутлоадер. Настоящий бутлоадер переводит машину с x86 в защищенный режим, загружает информацию с диска (BIOS загружает только первые 512 байт) и так далее. Я пропустил все это и вместо этого задаю некоторые регистры и прыгаю прямо в игру.
hugi.de
Один из популярных сайтов, где проходили эти соревнования, назывался Hugi, и игра Nibbles, которую Altair и ODDS entertainment уместили в 48 байт, была одним из моих любимых творений. Данная игра также широко известна как Tron и «Змейка».
РЕКОМЕНДУЕМ:
Лучшие игры для программистов
К моему большому сожалению, старый код оказалось не так-то просто заставить работать. Нужен DOS, и если работать с эмулятором, то приходится догадываться, какое железо могло тогда быть у разрабов и на какой частоте все это работало.
По-сему я решил взять код Altair и сделать из него загрузочную дискету. По ходу сделал несколько изменений, чтобы можно было нормально поиграть.
floppy.asm
Итак, давайте разберем код.
Для запуска кода его нужно сначала скомпилировать:
1 |
nasm floppy.asm -o floppy.img |
После чего запустить в QEMU, если у вас по каким-то причинам нет возможности записать на настоящую дискету:
1 2 |
qemu-system-i386 -fda floppy.img |
Код начинается с прагмы, которая сообщает процессору, что нужно перейти в шестнадцатиразрядный режим (в нем машина x86 изначально находится при загрузке с флоппи).
1 |
[bits 16] |
Еще одна прагма сообщает ассемблеру, где находится код, который необходимо загрузить.
1 |
[org 0x7C00] |
Задаем начальную позицию для нашей змейки.
1 |
mov bl, 1 |
Загружаем адрес VRAM в регистр ES.
1 2 |
push 0xa000 pop es |
Теперь задаем позицию червя в центре экрана и переключаем видеорежим. Режим 13h — это VGA (1 байт на пиксель, тогда как настоящий цвет хранится в палитре), общий размер 320 на 200. При рестарте сразу же очищаем экран.
1 2 3 4 5 |
restart_game: mov si, 320*100+160 mov ax, 0x0013 int 0x10 |
Рисуем границы. Предполагаем, что палитра по умолчанию нас устроит. Также предполагаем, что если мы начнем снизу и закрасим 2176 пикселей, то получатся границы снизу и сверху.
1 2 3 4 5 6 7 8 |
mov di, 320*199 mov cx, 2176 rep draw_loop: stosb stosb add di, 318 jnc draw_loop |
Обратите внимание на переход в середине инструкции rep stosb.
В основном цикле мы читаем ввод с клавиатуры на порте 0x60. Сюда же попадает ввод мыши, но нам необходимо обрабатывать только «вверх» (0x48), «влево» (0x4b), «вправо» (0x4d) и «вниз» (0x50).
1 2 3 4 5 6 |
game_loop: in al, 0x60 cmp al, 0x48 jb kb_handle_end cmp al, 0x50 ja kb_handle_end |
В конце регистр BX будет содержать сдвиг позиции (+1, -1, +320, -320) в зависимости от нажатой/отпущенной клавиши на клавиатуре. Я почти уверен, что тут можно срезать пару байтов, учитывая, что мы выше уже проверяли края.
1 2 3 4 5 6 7 8 9 10 11 12 |
aaa cbw dec ax dec ax jc kb_handle sub al, 2 imul ax, ax, byte -0x50 kb_handle: mov bx, ax kb_handle_end: add si, bx |
Изначально тут использовалась команда, которая задавала палитру (10h/0bh), чтобы подождать вертикальной перерисовки. Теперь компьютеры слишком быстры, так что мы вместо этого используем 15h 86h. Это заодно сэкономит нам пару байтов.
Обратите внимание: вам понадобится твикнуть cx+dx, чтобы запускать этот код в виртуальной машине, а не на реальном железе. Практика показывает, что виртуалки ждут в три-четыре раза дольше, чем реальное железо.
1 2 3 |
mov ah, 0x86 mov dh, 0xef int 0x15 |
Рисуем змейку и проверяем достижение коллизий (четное равенство означает коллизию).
1 2 |
mov ah, 0x45 xor [es:si], ah |
Возвращаемся в основной цикл.
1 |
jpo game_loop |
Мы столкнулись со стеной или с хвостом. Начинаем заново.
1 |
jmp restart_game |
Забиваем остаток сектора нулями. Значение 0xaa55 — это подпись в конце загрузчика.
1 2 |
TIMES 510 - ($ - $$) db 0 dw 0xaa55 |
Код в твите создает загрузочный файл boot.img. Можно загрузить его в QEMU или VirtualBox и играть в игру стрелочками. Или можно записать все это на дискету и загрузиться с нее по-настоящему.
- Соревнование на ассемблерную оптимизацию в рамках Hugi
- Исходники Altair
- Загрузчик с Hello World
- Вики OSKit
Образ загрузочного компакт-диска
С тех пор как я сделал загрузочный флоппи, в «Твиттере» расширили лимит длины вдвое, так что я решил сделать своими руками образ загрузочного компакт-диска. На нем будет работать улучшенная версия нашей «Змейки».
1 |
perl -E 'say"A"x46422,"BDRDAwMQFFTCBUT1JJVE8gU1BFQ0lGSUNBVElPTg","A"x54,"Ew","A"x2634,"/0NEMDAxAQ","A"x2721,"BAAAAYQ","A"x30,"SVVVqogAAAAAAAEAF","A"x2676,"LMBaACgB76gfbgTAM0Qv8D4uYAI86qqgcc+AXP45GA8SHIRPFB3DTeYSEhyBSwCa8CwicMB3rSGtu/NFbRFJjAke9rrwQ","A"x2638'|base64 -D>cd.iso |
Этот код создает образ загрузочного компакт-диска cd.iso. Как и образ дискеты, его можно отправлять в QEMU или VirtualBox, загружаться и играть.
Чтобы вручную сделать образ CD, сначала нужно разобраться со стандартом ISO 9660. К сожалению, доступ к стандартам ISO недешев, но, по счастью, этот существует в варианте ECMA 119, где и можно бесплатно позаимствовать все спецификации.
У ISO 9660 много всяких дополнений, например UDF, El Torito, RockRidge, Joliet и так далее. В случае с загрузочными образами нас интересует только El Torito. Однако его спецификация, на мой взгляд, одна из самых плохо написанных. В ней есть ошибки (смотри, например, последнюю строчку на рисунке 7), легко забыть, что все значения — шестнадцатеричные (нет префиксов 0x), картинки стоят в неочевидном порядке и так далее. Одно хорошо — документ короткий.
Чтобы создать загрузочный диск, нам понадобится сначала сделать 17 пустых секторов, за которыми будет идти набор дескрипторов тома (Volume Descriptor Set). Сектор составляет 2048 байт.
Обратите внимание: по спецификации ISO 9660 дескрипторы тома должны начинаться с сектора 16. А вот по El Torito загрузочная запись должна проживать в секторе 17. Технически это значит, что нужно поместить в шестнадцатом секторе пустой дескриптор тома в качестве заглушки, но все вроде бы работает и без этого.
Итак, пишем дескриптор тома.
1 2 3 4 5 6 7 8 9 10 |
0x00 // Тип (0 означает загрузочную запись) 'CD001' // Идентификатор 0x01 // Версия 'EL TORITO SPECIFICATION' // Идентификатор загрузочной системы 9 x 0x00 // Отступ 32 x 0x00 // Не используется 0x13 0x00 0x00 0x00 // Адрес каталога загрузки в абсолютных секторах 1973 x 0x00 // Не используется |
Следующий сектор — это терминатор набора дескрипторов тома (Volume Descriptor Set Terminator).
1 2 3 4 |
0xff // Тип (255 — терминатор) 'CD001' // Идентификатор 0x01 // Версия 2041 x 0x00 // Не используется |
За дескрипторами томов следует загрузочный каталог (Boot Catalog). El Torito поддерживает разные режимы эмуляции. Компакт-диск может эмулировать загрузочный флоппи, загрузочный жесткий диск и так далее. Я выбрал вариант без эмуляции — в этом случае подразумевается, что BIOS загрузит определенное число секторов и передаст управление нашему бутлоадеру.
Контрольная сумма вычисляется таким образом, чтобы все шестнадцатибитные значения в записи давали в сумме ноль (mod 65536).
Вот первая запись в загрузочном каталоге (Validation Entry).
1 2 3 4 5 6 7 |
0x01 // Идентификатор заголовка 0x00 // Идентификатор платформы (0 — Intel x86) 0x00 0x00 // Зарезервировано 'a' // Строка-идентификатор 23 x 0x00 // Отступ cksum cksum // Контрольная сумма (2 байта) 0x55 0xaa // Байты ключей |
Вторая запись (Default Entry):
1 2 3 4 5 6 7 8 |
x88 // Индикатор загрузчика (0x88 — загрузочный) 0x00 // Тип носителя (0 — без эмуляции) 0x00 0x00 // Загрузочный сегмент 0x00 // Тип системы 0x00 // Не используется 0x01 0x00 // Число секторов для считывания 0x14 0x00 0x00 0x00 // Адрес виртуального диска (в абсолютных секторах) 20 x 0x00 // Не используется |
Дальше идут нули до конца сектора.
1 |
1984 x 0x00 // Не используется |
Дальше идет бутлоадер игры, ниже — он целиком. Никаких отличий от флоппи-версии здесь нет.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
[bits 16] [org 0x7C00] mov bl, 1 push 0xa000 pop es restart_game: mov si, 320*100+160 mov ax, 0x0013 int 0x10 mov di, 320*199 mov cx, 2176 rep draw_loop: stosb stosb add di, 318 jnc draw_loop game_loop: in al, 0x60 cmp al, 0x48 jb kb_handle_end cmp al, 0x50 ja kb_handle_end aaa cbw dec ax dec ax jc kb_handle sub al, 2 imul ax, ax, byte -0x50 kb_handle: mov bx, ax kb_handle_end: add si, bx mov ah, 0x86 mov dh, 0xef int 0x15 mov ah, 0x45 xor [es:si], ah jpo game_loop jmp restart_game TIMES 2048 - ($ - $$) db 0 |
Дальше мне оставалось только написать скрипт для компиляции загрузчика, сборки образа и создания текста твита. Закончив со всем этим, я прожег результат на болванку и потестировал на реальном железе.