Как прошить sega mega drive

Обновлено: 02.07.2024

Несмотря на мой большой опыт в реверсе игр под Sega Mega Drive , крякмисов под неё я никогда не решал, да и не попадались они мне на просторах интернета. Но, на днях появился забавный крэкми, который захотелось решить. Делюсь с вами решением.

Экспорт дистрибутива и установка в GHIDRA

Прежде чем экспортировать наш дистрибутив, давайте добавим какое-то описание для нашего проекта. Для этого в корне проекта в Eclipse находим файл extension.properties , и редактируем поля:

Для создания дистрибутива вашего плагина жмём GhidraDev -> Export -> Ghidra Module Extension. и следуем подсказкам мастера создания дистрибутива:




После всех манипуляций в папке dist вашего проекта получим zip-архив (что-то типа ghidra_9.0_PUBLIC_20190320_Sega.zip ) с готовым к употреблению плагином для GHIDRA .

Давайте теперь установим наш плагин. Запускаем Гидру , жмём File -> Install Extensions. , жмём значок с зелёным плюсом, и выбираем созданный ранее архив. Вуаля.



Пишем код

Дерево пустого проекта выглядит внушительно:


Все файлы с java -кодом лежат в ветке /src/main/java :


getName()

Для начала, давайте выберем имя для загрузчика. Его возвращает метод getName() :

findSupportedLoadSpecs()

Метод findSupportedLoadSpecs() решает (на основе данных, которые содержатся в бинарном файле), какой процессорный модуль должен быть использован для дизассемблирования (так же как и в IDA ). В терминологии GHIDRA это называется Compiler Language . В него входят: процессор, endianness, битность и компилятор (если известен).

Данный метод возвращает список поддерживаемых архитектур и языков. Если же данные не того формата, мы просто вернём пустой список.

Итак, в случае с Sega Mega Drive , по смещению 0x100 заголовка чаще всего присутствует слово " SEGA " (это не обязательное условие, но выполняется в 99% случаев). Нужно проверить, имеется ли эта строка в импортируемом файле. Для этого, на вход findSupportedLoadSpecs() подаётся ByteProvider provider , с помощью которого мы и будем работать с файлом.

Создаём объект BinaryReader , для удобства чтения данных из файла:

Аргумент false в данном случае указывает на использование Big Endian при чтении. Теперь давайте прочитаем строку. Для этого воспользуемся методом readAsciiString(offset, size) у объекта reader :

Если equals() вернёт true , значит мы имеем дело с сеговским ромом, и в список List<LoadSpec> loadSpecs = new ArrayList<>(); можно будет добавить мотороловский m68k . Для этого создаём новый объект типа LoadSpec , конструктор которого принимает на вход объект загрузчика (в нашем случае это this ), ImageBase , в который будет грузиться ROM, объект типа LanguageCompilerSpecPair и флаг — предпочтительный ли этот LoadSpec среди остальных в списке (да, в списке может быть не один LoadSpec ).

Формат конструктора у LanguageCompilerSpecPair следующий:

  1. Первый аргумент — languageID — строка вида "ProcessorName:Endianness:Bits:ExactCpu". В моём случае это должна быть строка "68000:BE:32:MC68020" (к сожалению, ровно MC68000 в поставке нет, но, это не такая уж и проблема). ExactCpu может быть и default
  2. Второй аргумент — compilerSpecID — найти то, что здесь необходимо указывать, можно в каталоге с процессорными описаниями Гидры ( $(GHIDRA)/Ghidra/Processors/68000/data/languages ) в файле 68000.opinion . Видим, что здесь указаны только default . Собственно, его и указываем

В итоге, имеем следующий код (как видим, пока ничего сложного):

Разница есть, и она прямо таки очень сильная. В GHIDRA можно писать один проект, который будет понимать разные архитектуры, разные форматы данных, быть загрузчиком, процессорным модулем, расширением функционала декомпилятора, и другими плюшками.

В IDA же это отдельный проект под каждый тип дополнения.

Насколько это удобнее? На мой взгляд, у GHIDRA — в разы!

В этом методе мы будем создавать сегменты, обрабатывать входные данные, создавать код и заранее известные метки. Для этого, нам в помощь на вход подаются следующие объекты:

  1. ByteProvider provider : его мы уже знаем. Работа с бинарными данными файла
  2. LoadSpec loadSpec : спецификация архитектуры, которая была выбрана на этапе импорта файла методом findSupportedLoadSpecs . Нужно, если мы, к примеру, умеем работать с несколькими форматами данных в одном модуле. Удобно
  3. List<Option> options : список опций (включая кастомные). С ними я пока не научился работать
  4. Program program : основной объект, который предоставляет доступ ко всему необходимому функционалу: листинг, адресное пространство, сегменты, метки, создание массивов и прочее
  5. MemoryConflictHandler handler и TaskMonitor monitor : напрямую с ними нам редко придётся работать (обычно, достаточно передавать эти объекты в уже готовые методы)
  6. MessageLog log : собственно, логгер

Итак, для начала создадим некоторые объекты, которые упростят нам работу с сущностями GHIDRA и имеющимися данными. Конечно, нам обязательно понадобится BinaryReader :

