PROJEKT: OVERFLOW RISC-V assembly board game
hack the planet
- ИГРАТЬ В ВЕБ-ВЕРСИЮ: ОДИН
- ИГРАТЬ В ВЕБ-ВЕРСИЮ: С ДРУГОМ
- ПЕЧАТЬ
- ПРАВИЛА
- ПОХОЖИЕ ПРОЕКТЫ
- СИМВОЛЫ
- БЛАГОДАРНОСТИ
- КОНТАКТЫ
- ПОМОЩНИК ДЛЯ ИГРЫ: ESP32 | МОБИЛЬНЫЙ
- РУКОВОДСТВО ПО СБОРКЕ
ИГРА
Я создал эту игру, чтобы научить свою дочь тому, как работают переполнения буфера. Моя любимая часть в вычислениях — возможность относиться к программам как к чему-то, с чем можно играть, экспериментировать, изменять и делать их такими, какими вы хотите. Когда ваша микроволновка получает обновление и начинает зависать — вы можете её взломать. Или когда прошивка контроллера клавиатуры работает плохо — вы можете её взломать (привет, Vortex Pok3r). Сейчас ей 12 лет, но её навыки в ассемблере становятся всё лучше и лучше — надеюсь, однажды она сможет взломать свою собственную клавиатуру 😃
Игра заключается в создании небольшого шеллкода в памяти путём копирования существующих инструкций, а затем использовании переполнения буфера для перехода в него, чтобы вы могли перезаписать адрес возврата оппонента и заставить его перейти к функции game_over(). Есть и другие механики, а также дополнительные слои стратегии (например, установка обработчика исключений или «монкипатчинг»).
Все игроки используют одну и ту же память и выполняют одну и ту же программу, при этом совместно используют один процессор с операционной системой, реализующей вытесняющую многозадачность. Каждый ход игрок выполняет 10 инструкций, после чего процесс прерывается операционной системой, и наступает ход другого игрока. Указатель стека (stack pointer) каждого игрока начинается с разного адреса. Виртуальная память отсутствует.
Как выглядит игра, когда мы играем с друзьями (доказательство того, что люди действительно играют в эту игру, и также того, что у меня есть друзья):

С моей дочерью и собакой:

Код компилируется следующей командой:
riscv64-unknown-elf-gcc -fomit-frame-pointer -fno-inline-small-functions \
-mno-shorten-memrefs -march=rv32g -mabi=ilp32 -ffreestanding -nostdlib \
-nostartfiles -T code/link.ld -O0 -fverbose-asm -g -o game game.cСкрипт линковщика link.ld содержит только смещения для конкретных функций, чтобы они хорошо помещались на игровом поле. Отсутствие оптимизации -O0 делает машинный код довольно многословным, но при этом понятным. Затем я использую:
riscv64-unknown-elf-objdump -S -l -fd gameПаршу вывод, исправляю инструкции ▲ и ✎, конвертирую смещения переходов из шестнадцатеричного формата в десятичный, немного очищаю ассемблерный код, сопоставляю его с исходным кодом и генерирую SVG, который затем конвертирую в PDF с помощью Inkscape.
ПЕЧАТЬ
- Игровое поле: левая часть [A3 или A4 PDF]
- Игровое поле: правая часть [A3 или A4 PDF]
- Шаблон памяти для игроков 1,3 (адреса 1988–2244) [A4 PDF]
- Шаблон памяти для игроков 2,4 (адреса 3588–3840) [A4 PDF]
Предпросмотр игрового поля:
Требования к оборудованию
- Принтер: предпочтительно A3, A4 тоже подойдёт, но будет немного мелко
- Фишки: 1 фишка для свободной инструкции nop, одна фишка для адреса ловушки (trap), и по 2 фишки на игрока — одна для счётчика команд (program counter), одна для указателя стека (stack pointer). Я использую конденсаторы с обрезанными ножками или колпачки от велосипедных ниппелей
- Карандаш и ластик
ПРАВИЛА ИГРЫ
✱ Настройка игрового поля:
- Оба игрока начинают со всеми регистрами, установленными в ноль, за исключением регистра адреса возврата (
ra), который начинается со значения 1000. - Указатель стека (
sp) Игрока 1 инициализируется по адресу 2244. - Указатель стека (
sp) Игрока 2 инициализируется по адресу 3844. - Фишка инструкции nop изначально не размещается на поле.
- Счётчики команд (
pc) обоих игроков начинаются с адреса 1000 — начала функцииmain. - Разместите фишку ловушки (trap) по адресу 1000.
- Все адреса памяти установлены в ноль, за исключением предварительно загруженной программы.
✱ Игровой процесс:
- Ходы за ход: Игроки обязаны выполнить 10 инструкций за свой ход.
- Выполнение инструкций: Игроки должны следовать за счётчиком команд и выполнить либо 10 инструкций, либо накопленное количество до 20. Все переходы (
jal,beqи т.д.) должны соблюдаться. - Приостановка выполнения: Игроки могут решить приостановить свой ход после выполнения хотя бы одной инструкции, сохранив оставшиеся инструкции для следующего хода. Максимальное накопленное количество — 20 инструкций.
- Монкипатч: После выполнения ровно одной инструкции в начале каждого хода игрок может переместить фишку инструкции
nopна любой адрес в любой функции, которая в данный момент не выполняется игроками. Когда счётчик команд достигнет этого адреса, инструкция будет действовать как «нет операции» (nop). Перемещение фишкиnopстоит игроку текущего и следующего хода, позволяя оппоненту выполнить до 20 инструкций в свой следующий ход. Механика монкипатча всё ещё несбалансирована — каждые несколько дней я немного меняю правило в попытке улучшить его, так что если у вас есть лучшая идея, дайте мне знать.
◉ Условия победы [СЛОЖНЫЙ РЕЖИМ]:
- Game Over: Игра заканчивается, когда один игрок успешно «взламывает» оппонента, заставляя его вызвать функцию
game_over(). - Ничья: Возможно, игра зайдёт в состояние, когда никто не может заставить другого игрока перейти в
game_over()— в этом случае игра заканчивается вничью.
⊙ Условия победы [ПРОСТОЙ РЕЖИМ]:
- Освобождение: Победителем становится первый игрок, вышедший из основного цикла (выполнивший ret в функции
main).
✱ Специальные символы:
- ✎: Этот символ позволяет игрокам выбрать любое 12-битное число (от 0 до 4095) в качестве непосредственного значения в инструкции
li. - ▲: Этот символ позволяет игрокам выбрать значение для инструкции загрузки в пределах ±128 байт от их указателя стека. Например, если ваш
spравен 2180, вы можете выбрать значение между 2052 и 2308.
✱ Правила и ограничения:
- Незаконные действия: Перезапись адреса памяти ниже 1192, невыровненное чтение или запись (чтение/запись по адресу, не кратному 4) или выполнение незаконной инструкции приведёт к краху вашей программы.
- Обработка исключений: Крах вызывает обработчик исключений, который переходит по адресу ловушки (изначально 1000, но его можно перезаписать из функции
set_trap()). После возникновения исключения счётчик команд устанавливается в указанное значение, и выполнение продолжается. - Читерство/Ошибки: Если игрок читерит или допускает ошибку и его ловят, состояние его программы, включая память и регистры, сбрасывается.
Расширенные правила для большего количества игроков:
- Регистр указателя стека (
sp) Игрока 3 устанавливается в 2116. - Регистр указателя стека (
sp) Игрока 4 устанавливается в 3716. - Символ ▲ теперь позволяет записывать только на -128 байт от вашего указателя стека.
- Игра будет довольно нестабильной с более чем 2 игроками и быстро «повредится». Достичь условия победы будет сложнее, но игровой процесс станет более весёлым и хаотичным.
Подсказки и стратегии:
Офензивные краши: Используйте краши как наступательную стратегию для нарушения игрового процесса оппонента.
Модификация обработчика ловушек: Измените обработчик ловушек так, чтобы он указывал на функцию
game_over— тогда первый игрок, получивший крах, проиграет. Внимание: фишка nop — чрезвычайно мощный инструмент, если ловушка установлена наgame_over, потому что вы можете поместить её поверх ret в функции, которую в данный момент выполняет оппонент, и он проиграет.Перезапись адреса возврата: Используйте функцию
bug(), чтобы перезаписать адрес возврата оппонента, переполнив индекс значением 400 или -400, что позволит получить доступ к стеку другого игрока. Например, чтобы перейти с адреса 3784 (buffer[0]с указателем стека 3780) на адрес 2184, требуется индекс -400, вычисляемый как (3784 - 2184) / 4 = 400.Создание шеллкода: Используйте функцию
copy(), чтобы скопировать конкретные инструкции и сформировать компактный шеллкод в своей памяти, позволяющий произвольные записи. Используйте переполнение буфера в функцииbug(), чтобы перейти в этот шеллкод. Пример простого шеллкода для произвольной записи:li a4, ✎(копировать с адреса 256, 268)li a5, ✎(доступно по адресам 136, 144, 1016 и т.д.)sw a4, 0(a5)(доступно по адресам 264, 272)ret(доступно по адресам 272, 200, 1060 и т.д.)
Копирование инструкции
retприведёт к бесконечному циклу, так как адрес возврата будет установлен на начало шеллкода при переходе в него, вызывая непрерывное выполнение.Переход в память: Чтобы перейти в свою память, используйте переполнение буфера в функции
bug(). Установив переменнуюindexв значение 6, вы можете записать переменнуюvalueповерх сохранённого адреса возврата в стеке (28(sp)). При возврате изbug()программа копирует значение из 28(sp) в регистр адреса возврата. Поместите адрес созданного вами шеллкода в переменнуюvalue.
Примечание: Все переходы относительны к текущему счётчику команд, даже если они отображаются как абсолютные адреса в дизассемблере. Например,
jal a4, 0(машинный код 1903) на самом деле является бесконечным циклом при выполнении.
Список всех допустимых инструкций игры с машинным кодом от 0 до 4095 (RV32 JRI, работающие с a0, a4, a5, sp, ra)
0x00000013 (19) addi zero, zero, 0
0x00000033 (51) add zero, zero, zero
0x00000067 (103) jalr zero, zero, 0
0x0000006f (111) jal zero, 0
0x00000093 (147) addi ra, zero, 0
0x000000b3 (179) add ra, zero, zero
0x000000e7 (231) jalr ra, zero, 0
0x000000ef (239) jal ra, 0
0x00000113 (275) addi sp, zero, 0
0x00000133 (307) add sp, zero, zero
0x00000167 (359) jalr sp, zero, 0
0x0000016f (367) jal sp, 0
0x00000513 (1299) addi a0, zero, 0
0x00000533 (1331) add a0, zero, zero
0x00000567 (1383) jalr a0, zero, 0
0x0000056f (1391) jal a0, 0
0x00000593 (1427) addi a1, zero, 0
0x000005b3 (1459) add a1, zero, zero
0x000005e7 (1511) jalr a1, zero, 0
0x000005ef (1519) jal a1, 0
0x00000613 (1555) addi a2, zero, 0
0x00000633 (1587) add a2, zero, zero
0x00000667 (1639) jalr a2, zero, 0
0x0000066f (1647) jal a2, 0
0x00000713 (1811) addi a4, zero, 0
0x00000733 (1843) add a4, zero, zero
0x00000767 (1895) jalr a4, zero, 0
0x0000076f (1903) jal a4, 0
0x00000793 (1939) addi a5, zero, 0
0x000007b3 (1971) add a5, zero, zero
0x000007e7 (2023) jalr a5, zero, 0
0x000007ef (2031) jal a5, 0Журнал изменений
0.0.6: заменено while(run) на while(*prun), чтобы оппонент мог намеренно вызвать краш, заставив выполнить невыровненную разыменовку; изменено правило NOP — теперь его можно размещать только в функциях, которые в данный момент не выполняются.
СИМВОЛЫ
Квадраты слева/справа содержат двоичное сообщение, закодированное в ASCII — попробуйте расшифровать его: белые квадраты — это 1, чёрные на чёрном — 0, используйте интервалы, чтобы понять, где находятся нули.
Цвета минимальны, чтобы люди могли печатать дёшево, и чтобы всё было чётко видно на чёрно-белых принтерах A3 и A4. Используются только цвета: красный (255,0,0), синий (0,0,255), чёрный (0,0,0) и белый (255,255,255).
Я не дизайнер, но текущий дизайн — это, возможно, 20-я итерация, и он появился после 5 часов прослушивания Making of a Cyborg из Ghost in the Shell.
Логотип проекта символизирует переполнение и сгенерирован случайным образом. Текущее случайное зерно вызвало у меня лёгкое беспокойство, поэтому я оставил его как есть.
Интервалы в логотипе и японском тексте немного смещены — это сделано для того, чтобы можно было сложить бумагу A3 пополам, не задевая текст.
Японский текст — это последние слова Короля Пиратов, Гол Д. Роджера: «Унаследованная воля, судьба эпохи и мечты людей. Пока люди продолжают стремиться к смыслу Свободы, эти вещи никогда не перестанут существовать!»
Без подсветки синтаксиса. Я не использую подсветку синтаксиса и не учу свою дочь её использовать. У меня нет сильных предпочтений, но подсветка заставляет думать, что определённые вещи в коде важнее других (в зависимости от темы). Без подсветки я могу сам решать, на чём сосредоточиться, и легко фокусироваться на том, что считаю важным.
БЛАГОДАРНОСТИ
Я хочу поблагодарить Питера, Уильяма, Шантану, Рареса и мою дочь, которые сыграли в 50 разных версий игры — все они были довольно плохими 😃, но они не сдались. Спасибо tn1, который очень помог мне с дизайном, особенно указывая на то, что не работает.
Также большая благодарность людям, работающим над шрифтом Terminus — это абсолютная красота.
Отдельное спасибо эмулятору tinyfive от OpenMachine, который я использовал для тестирования игры, пока не написал свой собственный для веб-версии.
Спасибо gamesounds.xyz за .wav-файлы для веб-версии, а также Карлу Кейси @ White Bat Audio за фоновую музыку.
И, конечно, огромная благодарность людям, работающим над RISC-V — с ним супер-весело работать.
Большое спасибо @matsuu за указание на опечатку в японском написании.
КОНТАКТЫ
Email: jackdoe@fastmail.com
Если у вас есть идея по поводу другого игрового поля или правил, или вы нашли ошибку в поле или онлайн-версии — пожалуйста, напишите мне. Также оказалось, что я могу играть в эту игру с дочерью и примерно двумя людьми из всего моего списка контактов 🥹, так что если вам понравилось играть — я буду рад услышать об этом.
РУКОВОДСТВО ПО АССЕМБЛЕРУ
Вы можете найти множество руководств по ассемблеру RISC-V, например:
И множество интерпретаторов, например:
И декодеры, например rvcodecjs от luplab — отличный декодер инструкций: введите туда 0x13 (инструкция nop) и посмотрите, как она декодируется.
Если у вас возникают трудности, вы также можете поискать на YouTube «risc v assembly tutorial» — там много действительно хороших видео. К сожалению, я не уверен, какое подойдёт именно вам, так что не унывайте и просто попробуйте несколько из них.
Я подготовил упражнения для своей дочери: каждый день я печатал несколько шаблонов памяти и несколько примеров кода ниже, и она просто выполняла инструкции. Иногда я менял код на лету или тратил больше времени на объяснение, например, приведения типов и структур. Но базовый ассемблер RISC-V она смогла понять довольно быстро.
Версия «Виселица» — это тот же обычный PDF, но некоторые инструкции ассемблера пропущены, и вам нужно их вписать:

Если вам нужно подтянуть знания по C, мы используем руководство Бижда: Beej's Guide to C Programming, особенно первые 7–8 глав. Но есть много других отличных ресурсов. Вы также можете использовать следующие примеры ассемблера, чтобы освежить знания по C — хороший пример: упражнение с указателями:
Когда вы печатаете упражнение и шаблон памяти, это выглядит так:

Список выполненных упражнений:
- l0-variables-0.pdf [A3 или A4 PDF] | ВЕРСИЯ «ВИСЕЛИЦА»
- l0-variables-1.pdf [A3 или A4 PDF] | ВЕРСИЯ «ВИСЕЛИЦА»
- l0-variables-2.pdf [A3 или A4 PDF] | ВЕРСИЯ «ВИСЕЛИЦА»
- l1-call-function-1.pdf [A3 или A4 PDF] | ВЕРСИЯ «ВИСЕЛИЦА»
- l1-call-function-2.pdf [A3 или A4 PDF] | ВЕРСИЯ «ВИСЕЛИЦА»
- l1-call-function-3.pdf [A3 или A4 PDF] | ВЕРСИЯ «ВИСЕЛИЦА»
- l1-call-function-4.pdf [A3 или A4 PDF] | ВЕРСИЯ «ВИСЕЛИЦА»
- l1-call-function-5.pdf [A3 или A4 PDF] | ВЕРСИЯ «ВИСЕЛИЦА»
- l1-call-function-6.pdf [A3 или A4 PDF] | ВЕРСИЯ «ВИСЕЛИЦА»
- l1-call-function-7.pdf [A3 или A4 PDF] | ВЕРСИЯ «ВИСЕЛИЦА»
- l1-call-function-8.pdf [A3 или A4 PDF] | ВЕРСИЯ «ВИСЕЛИЦА»
- l1-call-function-9.pdf [A3 или A4 PDF] | ВЕРСИЯ «ВИСЕЛИЦА»
- l1-call-function-10.pdf [A3 или A4 PDF] | ВЕРСИЯ «ВИСЕЛИЦА»
- l1-call-function-11.pdf [A3 или A4 PDF] | ВЕРСИЯ «ВИСЕЛИЦА»
- l1-call-function-12.pdf [A3 или A4 PDF] | ВЕРСИЯ «ВИСЕЛИЦА»
- l1-call-function-13.pdf [A3 или A4 PDF] | ВЕРСИЯ «ВИСЕЛИЦА»
- l1-call-function-14.pdf [A3 или A4 PDF] | ВЕРСИЯ «ВИСЕЛИЦА»
- l1-pointers-0.pdf [A3 или A4 PDF] | ВЕРСИЯ «ВИСЕЛИЦА»
- l1-pointers-1.pdf [A3 или A4 PDF] | ВЕРСИЯ «ВИСЕЛИЦА»
- l1-string-0.pdf [A3 или A4 PDF] | ВЕРСИЯ «ВИСЕЛИЦА»
- l1-string-1.pdf [A3 или A4 PDF] | ВЕРСИЯ «ВИСЕЛИЦА»
- l1-string-2.pdf [A3 или A4 PDF] | ВЕРСИЯ «ВИСЕЛИЦА»
- l1-struct-0.pdf [A3 или A4 PDF] | ВЕРСИЯ «ВИСЕЛИЦА»
- l2-array-1.pdf [A3 или A4 PDF] | ВЕРСИЯ «ВИСЕЛИЦА»
- l2-array-2.pdf [A3 или A4 PDF] | ВЕРСИЯ «ВИСЕЛИЦА»
- l2-array-3.pdf [A3 или A4 PDF] | ВЕРСИЯ «ВИСЕЛИЦА»
- l2-recursion-0.pdf [A3 или A4 PDF] | ВЕРСИЯ «ВИСЕЛИЦА»
- l2-recursion-1.pdf [A3 или A4 PDF] | ВЕРСИЯ «ВИСЕЛИЦА»
Все изображения и ссылки сохранены в оригинальном формате. Для корректного отображения некоторых элементов (например,
<details>) в вашем Markdown-просмотрщике может потребоваться поддержка HTML.

