Методы модификации машинного кода: «селекция» vs. «генная инженерия»
Этот пост — 5-в-1! Он затрагивает такие темы, как: генная инженерия, реверс-инжиниринг, ненормальное программирование, ностальгия по Dendy и эмуляция NES. Как же такие разные темы могли встретиться вместе? Добро пожаловать под кат!
Про мифы касательно ГМО здесь уже писали. В дополнение к этому я хотел бы поделиться отличной аналогией с разбором реальных примеров из области реверс-инжиниринга и модификации машинного кода, что будет понятно и близко именно программистам.
«Мутации» машинного кода
В качестве примера возьмём приставку NES (известную у нас как Dendy), в которой используется процессор 6502. Система команд у него очень проста — опкод представлен всегда одним байтом, и каждый из 256 хоть что-то, да делает. Никаких «защит» от дурака не предусмотрено, и почти любой случайный набор байт будет выполняться без сопротивления со стороны процессора. Таким образом, мы можем взять ROM какой-нибудь игры, исправить в нём случайные биты (будем называть это «мутациями») — и после запуска наблюдать забавные глюки в разных неожиданных местах, но при этом в целом игра скорее всего будет работоспособной. Похоже, что на YouTube имеется целый жанр подобного видео. Полученный таким образом машинный код наверняка не очень корректен, но в большинстве случаев процессор сможет его выполнить и что-то сделать.
Как оказалось, такую методику используют не только для веселья (а играть в знакомые игры с неожиданными глюками весьма забавно), но и для полученя вполне себе конкретных модификаций: делают большое количество «мутантов» и ищут тот, в котором проявился нужный эффект. Точь-в-точь как в современных методах селекции, когда зародыши организмов подвергаются воздействию мутагенов (что приводит к случайным изменениям в генетическом коде), а потом из того что смогло вырасти отбираются те, у которых есть нужный признак. Полученные таким образом организмы получают в довесок массу других нежелательных мутаций. Избавляются от них путем постепенного скрещивания c нормальным видом, добиваясь получения более-менее вменяемого организма с нужным признаком и минимумом других мутаций, которые оказались заметны. То же самое можно сделать и с машинным кодом.
«Генная инженерия» на машинном коде
В качестве прямой аналогии здесь напрашивается реверс-инжиниринг. Мы изучаем машинный (двоичный) код, разбираемся как он работает (целиком или только некоторые его части), после чего вносим именно те изменения, которые наверняка приведут нас к целевому эффекту.
У программистов существует огромное количество инструментов для анализа машинного кода (всевозможные дизассемблеры, отладчики и тому подобное). Не глядя на то, что сам по себе машинный код не предназначен для чтения человеком, он был придуман людьми и задокументирован, так что создание инструментов для преобразования его в более понятный вид (ассемблерный листинг) не было проблемой. Также практически любая исследуемая программа скорее всего тоже была написана человеком, поэтому то что делает её код обычно выглядит логичным и понятным.
Понятное дело, что генетикам гораздо сложнее. Нет никакой официальной документации, которая могла бы пролить свет на все возникающие вопросы. Генетический код был «написан» волей случая и отобран по принципу «смогло ли оно выжить, приспособиться и дать потомство», поэтому наверняка он далёк от логичного и оптимального, скорее даже любая придуманная нами обфускация программного кода покажется просто детским лепетом. Тем не менее это не значит, что генетический код невозможно изучать и пробовать его изменять.
Предыстория
Для меня Dendy всегда была чем-то большим, чем просто приставкой. Я не только играл в неё, но и значительное время провёл внутри неё с паяльником в руках для некоторых простых модификаций. По дороге куда-нибудь я часто размышлял о том, как же создаются эти игры и как это работает внутри. Наверняка, многие из вас тоже когда-то задавались подобными вопросами, такова уж натура будущих IT-шников.
Прошли годы. С некоторой периодичностью погружался в эму-тему, изучая всё новое на тематических сайтах, но я не решался окунуться в изучение ассемблера 6502 и архитектуры NES. Внутренний конфликт рационального и иррационального. Я долго убеждал себя, что мне не нужно тратить на это время, но… сорвался. Глядя на то, какие интересные вещи делают энтузиасты эму-сцены, я взялся за свою давнюю идею со светлой мыслью: «Я тоже смогу!».
Наверняка многие из вас помнят картриджи типа »9999-in-1», которые были примечательны красивым меню с романтическим сюжетом у моря, летающими чайками и приятной музыкой (Unchained Melody). Я дизассемблировал это меню и сделал на его основе трибьют-демку Unchained Nostalgia.
Основная идея — никакого списка игр, а слайды переключаются под музыку. В освободившемся небе я нарисовал осмысленные облака и звёзды; реализовал автоматическое переключение слайдов синхронно с музыкой и возможность ручного переключения; добавил автокоррекцию скорости воспроизведения и высоты звука для систем, отличных от Dendy (ведь это меню делалось именно для китайских фамиклонов, а они отличались от оригинальных NES, из-за чего оригинальное меню работает слишком быстро на NES NTSC, а на NES PAL получается слишком низкий звук); не устоял и перед соблазном добавления целого ряда любопытных пасхалок. То есть используемый подход ничем не ограничивает фантазию и позволяет в принципе вносить любые изменения за какое-то конечное время.
Однако, похожие идеи приходят в голову разным людям
Уже после релиза своей демки я обнаружил, что кто-то тоже пробовал реализовать эту же идею. Автор указал, что он использовал «corruptor», который обычно используется для создания испорченных версий игр с различными глюками.
Если бы мне раньше кто-то сказал, что подобный «случайный» метод реально используется для модификации машинного кода с нужными эффектами — я бы наверняка не поверил. Ведь для этого нужно уйма времени и удача. Хотя с другой стороны нет необходимости изучать и понимать машинный код. Поскольку у меня в руках уже был «живой» пример подобного хака, ничего не оставалось, кроме как исследовать его работу, чтобы понять, как оно вообще работает.
Слайды переключаются автоматически, примерно каждые 2 секунды (что слишком быстро). Ручного управления нет. При смене слайда экран почему-то мерцает дважды. Но оно работает! Но каким образом?
Следы хирургического вмешательства
В картридже программный код и тайлы для графики хранятся раздельно. Код — в PRG ROM, тайлы — в CHR ROM. Второй может быть легко изменён с использованием стандартных инструментов, по этой причине для Dendy существует огромное количество графических хаков известных игр, где код оставлен абсолютно без изменений.
Сравним CHR ROM оригинального меню и исследуемого хака:
На лицо следы прямого «хирургического» вмешательства. По какой-то причине были вырезаны тайлы цифр, точки и маленькой чайки. Попробуем вернуть оригинальный CHR ROM и посмотрим, что именно было скрыто таким образом.
Теперь стало немного понятнее. В программном коде был сломан ввод, из-за чего менюшка думает, что кнопка «вниз» всегда нажата. Также был сломан вывод названий игр. Видимо, сломать вывод номеров игр и маленькой чайки (это курсор, указывающий на выбранную игру) не получилось, поэтому их пришлось прятать вырезанием соответствующих тайлов в CHR ROM.
… и немного магии!
Копаем глубже. При помощи дизассемблера посмотрим, что именно делают исправленные в PRG ROM байты.
Код оригинала | Код хака |
---|---|
NMI: |
NMI: |
Это обработчик прерывания NMI, который вызывается при начале отрисовки очередного кадра. Как видно, был исправлен адрес вызываемой функции. Оригинальная функция занималась считыванием состояния всех кнопок и записыванием их значений в один байт в виде битовой маски. Новый вызов выполняет только последнюю инструкцию в этой функции, которая сохраняет результат из регистра Y в переменную с маской нажатых в данный момент кнопок. Таким образом, используется мусорное значение регистра Y, которое осталось после выполнения предыдущего кода. В большинстве случаев тут оказывается мусорное значение 0×07, которое в двоичном виде выглядит как 00000111 и соответствует нажатым одновременно кнопкам «Вниз», «Влево» и «Вправо» (основной код игнорирует «Влево» и «Вправо», обрабатывая кнопку «Вниз»). Однако, сразу после переключения слайда в Y остаётся мусорное значение 0xFF, что означает, что все кнопки были нажаты одновременно, из-за чего в итоге вызывается отладочный код, который в оригинальном меню (по Left+Start+B) выводит номер ревизии на экран, что сопровождается отключением PPU на один кадр. Это и вызывает двойное мерцание экрана.
Код оригинала | Код хака |
---|---|
LDA #0 |
LDA #0 |
Здесь сломан код вывода номера ревизии по Left+Start+B, поэтому хоть этот отладочный код и выполняется каждый раз из-за описанной выше проблемы, номера ревизии мы всё равно не видим на экране. Получилось это забавным образом: инструкция, что устанавливала в регистр A значение 0×22, была заменена на инструкцию, которая вычитает из регистра A число 0×22. Поскольку регистр A до этого всегда 0, получается число 0xDE. Оригинальный код устанавливал для PPU начало вывода по адресу 0×22AC в видеопамяти, что находится в пределах первой экранной страницы. Новый код, получается, устанавливает адрес 0xDEAC, что находится далеко за пределами доступной видеопамяти (максимальный разрешённый адрес $3FFF), и в результате надпись выводится «в никуда».
Код оригинала | Код хака |
---|---|
ASL A |
ASL A |
Здесь у нас был сломан код, который выводит названия игр. При записи указателя на выводимую строку младший байт 16-разрядного указателя записывается по адресу 0xFA вместо 0×0A, а старший байт пишется как и должен — по адресу 0×0B. Повезло, что 0xFA не использовалось, и это ничего не сломало. Далее программа читает строку, на которую указывает пара байт 0xFA и 0xFB (0xFB всегда 0). По счастливому стечению обстоятельств, все получающиеся таким образом указатели указывают на заполненную нулями память, поэтому строки с названием игр не выводятся.
Выводы
Вы смогли прочувствовать, как всё «лихо закручено» в модификациях, созданных случайным образом? Одна поломка подпирает другую поломку, и оно как-то в целом даёт результат, близкий к нужному. При этом появляются просто невероятные зависимости между случайными и на первый взгляд никак не связанными участками кода, и при малейших дальнейших модификациях (даже самих по себе корректных) всё может рухнуть. Полноценный реверс-инжиниринг гораздо надёжнее. То же самое можно сказать и о генной инженерии, в сравнении с традиционной селекцией.
Ссылки на файлы
- unchained_nostalgia.zip — демка Unchained Nostalgia, созданная методами реверс-инжиниринга.
- guyver_hack.zip — исследуемый хак, созданный «случайным» образом, и его исходный ROM.