Далее. Нам очень пригодится и упростит практически всё объект класса FlatProgramAPI (далее вы увидите, что с его помощью можно делать):

Заголовок рома

Для начала определимся, что из себя представляет заголовок обычного сеговского рома. В первых 0x100 байтах идёт таблица из 64-х DWORD-указателей на вектора, например: Reset , Trap , DivideByZero , VBLANK и прочие.

Далее идёт структура с именем рома, регионами, адресами начала и конца блоков ROM и RAM , чексумма (поле проверяется по желанию разработчиков, а не приставкой) и другая информация.

Давайте создадим java -классы для работы с этими структурами, а также для реализации типов данных, которые будут добавлены в список структур.

VectorsTable

Создаём новый класс VectorsTable , и, внимание, указываем, что он реализует интерфейс StructConverter . В этом классе мы будем хранить адреса векторов (for future use) и их имена.


Объявляем список имён векторов и их количество:

Создаём отдельный класс для хранения адреса и имени вектора:

Список векторов будем хранить в массиве vectors :

Констуктор для VectorsTable у нас будет принимать:

  1. FlatProgramAPI fpa для преобразования long адресов в тип данных Address Гидры (по сути, этот тип данных дополняет простое числовое значение адреса привязкой его к ещё одной фишке — адресному пространству)
  2. BinaryReader reader — чтение двордов

У объекта fpa есть метод toAddr() , а у reader есть setPointerIndex() и readNextUnsignedInt() . В принципе, больше ничего не требуется. Получаем код:

Метод toDataType() , который нам требуется переопределить для реализации структуры, должен вернуть объект Structure , в котором должны быть объявлены имена полей структуры, их размеры, и комментарии к каждому полю (можно использовать null ):

Ну, и, давайте реализуем методы для получения каждого из векторов, либо всего списка целиком (куча шаблонного кода):

GameHeader

Поступим аналогичным образом, и создадим класс GameHeader , реализующий интерфейс StructConverter .

Start Offset End Offset Description
$100 $10F Console name (usually 'SEGA MEGA DRIVE ' or 'SEGA GENESIS ')
$110 $11F Release date (usually '©XXXX YYYY.MMM' where XXXX is the company code, YYYY is the year and MMM — month)
$120 $14F Domestic name
$150 $17F International name
$180 $18D Version ('XX YYYYYYYYYYYY' where XX is the game type and YY the game code)
$18E $18F Checksum
$190 $19F I/O support
$1A0 $1A3 ROM start
$1A4 $1A7 ROM end
$1A8 $1AB RAM start (usually $00FF0000)
$1AC $1AF RAM end (usually $00FFFFFF)
$1B0 $1B2 'RA' and $F8 enables SRAM
$1B3 ---- unused ($20)
$1B4 $1B7 SRAM start (default $00200000)
$1B8 $1BB SRAM end (default $0020FFFF)
$1BC $1FF Notes (unused)

Заводим поля, проверяем на достаточную длину входных данных, пользуемся двумя новыми для нас методами readNextByteArray() , readNextUnsignedShort() объекта reader для чтения данных, и создаём структуру. Итоговый код получается следующим:

Создаём объекты для заголовка:

Сегменты

У Сеги есть вполне известная карта регионов памяти, которую я, пожалуй, тут приводить не буду в виде таблицы, а приведу лишь код, который используется для создания сегментов.

