Привет всем,  Не знаю как вам, а мне всегда хотелось пореверсить старые приставочные игры, имея в запасе ещё и декомпилятор. И вот, этот радостный момент в моей жизни настал — вышла GHIDRA. О том, что это такое, писать не буду, можно легко загуглить. И, отзывы настолько разные (особенно от ретроградов), что новичку будет сложно даже решиться на запуск этого чуда… Вот вам пример: "20 лет работал в иде, и смотрю я на вашу Гидру с большим недоверием, потому что АНБ. Но когда-нибудь запущу и проверю её в деле". Если в двух словах — запускать Гидру не страшно. И то, что мы получаем после запуска перекроет весь ваш страх перед закладками-и-бэкдорами от вездесущих АНБ. Так вот, о чём это я… Есть такая приставка: Sony Playstation 1 (PS1, PSX, Плойка). Под неё было создано множество крутых игр, появилась куча франшиз, которые популярны до сих пор. И мне однажды захотелось узнать, как они устроены: какие есть форматы данных, используется ли сжатие ресурсов, попробовать что-то перевести на русский язык (сразу скажу, пока не перевёл ни одной игры). Начал с того, что написал с товарищем на Delphi крутую утилиту для работы с форматом TIM (это что-то типа BMP от мира Playstation): Tim2View. В своё время пользовалась успехом (а может и сейчас пользуется). Потом захотелось углубиться в сжатие.  И тут начались проблемы. С процессором MIPS я тогда ещё не был знаком. Взялся изучать. С IDA Pro я тоже тогда не был знаком (я пришёл к реверсу игр на Sega Mega Drive позже Playstation). Но, благодаря интернету я узнал, что как раз таки IDA Pro поддерживает загрузку и анализ исполняемых файлов PS1: PS-X EXE. Попробовал закинуть файл игры (кажется, это были Lemmings) со странным именем и расширением, типа SLUS_123.45 в Иду, получил кучу строк ассемблерного кода (к счастью, я уже имел представление о том, что это такое, благодаря exe-шникам Винды под x86), и начал разбираться.  Первым трудным для понимания местом был конвейер инструкций. Например, вы видите вызов какой-то функции, а сразу за ним идёт загрузка в регистр параметра, который должен использоваться в этой функции. Если вкратце, то перед всякими прыжками и вызовами функций сначала выполняется следующая за прыжком/вызовом инструкция, а уже потом — сам вызов или прыжок. После всех пройденных трудностей мне удалось написать несколько упаковщиков/распаковщиков ресурсов игр. Но именно изучением кода я никогда не занимался. Почему? Ну, всё банально: кода было очень много, обращения к BIOS и функциям, которые понять было фактически невозможно (они были библиотечными, а SDK под плойку у меня тогда не было), инструкции, работающие с тремя регистрами одновременно, отсутствие декомпилятора. И вот, спустя много-много лет, выходит GHIDRA. Среди поддерживаемых декомпилятором платформ есть MIPS. О, радость! Давайте же скорее попробуем что-то декомпилировать! Но… меня ждал облом. Исполняемые файлы PS-X EXE не поддерживаются Гидрой. Не беда, напишем свой! Хватит лирических отступлений, давайте писать код. Как создавать свои загрузчики для Ghidra, я уже представление имел, о чём писал ранее. Поэтому, осталось только найти Memory Map первой плойки, адреса регистров и, можно собирать и грузить бинари. Сказано — сделано. Код был готов, регистры и регионы добавлялись и распознавались, но на местах вызовов библиотечных функций и функций BIOS по-прежнему было большое белое пятно. И, к сожалению, поддержки FLIRT у Гидры не было. Если нету, давай добавим. Формат FLIRT-сигнатур известен и описан в pat.txt файлике, который можно найти в SDK Иды. Также, у Иды есть утилита для создания этих сигнатур специально из библиотечных файлов Playstation, и называется: ppsx. Скачал SDK для плойки, который называется PsyQ Playstation Development Kit, нашёл там lib-файлы и попробовал создать из них хоть какие-то сигнатуры — успешно. Получается текстовичок, в котором каждая строка имеет определённый формат. Остаётся написать код, который будет парсить эти строки, и применять их на код.  Так как каждая строка имеет определённый формат, логично будет написать регулярное выражение. Оно получилось таким: Ну и для выделения потом в списке модулей отдельно смещения, типа, и имени функции, пишем отдельный регэксп: Теперь пройдёмся по составляющим каждой сигнатуры отдельно: У каждого имени в модуле есть определённый тип и смещение относительно начала. Тип может быть обозначен одним из символов: :, ^, @, в зависимости от типа: С одной стороны всё просто, но, ссылка запросто может быть не ссылкой на функцию (и, соответственно, прыжок будет относительным), а на глобальную переменную. В чём, скажете вы, проблема? А она в том, что в PSX нельзя одной инструкцией затолкать целый DWORD в регистр. Для этого необходимо загрузить его в виде половинок. Дело в том, в MIPS размер инструкции ограничен четырьмя байтами. И, казалось бы, нужно всего лишь сначала получить одну половинку из одной инструкции, а затем дизассемблировать следующую — и получить вторую половинку. Но не так всё просто. Первая половинка может быть загружена инструкций 5 назад, а ссылка в модуле будет дана только после загрузки её второй половины. Пришлось писать изощрённый парсер (наверное его можно доработать). В итоге, создаём enum для трёх типов имён: Давайте напишем код, который преобразовывает текстовые шестнадцатеричные последовательности и точки в тип MaskedBytes: Уже можно подумать и о классе, который будет хранить информацию о каждой отдельной функции: имя функции, смещение в модуле, и тип: И, последнее: класс, который будет хранить всё, что указано в каждой строке pat-файла, то бишь: байты, crc, список имён со смещениями: Теперь основное: пишем код для создания всех эти классов: Код создания функции там, где распозналась одна из сигнатур: Самое сложное место, как говорилось ранее — подсчёт ссылки на другое имя/переменную (возможно, код нуждается в доработке): И, финальный штрих — применяем сигнатуры: Тут можно рассказать об одной интересной функции: findBytes(). С её помощью можно искать определённые последовательности байт, с указанными битовыми масками для каждого байта. Метод вызывается так: В итоге возвращается адрес, с которого начинаются байты, либо null. Давайте сделаем красиво, и не будем применять сигнатуры, если мы не хотим, а позволим выбирать этот шаг пользователю. Для этого необходимо будет написать свой собственный анализатор кода (вы могли видеть подобные в этом списке — это всё они, да):  Итак, чтобы вклиниться в этот список, нужно будет унаследоваться от класса AbstractAnalyzer и переопределить некоторые методы: Где PsxLoader.PSX_LOADER хранит имя загрузчика, и определено ранее в нём же. Итого, имеем: Тут необходимо пояснить. Статический метод getModuleDataFile() класса Application возвращает полный путь к файлу в каталоге data, который имеется в дереве нашего модуля, и может хранить любые необходимые файлы, на которые мы захотим позже сослаться. Ну а метод registerOption() регистрирует опцию с именем указанным в OPTION_NAME, типом File (т.е. у пользователя будет возможность выбирать файл через обычное диалоговое окно), дефолтным значением и описанием. Далее. Т.к. нормальной возможности потом сослаться на зарегистрированную опцию у нас не будет, потребуется переопределить метод optionsChanged(): Здесь мы просто обновляем глобальную переменную согласно новому значению. Метод added(). Теперь основное: метод, который будет вызываться при запуске анализатора. В него нам будет приходить список адресов доступных для анализа, но, нам нужны только те, что содержат код. Поэтому нужно отфильтровать. Итоговый код: Тут мы проходимся по списку адресов, которые являются исполняемыми, и пытаемся применить там сигнатуры.  Вроде всё. На самом деле, супер сложного здесь ничего нет. Примеры есть, сообщество живое, можно спокойно спрашивать о том, что не понятно, пока пишешь код. Итог: рабочий загрузчик и анализатор исполняемых файлов Playstation 1.  Все исходные коды доступны здесь: ghidra_psx_ldr
Релизы тут: Releases Реверс-инженер