Итак, у объекта класса FlatProgramAPI есть метод createMemoryBlock() , с помощью которого удобно создавать регионы памяти. На вход он принимает следующие аргументы:

  1. name : имя региона
  2. address : адрес начала региона
  3. stream : объект типа InputStream , который будет являться основой для данных в регионе памяти. Если указать null , то будет создан неинициализированный регион (например, для 68K RAM или Z80 RAM нам именно такой и будет нужен
  4. size : размер создаваемого региона
  5. isOverlay : принимает true или false , и указывает, что регион памяти оверлейный. Где это нужно кроме исполняемых файлов я не знаю

На выходе createMemoryBlock() возвращает объект типа MemoryBlock , которому дополнительно можно установить флаги прав доступа ( Read , Write , Execute ).

В итоге, получится функция следующего вида:

Сегмент, содержащий игровой ром может иметь максимальный размер 0x3FFFFF (всё остальное уже будет принадлежать другим регионам). Создадим его:

Здесь мы создали InputStream на основе входного файла, начиная со смещения 0.

Некоторые сегменты я бы не хотел создавать, не спросив у пользователя (это SegaCD и Sega32X сегменты). Для этого можно воспользоваться статическими методами класса OptionDialog . Например, showYesNoDialogWithNoAsDefaultButton() покажет диалоговое окно с кнопками YES и NO с активированной по-умолчанию кнопкой NO .

Создаём указанные выше сегменты:

Теперь можно создать все остальные сегменты:

Массивы, метки и конкретные адреса

Для создания массивов имеется специальный класс CreateArrayCmd . Создаём объект класса, указывая в конструкторе следующие поля:

  1. address : адрес, по которому будет создан массив
  2. numElements : количество элементов массива
  3. dataType : тип данных у элементов в массиве
  4. elementSize : размер одного элемента

Далее достаточно вызвать у объекта класса метод applyTo(program) , чтобы создать массив.

Для некоторых адресов мне требуется создать не массив, а конкретный тип данных, например BYTE , WORD , DWORD или структура. Для этого, у объекта класса FlatProgramAPI есть методы createByte() , createWord() , createDword() и т.д.

Так же, кроме указания типа данных, необходимо дать имя каждому конкретному адресу (например, это могут быть порты VDP ). Для этого, используется следующая хитрая конструкция:

  1. У объекта типа Program вызываем метод getSymbolTable() , который предоставляет нам доступ к таблице символов, меток и т.д.
  2. У таблицы символов дёргаем метод createLabel() , который принимает на вход адрес, имя и тип символа. С типом символов не очень понятно, но, в имеющихся примерах используется SourceType.IMPORTED и я поступил так же

В итоге получаем парочку шаблонных методов для создания именованных массивов, либо одиночных данных:

Применение структур заголовка

Для применения структур на конкретные адреса я воспользуюсь статическим методом createData() класса DataUtilities . Данный метод принимает на вход следующие аргументы:

  1. program : объект класса Program
  2. address : адрес, на который будет применена структура
  3. dataType : тип структуры
  4. dataLength : размер структуры. Можно указать -1 для автоматического подсчёта
  5. stackPointers : если true , происходит какая-то магия с подсчётом глубины указателей. Ставлю false
  6. clearDataMode : если вдруг на месте создания структуры уже есть объявленные данные, выбираем метод их андефайна (простите, не смог придумать русское слово)

Остался ещё один момент: т.к. мы применяем структуру с векторами (читай, адресами функций), то было бы логично по этим адресам объявить функции. Для этого у объекта типа FlatProgramAPI можно вызвать метод createFunction() , принимающий на вход адрес и имя функции.

Теперь у нас всё есть для создания структур заголовка и обозначения данных по адресам векторов как функций:

Завершаем метод load()

Для красивого уведомления пользователя о ходе работы метода load() можно воспользоваться методом setMessage() объекта типа TaskMonitor , который у нас уже есть.

Собираем воедино получившийся набор функций, и получаем такой вот код:

getDefaultOptions и validateOptions

В данной статье я их не рассматриваю, потому как пока не пригодились

Выводы и решение

Приходим к следующим результатам:

Экспериментальным путём выясняем клавишу, которую нужно нажать, чтобы проверить введённый ключ: B . Пробуем:

Решение

Запуск любой игры на Сегу начинается с выполнения вектора Reset . Указатель на него можно найти во втором DWORD-е от начала рома.



Видим парочку неопознанных функций начиная с адреса 0x27A . Давайте взглянем что там.

sub_2EA()


По своему опыту скажу, что так обычно выглядит функция ожидания завершения VBLANK -прерывания. Посмотрим, где ещё есть обращения к переменной byte_FF0026 :


Видим, что нулевой бит как раз устанавливается в прерывании VBLANK . Значит переменную назовём vblank_ready , а функцию, где она проверяется — wait_for_vblank .

sub_60E()

Далее по коду вызывается функция sub_60E . Посмотрим, что там:


То, что записывается первой командой в адрес VDP_CTRL — это команда управления VDP . Чтобы узнать, что же она делает, становимся на эту команду, и нажимаем клавишу J :


Видим, что инициализируется запись в CRAM (место, где хранятся палитры). Значит, весь последующий код функции просто задаёт какую-то начальную палитру. Соответственно, функцию можно назвать init_cram .

sub_71A()


Видим, что снова передаётся какая-то команда в VDP_CTRL , значит снова жмём J и узнаём, что это команда инициализирует запись в видеопамять:


Далее разбираться, что же там передаётся в видео-память, смысла нет. Поэтому просто обзываем функцию load_vdp_data .

sub_C60()

Здесь происходит практически то же самое, что и в предыдущей функции, поэтому, не вдаваясь в подробности, просто назовём функцию load_vdp_data2 .

sub_8DA()

Тут уже кода побольше. И, к тому же, в этой функции вызывается ещё одна. Заглянем сразу туда — в sub_D08 .


Видим, что в регистре D0 приходит команда для VDP_CTRL , в D1 — значение, которым будет заполняться VRAM , а в D2 и D3 — ширина и высота заполнения (т.к. получается два цикла: внутренний и внешний). Обзываем функцию fill_vram_by_addr .

sub_8DA()

Возвращаемся в предыдущую функцию. Раз значение в регистре D0 передаётся как команда для VDP_CTRL , нажмём на значении клавишу J . Получим:


Опять же, из опыта реверса игр на Сегу могу сказать, что эта команда инициализирует запись маппинга тайлов. Адреса, которые начинаются на $Fxxx , $Exxx , $Dxxx , $Cxxx в 90% случаев будут адресами регионов с этими самыми маппингами. Что такое маппинги:
это такие значения, которыми можно указывать, куда выводить тот или иной тайл на экране (тайл — это квадрат из пикселей размером 8x8 ).

Значит функцию можно назвать как init_tile_mappings .

sub_CDC()


Первая же команда инициализирует запись по адресу $F000 . Одно замечание: среди адресов "маппингов", есть ещё регион, где хранится таблица спрайтов (это их позиции, тайлы, на которые они указывают и т.д.) Узнать, какой регион за что отвечает можно будет под отладкой. Но пока нам это не нужно, поэтому назовём функцию просто init_other_mappings .

Что теперь?

Давайте пока проверим, что переменная, которую мы назвали jmp_addr действительно хранит введённый ключ. Воспользуемся теми же Memory Watch :


Как видим, догадка была верна. Что нам это даёт? Мы можем прыгать на любой адрес. Только на какой? В списке функций все разобраны ведь:


Тогда я начал просто прокручивать код, пока не обнаружил вот такое:


Намётанный глаз увидел в конце неразмеченных байт последовательность $4E, $75 . Это опкод инструкции rts , т.е. возврата из функции. Значит эти неразмеченные байтики могут быть кодом какой-то функции. Попробуем их обозначить как код, жмём C :


Очевидно, это код функции. Можно также нажать на нём P , чтобы код стал функцией. Запомним это имя: sub_D3C .

Тут возникает мысль: а что если прыгнуть на sub_D3C ? Звучит неплохо, правда одного прыжка сюда явно будет недостаточно, т.к. на переменную word_FF0020 ссылок больше не нашлось.

Тогда меня посетила ещё одна мысль: а что если поискать другой такой неразмеченный код? Открываем диалог Binary search (Alt+B), вводим в нём последовательность 4E 75 , ставим галку Find all occurrences :


Жмём ОК , чтобы начать поиск, получаем следующие результаты.


Как минимум ещё два места в роме могут содержать код функции, нужно их проверить. Кликаем по первому из вариантов, прокручиваем чуть вверх, и снова видим последовательность неопределённых байт. Обозначим их как функция? Да! Жмём P там, где начинаются байты:


Круто! Теперь у нас есть функция sub_34C . Пробуем повторить то же самое ещё и с последним из найденных вариантов, и… получаем облом. Там такое большое количество байт перед 4E 75 , что не понятно, где начинается функция. И, явно, не всех из этих байт выше являются кодом, т.к. очень много повторяющихся байт.

Определяем начало функции

Нам будет проще всего найти начало функции, если мы найдём, где заканчиваются именно данные. Как это сделать? На самом деле совершенно не сложно:

  1. Крутим до начала данных (там будет ссылка на них из кода)
  2. Переходим по ссылке и ищем цикл, в котором должен будет фигурировать размер этих самых данных
  3. Размечаем массив

Итак, выполняем первый пункт.


… и сразу видим, что в цикле из нашего массива копируется по 4 байта данных за раз (потому что move.l ) в VDP_DATA . Рядом видим число 2047 . Может сначала показаться, что итоговый размер массива 2047 * 4 , но цикл на основе dbf выполняется на +1 итерацию больше, т.к. последнее сравниваемое значение не 0 , а -1 .

Итого: размер массива равен 2048 * 4 = 8192 . Обозначим байты как массив. Для этого жмём * и указываем размер:


Крутим в конец массива, и видим там байты, которые являются именно байтами кода:



Теперь у нас появилась функция sub_2D86 , и у нас есть всё, чтобы решить этот крекми! Посмотрим, что делает новоиспечённая функция.

sub_2D86()

sub_34C()


Видим, что здесь вычитывается значение переменной word_FF0020 . Если посмотреть ссылки на неё, то увидим ещё одно место, где как раз происходит запись в эту переменную, и это будет как раз то место, куда я хотел прыгать через переменную jmp_addr . Это подтверждает догадку, что прыгать на sub_D3C точно нужно.

А вот происходящее далее мне стало лень понимать, поэтому я закинул ром в GHIDRA, нашёл эту функцию, и посмотрел декомпилированный код:

Видим, что используется переменная со странным именем in_D1w , а ещё переменная DAT_00ff0020 , которая адресом своим напоминает упомянутую ранее word_FF0020 .

Для этого в окне с декомпилированным кодом жмём правой кнопкой мыши на имени функции, и выбираем пункт меню Edit Function Signature :


Для того, чтобы указать, что функция принимает аргумент через конкретный регистр, а именно не стандартным для текущей конвенции вызовов способом, нужно поставить галку Use Custom Storage и нажать на иконку с зелёным плюсом:


Появится позиция для нового входного аргумента. Кликаем по ней два раза, и получаем диалог указания типа и носителя аргумента:


В декомпилированном коде видим, что in_D1w имеет тип ushort , значит его и укажем в поле с типом. Затем нажмём кнопку Add :


Появится позиция для указания носителя аргумента, нам нужно указать в Location регистр D1w , и нажать OK :


Декомпилированный код примет вид:

Т.к. xor — операция обратимая, можно все константные числа поксорить между собой и получить искомое значение переменной DAT_00ff0020 .

Выходит, что значение переменной должно быть равно 0x4553 . Кажется, я уже видел место, где такое значение устанавливается.


Часть первая: ядро отладчика

Эмуляцией инструкций мотороловского процессора в Genesis Plus GX занимается Musashi. В его оригинальном исходнике уже имеется базовый отладочный функционал (хук на выполнение инструкций), но EkeEke решил убрать его за ненадобностью. Возвращаем.



Теперь самое главное: необходимо определиться с архитектурой отладчика. Требования следующие:

  • Бряки (точки останова) на исполнение, на чтение и запись в память
  • Функционал Step Into , Step Over
  • Приостановка ( Pause ), продолжение ( Resume ) эмуляции
  • Чтение/установка регистров, чтение/запись памяти

Если эти четыре пункта — это работа отладчика изнутри, то необходимо ещё продумать доступ к данному функционалу извне. Добавляем ещё один пункт:

  • Протокол общения отладчика-сервера (ядра) с отладчиком-клиентом (GUI, пользователь)

Ядро отладчика: список бряков

Для реализации списка заводим следующую структуру:

Поля next и prev будут хранить указатели на следующий и предыдущий элемент соответственно.
Поле enabled будет хранить 0 , если данный бряк требуется пропустить в проверках на срабатывание.
width — количество байт начиная от адреса в поле address , которые покрывает бряк.
Ну а в поле type будем хранить тип бряка (исполнение, чтение, запись). Подробнее ниже.

Для работы со списком точек останова я добавил следующие функции:

Ядро отладчика: основные переменные

Собственно, данную реализацию я подсмотрел в другом отладчике PCSXR.

Добавляем переменные, которые будут хранить состояние эмуляции:

dbg_trace нам понадобиться для исполнения по одной инструкции (функционал Step Into ). Если равно 1 , выполняем одну инструкцию, становимся на паузу, и сбрасываем значение в 0 .

Переменную dbg_dont_check_bp я завёл для того, чтобы бряки на чтение/запись памяти не срабатывали, если это делает отладчик.

dbg_step_over у нас будет хранить 1 , если мы в режиме Step Over до тех пор, пока текущий PC (Program Counter, он же Instruction Pointer) не станет равным адресу в dbg_step_over_addr . После этого обе переменные обнуляем. О подсчёте значения dbg_step_over_addr я расскажу позже.

Я завёл переменную dbg_last_pc для одного конкретного случая: когда мы уже стоим на бряке, и клиент просит Resume . Чтобы бряк не сработал снова, я сравниваю адрес последнего PC в этой переменной с новым, и, если значения разные, можно проверять брейкпоинт на текущем PC .

dbg_active — собственно, хранит состояние 1 , когда отладка активна и требуется проверять бряки, обрабатывать запросы от клиента.

С переменной dbg_paused , думаю, всё понятно: 1 — мы на паузе (например, после срабатывания бряка) и ожидаем команд от клиента, 0 — выполняем инструкции.

Пишем функции для работы с этими переменными:

Видим, что в реализации detach_debugger() я использовал очистку списка бряков. Это нужно для того, чтобы после отсоединения клиента, старые точки останова не продолжали срабатывать.

Ядро отладчика: реализуем хук на инструкции

Собственно, здесь и будет происходить основная работа с паузой, продолжением эмуляции, Step Into , Step Over .

Вот такой получился код для функции process_breakpoints() :

  1. Если отладка не включена, просто выходим из хука
  2. Трюк с setjmp / longjmp нужен был потому, что окно оболочки RetroArch , для которой была написана своя версия Genesis Plus GX , с помощью который мы и запускаем эмуляцию, подвисает в ожидании выхода из функции рендеринга кадра, которую как раз реализует эмулятор. Вторую часть трюка я покажу позже, т.к. она касается уже оболочки над эмулятором, нежели ядра.
  3. Если это наше первое срабатывание хука, а, соответственно, и начало эмуляции, ставим на паузу и отправляем событие о старте эмуляции клиенту.
  4. Если клиент ранее отправил команду Step Into , обнуляем значение переменной dbg_trace и ставим эмуляцию на паузу. Отправляем клиенту соответствующее событие.
  5. Если мы не на паузе, режим Step Over включен, и текущий PC равен адресу назначения dbg_step_over_addr , обнуляем необходимые переменные и ставим на паузу.
  6. Проверяем брейкпоинт, если мы сейчас не на нём, и, если бряк сработал, ставим на паузу и отправляем клиенту событие о Step Over или бряке.
  7. Если это не бряк, не Step Into , и не Step Over , значит клиент попросил паузу. Отправляем событие о сработавшей паузе.
  8. Реализуем трюк с longjump в качестве реализации бесконечного цикла ожидания действий от клиента во время паузы.

Код подсчёта адреса для Step Over оказался не таким простым, как можно предположить сначала. У мотороловского процессора бывает разная длина инструкций, поэтому приходится считать адрес следующей вручную, в зависимости от опкода. При том, нужно избегать инструкций типа bra , jmp , rts условных прыжков вперёд, и выполнять их как Step Into . Реализация следующая:

Ядро отладчика: инициализация и остановка отладки

Ядро отладчика: реализация протокола

Протокол общения между сервером-отладчиком и клиентом-пользователем можно смело назвать вторым сердцем процесса отладки, т.к. в нём реализован функционал обработки запросов от клиента, и реакции на них.
Реализовывать было решено на основе Shared Memory, потому как требуется пересылать большие блоки памяти: VRAM , RAM , ROM , а по сети это будет тем ещё удовольствием.

Суть такова: ядро создаёт расшареную память с заранее определённой структурой, и ожидает входящих запросов от клиента. После обработки запроса в ту же память сохраняется ответ, и в список событий отладчика в той же памяти добавляется соответствующая информация.

Прототип был выбран такой:

Первым полем в структуре у нас будет тип запроса:

  • чтение/установка регистров
  • чтение/запись памяти
  • работа с брейкпоинтами
  • приостановка/продолжение эмуляции, отсоединение/остановка отладчика
  • Step Into / Step Over

Далее идут регистры M68K , Z80 , VDP . Следом — блоки памяти ROM , RAM , VRAM , Z80 .

Для добавления/удаления бряка я так же завёл соответствующую структуру. Ну и их список тоже здесь (по большей части, он лишь для отображения в GUI, без необходимости помнить все установленные бряки, как это делает IDA ).

Далее идёт список отладочных событий:

  • Отладка начата (необходим для IDA Pro )
  • Отладка приостановлена (в событии сохраняется PC , на котором в данный момент приостановлена эмуляция)
  • Сработал брейкпоинт (так же хранит значение PC , на котором произошло срабатывание)
  • Был выполнен Step Into или Step Over (тоже, по сути, нужно только для IDA , т.к. можно обойтись и одним лишь событием паузы)
  • Процесс эмуляции был остановлен. После нажатия кнопки Stop в IDA без получения этого события она будет бесконечно ожидать остановки

Вооружившись идеей протокола, реализуем обработку запросов клиента, получая таким образом следующий код ядра отладчика:

Для RAM это будет выглядеть так (файл m68kcpu.h ):

Для доступа клиента к расшареной памяти, а также для отправки им запросов, и ожидания отладочных событий сделаем обёртку.

Сразу прошу меня простить за качество кода. Чукча больше реверсер, чем программист. Возможно, для синхронизации и ожидания стоило выбрать более адекватные способы, но, на момент написания кода они работали.

Ядро отладчика: запуск

Для включения отладки я добавил соответствующий пункт в опции Genesis Plus GX :

Немного об архитектуре RetroArch :
Для рендеринга фреймов, эмулятор каждый раз дёргает функцию retro_run() . Именно здесь выполняются инструкции процессора (а там как раз срабатывает наш хук), формируется буфер с картинкой. И, пока ядро не завершит функцию retro_run() , окно RetroArch будет висеть. Я исправил это трюком с setjmp() / longjmp() . Так вот, первую часть трюка я вставил в начало retro_run() :

Ну и в конце функции retro_run() я так же воткнул вызов process_request() , чтобы когда отладка не на паузе, иметь возможность принимать запросы.

P.S. Затравка для второй части


Update:
Во второй части статьи я расскажу о написании собственно плагина-отладчика для IDA Pro , и дам ссылки на все исходники.

Модернизация GHIDRA. Загрузчик для ромов Sega Mega Drive


Приветствую вас, товарищи. Не слышал о пока-ещё-не-опенсорсной GHIDRA , наверное, только глухой/слепой/немой/без_интернета реверс-инженер. Её возможности из коробки поражают: декомпиляторы для всех поддерживаемых процессоров, простое добавление новых архитектур (с сразу же активной декомпиляцией благодаря грамотному преобразованию в IR), куча скриптов упрощающих жизнь, возможность Undo / Redo … И это только очень малая часть всех предоставляемых возможностей. Сказать что я был впечатлён — это практически ничего не сказать.

Так вот, в этой статье я хотел бы рассказать вам, как я написал свой первый модуль для GHIDRA — загрузчик ромов игр для Sega Mega Drive / Genesis . Чтобы написать его мне понадобилась… всего пара-тройка часов! Поехали.

На понимание процесса написания загрузчиков для IDA я потратил когда-то несколько дней. Тогда это была версия 6.5 , кажется, а в те времена с документацией по SDK было очень много проблем.

Подготавливаем среду разработки

Разработчики GHIDRA продумали практически всё (Ильфак, где ты был раньше?). И, как раз для упрощения реализации нового функционала, ими был разработан плагин для Eclipse — GhidraDev , который фактически "помогает" писать код. Плагин интегрируется в среду разработки, и позволяет в несколько кликов создавать шаблоны проектов для скриптов, загрузчиков, процессорных модулей и расширений для них, а также — модули экспорта (как я понял, это какой-либо экспорт данных из проекта).

Для того, чтобы установить плагин, качаем Eclipse для Java , жмём Help -> Install New Software. , далее жмём кнопку Add , и открываем диалог выбора архива с плагином кнопкой Archive. . Архив с GhidraDev находится в каталоге $(GHIDRA)/Extensions/Eclipse/GhidraDev . Выбираем его, нажимаем кнопку Add .


В появившемся списке ставим галку на Ghidra , жмём Next > , соглашаемся с соглашениями, жмём Install Anyway (т.к. у плагина нет подписи), и перезапускаем Eclipse .


Итого, в менюшке IDE появится новый пункт GhidraDev для удобного создания и дистрибуции ваших проектов (конечно, можно создавать и через обычный мастер новых проектов Eclipse ). Кроме этого, у нас появляется возможность отлаживать разрабатываемый плагин или скрипт.


Что очень бесит в ситуации с GHIDRA , так это долбаные скопипасченые хайповые статьи, содержащие практически один и тот же материал, который, к тому же, не соответствует действительности. Пример? Да, пожалуйста:

The current version of the tool is 9.0. and the tool has options to include additional functionality such as Cryptanalysis, interaction with OllyDbg, the Ghidra Debugger.

И где это всё? Нету!

Второй момент: опенсорсность. По факту, она почти есть, но её практически нет. В поставке GHIDRA имеются исходники компонентов, которые были написаны на Java , но, если посмотреть Gradle -скрипты, можно увидеть, что там есть зависимости от кучи внешних проектов из пока ещё секретных лабораторий репозиториев NSA .
На момент написания статьи, исходников декомпилятора и SLEIGH (это утилита для компиляции описаний процессорных модулей и преобразований в IR) нету.

Ну да ладно, я отвлёкся что-то.

Итак, давайте всё таки создадим новый проект в Eclipse .

Модернизация IDA Pro. Отладчик для Sega Mega Drive (часть 1)


Товарищи реверсеры, ромхакеры: в основном эта статья будет посвящена вам. В ней я расскажу вам, как написать свой плагин-отладчик для IDA Pro . Да, уже была первая попытка начать рассказ, но, с тех пор много воды утекло, многие принципы пересмотрены. В общем, погнали!

Лирическое вступление

Собственно, из предыдущих статей (раз, два, три), думаю, не будет секретом, что мой любимый процессор — это Motorola 68000 . На нём, кстати, работает моя любимая старушка Sega Mega Drive / Genesis . И, так как мне всегда было интересно, как же всё таки устроены сеговские игры, я ещё с первых месяцев владения компьютером решил глубоко и надолго погрязть в дебрях дизассемблирования и реверсинга.

Так появился Smd IDA Tools.
Проект включает в себя различные вспомогательные штуки, которые делают работу по изучению ромов на Сегу значительно проще: загрузчик, отладчик, хелпер по командам VDP . Всё было написано для IDA 6.8 , и работало хорошо. Но, когда я решил поведать миру о том, как же я всё таки это сделал, стало понятно, что показывать такой код народу, а, тем более, описывать его, будет очень сложно. Поэтому я так и не смог этого сделать тогда.

А потом вышла IDA 7.0 . Желание портировать свой проект под неё появилось сразу, но архитектура эмулятора Gens , на основе которого я писал отладчик, оказалась непригодной для переноса: ассемблерные вставки под x86 , костыли, сложный в понимании код, и многое другое. Да и игра Pier Solar and the Great Architects , вышедшая на картриджах в 2010, и которую так хотелось исследовать (а антиэмуляционных трюков там полно), не запускалась в Gens 'е.


В поисках подходящего исходника эмулятора, который можно было бы адаптировать под отладчик, я в итоге наткнулся на Genesis Plus GX от EkeEke . Так и появилась данная статья.

Описание

Описание задания и сам ром можно скачать здесь.

Несмотря на то, что в списке ресурсов там говорится про Гидру, стандартом де-факто среди инструментов для отладки и реверса игр на Сегу является Smd Ida Tools. В нём есть всё необходимое для решения данного крекми:

  • Загрузчик ромов для Иды
  • Отладчик
  • Просмотр и изменение памяти RAM/VDP
  • Отображение практически полной информации по VDP

Закидываем в плагины к Иде последний релиз, и начинаем смотреть, что у нас имеется.

Исходники и прочее

Все исходники вы сможете найти в github-репозитории, включая готовый релиз.

А вывод можно сделать такой: гонка между IDA и GHIDRA потихоньку начинает быть проигранной одной из сторон. Как мне кажется.

Отлаживаем результаты наших трудов

Для отладки достаточно поставить бряки и нажать Run -> Debug As -> 1 Ghidra . Тут всё просто.


sega mega drive 2 замена встроенных игр

Решил вспомнить детство и поиграть с братом в старые игры. Купил в Ашане sega mega drive 2 с 75 встроенными играми. Только вот ни M.K., ни Червяка Джима, ни Алладина там нет. Причем, картриджи продаются только с теми играми, которые уже встроены в консоль. Хочется узнать, можно ли как нибудь заменить встроенные игры? И есть ли вообще в продаже нужное железо для этого? Если есть прошаренные люди, то пожалуйста отпишитесь.


На форумах пишут, что добраться до шлеф памяти не просто. Готовых решений я не нашел, то бишь прошивок нет. (да и вообще, придется распаивать память, дампить, программатор собирать, заливать прошивку, впаивать обратно).
Вообще есть перезаписываемые картриджи. Даже схемы решений, да и готовые решения в Инете попадаются.

Вот ссылки на само решение:
Подробно о создании такого картриджа
Приобритение у умельцев
Оф. продажа перезаписываемых картриджей компанией Neo Flash

VBLANK

Так как дальше по коду идёт цикл, то можно предположить, что основную инициализацию мы закончили. Теперь можно посмотреть на VBLANK -прерывание.


Видим, что инкрементируются две переменные (что странно, в списке ссылок на каждую из них абсолютно пусто). Но, раз они обновляются раз в кадр, можно назвать их timer1 и timer2 .

Далее вызывается функция sub_2FE . Посмотрим, что там:

sub_2FE()


А там — работа с IO_CT1_DATA портом (отвечает за первый джойстик). В регистр A0 грузится адрес порта, и передаётся в функцию sub_310 . Переходим туда:

sub_310()


Мой опыт снова мне помогает. Если вы видите код, который работает с джойстиком, и две переменные в памяти, значит одна хранит pressed keys , а вторая — held keys , т.е. нажатые только что и удерживаемые клавиши. Так и обзовём эти переменные: pressed_keys и held_keys . А функцию тогда можно назвать как update_joypad_state .

sub_2FE()

Обзываем функцию как read_joypad .

Создаём проект загрузчика

Жмём GhidraDev -> New -> Ghidra Module Project.

Указываем имя проекта (учитываем, что к именам файлов будут доклеиваться слова типа Loader , и, чтобы не получить что-то типа sega_loaderLoader.java , называем соответствующим образом).


Жмём Next > . Здесь выставляем галки напротив категорий, которые нам необходимы. В моём случае это только Loader . Жмём Next > .


Здесь указываем путь к каталогу с Гидрой . Жмём Next > .


GHIDRA позволяет писать скрипты на питоне (через Jython ). Я буду писать на Java , поэтому галку не ставлю. Жму Finish .


Цикл обработчика

Теперь всё выглядит куда понятнее:


Значит этот цикл реагирует на нажатые клавиши, и выполняет соответствующие им действия. Пройдёмся по каждой из вызываемых в цикле функций.

sub_4D4()


Кода здесь много. Начнём с первой вызываемой функции: sub_60C .

sub_60C()

Она ничего не делает — так может показаться сначала. Просто возврат из текущей функции — rts . Но, т.к. на неё происходят только прыжки ( bsr ), значит rts вернёт нас обратно в цикл обработчика. Я бы назвал эту функцию как retn_to_loop .

sub_4D4()

Далее видим обращение к переменной word_FF000E . Она нигде, кроме текущей функции не используется и, поначалу, назначение мне её было не понятно. Но, если присмотреться, можно предположить, что эта переменная нужна лишь для небольшой задержки между обработкой нажатых клавиш. (Она и так плохо реализована в этом роме, но, думаю, без этой переменной было бы куда хуже).


Далее у нас идёт большое количество кода, который как-то обрабатывает переменные sprite_pos_x и sprite_pos_y , что может говорить только об одном — это нужно для отображения спрайта выделения вокруг выделенного в алфавите символа.

Значит теперь можно смело назвать функцию как update_selection . Идём дальше.


Код проверяет, установлены ли биты каких-то нажатых клавиш, и вызывает определённые функции. Посмотрим на них.


Какая-то шаманская магия. Сначала из переменной word_FF0018 берётся WORD , затем происходит выполнение одной интересной инструкции:

Эта команда просто прыгает на следующую за ней инструкцию.

Далее — ещё одна магия:

Значение в регистре D0 кладётся на вершину стека. Тут стоит отметить, что у Сеги, как и у какого-нибудь x86 , адрес возврата из функции при её вызове кладётся на вершину стека. Соответственно, первая инструкция кладёт на вершину какой-то адрес, а вторая — поднимает его со стека и совершает по нему переход. Хороший трюк.

Теперь нужно понять, что это за значение в переменной, по которому потом происходит переход. Но, для начала назовём эту переменную как jmp_addr .

А функции назовём так:

  • sub_D38 : goto_to_d0
  • sub_D28 : jump_to_var_addr

jmp_addr

Выясним, где эта переменная заполняется. Смотрим список референсов:


Существует лишь одно место записи в эту переменную. Посмотрим на него.

sub_3A4()


Здесь, в зависимости от координаты спрайта (помним, что это скорее всего адрес выделенного символа), заносится то или иное значение. Видим следующий участок кода:


Имеющееся значение сдвигается вправо на 4 бита, в младший байт помещается новое значение, и результат заносится в переменную снова. В теории, наша переменная jmp_addr хранит те символы, которые мы можем вводить на экране ввода ключа. Заметим также, что размер переменной — WORD .

По сути, функцию sub_3A4 можно назвать как update_jmp_addr .

sub_414()

Теперь у нас осталась всего одна функция в цикле, которая не распознана. И называется она sub_414 .


Код её напоминает код функции update_jmp_addr , только в конце у нас происходит вызов функции sub_45E . Заглянем туда.

sub_45E()


Далее по коду происходит работа с переменной byte_FF0014 , которая нигде, кроме текущей функции не используется. Если присмотреться, как она используется, можно заметить, что максимальное число, которое в ней может установиться, это 4 . У меня такое предположение, что эта текущая длина введённого ключа. Давайте это проверим.

Запускаем отладчик

Я воспользуюсь отладчиком из Smd Ida Tools , но, по сути, достаточно будет и какого-нибудь Gens KMod, или Gens ReRecording. Главное, чтобы была фича с отображением адресов в памяти.


Моя теория подтвердилась. Значит переменную byte_FF0014 теперь можно обозвать key_length .

Так что же делает эта функция? У меня есть предположение, что она отрисовывает введённый символ. Проверить это просто — запустив отладчик, и сравнив состояние до вызова функции sub_45E и после:



Я был прав — эта функция отрисовывает введённый символ. Назовём её do_draw_input_char , а функцию, которая её вызывает ( sub_414 ) — draw_input_char .

Читайте также: