Приветствую вас, товарищи. Не слышал о пока-ещё-не-опенсорсной 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
.
Создаём проект загрузчика
Жмём GhidraDev
-> New
-> Ghidra Module Project...
Указываем имя проекта (учитываем, что к именам файлов будут доклеиваться слова типа Loader
, и, чтобы не получить что-то типа sega_loaderLoader.java
, называем соответствующим образом).
Жмём Next >
. Здесь выставляем галки напротив категорий, которые нам необходимы. В моём случае это только Loader
. Жмём Next >
.
Здесь указываем путь к каталогу с Гидрой
. Жмём Next >
.
GHIDRA
позволяет писать скрипты на питоне (через Jython
). Я буду писать на Java
, поэтому галку не ставлю. Жму Finish
.
Пишем код
Дерево пустого проекта выглядит внушительно:
Все файлы с java
-кодом лежат в ветке /src/main/java
:
getName()
Для начала, давайте выберем имя для загрузчика. Его возвращает метод getName()
:
@Override
public String getName() {
return "Sega Mega Drive / Genesis Loader";
}
findSupportedLoadSpecs()
Метод findSupportedLoadSpecs()
решает (на основе данных, которые содержатся в бинарном файле), какой процессорный модуль должен быть использован для дизассемблирования (так же как и в IDA
). В терминологии GHIDRA
это называется Compiler Language
. В него входят: процессор, endianness, битность и компилятор (если известен).
Данный метод возвращает список поддерживаемых архитектур и языков. Если же данные не того формата, мы просто вернём пустой список.
Итак, в случае с Sega Mega Drive
, по смещению 0x100
заголовка чаще всего присутствует слово "SEGA
" (это не обязательное условие, но выполняется в 99% случаев). Нужно проверить, имеется ли эта строка в импортируемом файле. Для этого, на вход findSupportedLoadSpecs()
подаётся ByteProvider provider
, с помощью которого мы и будем работать с файлом.
Создаём объект BinaryReader
, для удобства чтения данных из файла:
BinaryReader reader = new BinaryReader(provider, false);
Аргумент false
в данном случае указывает на использование Big Endian
при чтении. Теперь давайте прочитаем строку. Для этого воспользуемся методом readAsciiString(offset, size)
у объекта reader
:
reader.readAsciiString(0x100, 4).equals(new String("SEGA"))
Если equals()
вернёт true
, значит мы имеем дело с сеговским ромом, и в список List<LoadSpec> loadSpecs = new ArrayList<>();
можно будет добавить мотороловский m68k
. Для этого создаём новый объект типа LoadSpec
, конструктор которого принимает на вход объект загрузчика (в нашем случае это this
), ImageBase
, в который будет грузиться ROM, объект типа LanguageCompilerSpecPair
и флаг — предпочтительный ли этот LoadSpec
среди остальных в списке (да, в списке может быть не один LoadSpec
).
Формат конструктора у LanguageCompilerSpecPair
следующий:
- Первый аргумент —
languageID
— строка вида "ProcessorName:Endianness:Bits:ExactCpu". В моём случае это должна быть строка "68000:BE:32:MC68020" (к сожалению, ровноMC68000
в поставке нет, но, это не такая уж и проблема).ExactCpu
может быть иdefault
- Второй аргумент —
compilerSpecID
— найти то, что здесь необходимо указывать, можно в каталоге с процессорными описаниямиГидры
($(GHIDRA)/Ghidra/Processors/68000/data/languages
) в файле68000.opinion
. Видим, что здесь указаны толькоdefault
. Собственно, его и указываем
В итоге, имеем следующий код (как видим, пока ничего сложного):
@Override
public Collection<LoadSpec> findSupportedLoadSpecs(ByteProvider provider) throws IOException {
List<LoadSpec> loadSpecs = new ArrayList<>();
BinaryReader reader = new BinaryReader(provider, false);
if (reader.readAsciiString(0x100, 4).equals(new String("SEGA"))) {
loadSpecs.add(new LoadSpec(this, 0, new LanguageCompilerSpecPair("68000:BE:32:MC68020", "default"), true));
}
return loadSpecs;
}
Разница есть, и она прямо таки очень сильная. В GHIDRA
можно писать один проект, который будет понимать разные архитектуры, разные форматы данных, быть загрузчиком, процессорным модулем, расширением функционала декомпилятора, и другими плюшками.
В IDA
же это отдельный проект под каждый тип дополнения.
Насколько это удобнее? На мой взгляд, у GHIDRA
— в разы!
load()
В этом методе мы будем создавать сегменты, обрабатывать входные данные, создавать код и заранее известные метки. Для этого, нам в помощь на вход подаются следующие объекты:
ByteProvider provider
: его мы уже знаем. Работа с бинарными данными файлаLoadSpec loadSpec
: спецификация архитектуры, которая была выбрана на этапе импорта файла методомfindSupportedLoadSpecs
. Нужно, если мы, к примеру, умеем работать с несколькими форматами данных в одном модуле. УдобноList<Option> options
: список опций (включая кастомные). С ними я пока не научился работатьProgram program
: основной объект, который предоставляет доступ ко всему необходимому функционалу: листинг, адресное пространство, сегменты, метки, создание массивов и прочееMemoryConflictHandler handler
иTaskMonitor monitor
: напрямую с ними нам редко придётся работать (обычно, достаточно передавать эти объекты в уже готовые методы)MessageLog log
: собственно, логгер
Итак, для начала создадим некоторые объекты, которые упростят нам работу с сущностями GHIDRA
и имеющимися данными. Конечно, нам обязательно понадобится BinaryReader
:
BinaryReader reader = new BinaryReader(provider, false);
Далее. Нам очень пригодится и упростит практически всё объект класса FlatProgramAPI
(далее вы увидите, что с его помощью можно делать):
FlatProgramAPI fpa = new FlatProgramAPI(program, monitor);
Заголовок рома
Для начала определимся, что из себя представляет заголовок обычного сеговского рома. В первых 0x100
байтах идёт таблица из 64-х DWORD-указателей на вектора, например: Reset
, Trap
, DivideByZero
, VBLANK
и прочие.
Далее идёт структура с именем рома, регионами, адресами начала и конца блоков ROM
и RAM
, чексумма (поле проверяется по желанию разработчиков, а не приставкой) и другая информация.
Давайте создадим java
-классы для работы с этими структурами, а также для реализации типов данных, которые будут добавлены в список структур.
VectorsTable
Создаём новый класс VectorsTable
, и, внимание, указываем, что он реализует интерфейс StructConverter
. В этом классе мы будем хранить адреса векторов (for future use) и их имена.
Объявляем список имён векторов и их количество:
private static final int VECTORS_SIZE = 0x100;
private static final int VECTORS_COUNT = VECTORS_SIZE / 4;
private static final String[] VECTOR_NAMES = {
"SSP", "Reset", "BusErr", "AdrErr", "InvOpCode", "DivBy0", "Check", "TrapV", "GPF", "Trace",
"Reserv0", "Reserv1", "Reserv2", "Reserv3", "Reserv4", "BadInt", "Reserv10", "Reserv11",
"Reserv12", "Reserv13", "Reserv14", "Reserv15", "Reserv16", "Reserv17", "BadIRQ", "IRQ1",
"EXT", "IRQ3", "HBLANK", "IRQ5", "VBLANK", "IRQ7", "Trap0", "Trap1", "Trap2", "Trap3", "Trap4",
"Trap5", "Trap6", "Trap7", "Trap8", "Trap9", "Trap10", "Trap11", "Trap12", "Trap13","Trap14",
"Trap15", "Reserv30", "Reserv31", "Reserv32", "Reserv33", "Reserv34", "Reserv35", "Reserv36",
"Reserv37", "Reserv38", "Reserv39", "Reserv3A", "Reserv3B", "Reserv3C", "Reserv3D", "Reserv3E",
"Reserv3F"
};
Создаём отдельный класс для хранения адреса и имени вектора:
package sega;
import ghidra.program.model.address.Address;
public class VectorFunc {
private Address address;
private String name;
public VectorFunc(Address address, String name) {
this.address = address;
this.name = name;
}
public Address getAddress() {
return address;
}
public String getName() {
return name;
}
}
Список векторов будем хранить в массиве vectors
:
private VectorFunc[] vectors;
Констуктор для VectorsTable
у нас будет принимать:
FlatProgramAPI fpa
для преобразованияlong
адресов в тип данныхAddress
Гидры (по сути, этот тип данных дополняет простое числовое значение адреса привязкой его к ещё одной фишке — адресному пространству)BinaryReader reader
— чтение двордов
У объекта fpa
есть метод toAddr()
, а у reader
есть setPointerIndex()
и readNextUnsignedInt()
. В принципе, больше ничего не требуется. Получаем код:
public VectorsTable(FlatProgramAPI fpa, BinaryReader reader) throws IOException {
if (reader.length() < VECTORS_COUNT) {
return;
}
reader.setPointerIndex(0);
vectors = new VectorFunc[VECTORS_COUNT];
for (int i = 0; i < VECTORS_COUNT; ++i) {
vectors[i] = new VectorFunc(fpa.toAddr(reader.readNextUnsignedInt()), VECTOR_NAMES[i]);
}
}
Метод toDataType()
, который нам требуется переопределить для реализации структуры, должен вернуть объект Structure
, в котором должны быть объявлены имена полей структуры, их размеры, и комментарии к каждому полю (можно использовать null
):
@Override
public DataType toDataType() {
Structure s = new StructureDataType("VectorsTable", 0);
for (int i = 0; i < VECTORS_COUNT; ++i) {
s.add(POINTER, 4, VECTOR_NAMES[i], null);
}
return s;
}
Ну, и, давайте реализуем методы для получения каждого из векторов, либо всего списка целиком (куча шаблонного кода):
public VectorFunc[] getVectors() {
return vectors;
}
public VectorFunc getSSP() {
if (vectors.length < 1) {
return null;
}
return vectors[0];
}
public VectorFunc getReset() {
if (vectors.length < 2) {
return null;
}
return vectors[1];
}
public VectorFunc getBusErr() {
if (vectors.length < 3) {
return null;
}
return vectors[2];
}
public VectorFunc getAdrErr() {
if (vectors.length < 4) {
return null;
}
return vectors[3];
}
public VectorFunc getInvOpCode() {
if (vectors.length < 5) {
return null;
}
return vectors[4];
}
public VectorFunc getDivBy0() {
if (vectors.length < 6) {
return null;
}
return vectors[5];
}
public VectorFunc getCheck() {
if (vectors.length < 7) {
return null;
}
return vectors[6];
}
public VectorFunc getTrapV() {
if (vectors.length < 8) {
return null;
}
return vectors[7];
}
public VectorFunc getGPF() {
if (vectors.length < 9) {
return null;
}
return vectors[8];
}
public VectorFunc getTrace() {
if (vectors.length < 10) {
return null;
}
return vectors[9];
}
public VectorFunc getReserv0() {
if (vectors.length < 11) {
return null;
}
return vectors[10];
}
public VectorFunc getReserv1() {
if (vectors.length < 12) {
return null;
}
return vectors[11];
}
public VectorFunc getReserv2() {
if (vectors.length < 13) {
return null;
}
return vectors[12];
}
public VectorFunc getReserv3() {
if (vectors.length < 14) {
return null;
}
return vectors[13];
}
public VectorFunc getReserv4() {
if (vectors.length < 15) {
return null;
}
return vectors[14];
}
public VectorFunc getBadInt() {
if (vectors.length < 16) {
return null;
}
return vectors[15];
}
public VectorFunc getReserv10() {
if (vectors.length < 17) {
return null;
}
return vectors[16];
}
public VectorFunc getReserv11() {
if (vectors.length < 18) {
return null;
}
return vectors[17];
}
public VectorFunc getReserv12() {
if (vectors.length < 19) {
return null;
}
return vectors[18];
}
public VectorFunc getReserv13() {
if (vectors.length < 20) {
return null;
}
return vectors[19];
}
public VectorFunc getReserv14() {
if (vectors.length < 21) {
return null;
}
return vectors[20];
}
public VectorFunc getReserv15() {
if (vectors.length < 22) {
return null;
}
return vectors[21];
}
public VectorFunc getReserv16() {
if (vectors.length < 23) {
return null;
}
return vectors[22];
}
public VectorFunc getReserv17() {
if (vectors.length < 24) {
return null;
}
return vectors[23];
}
public VectorFunc getBadIRQ() {
if (vectors.length < 25) {
return null;
}
return vectors[24];
}
public VectorFunc getIRQ1() {
if (vectors.length < 26) {
return null;
}
return vectors[25];
}
public VectorFunc getEXT() {
if (vectors.length < 27) {
return null;
}
return vectors[26];
}
public VectorFunc getIRQ3() {
if (vectors.length < 28) {
return null;
}
return vectors[27];
}
public VectorFunc getHBLANK() {
if (vectors.length < 29) {
return null;
}
return vectors[28];
}
public VectorFunc getIRQ5() {
if (vectors.length < 30) {
return null;
}
return vectors[29];
}
public VectorFunc getVBLANK() {
if (vectors.length < 31) {
return null;
}
return vectors[30];
}
public VectorFunc getIRQ7() {
if (vectors.length < 32) {
return null;
}
return vectors[31];
}
public VectorFunc getTrap0() {
if (vectors.length < 33) {
return null;
}
return vectors[32];
}
public VectorFunc getTrap1() {
if (vectors.length < 34) {
return null;
}
return vectors[33];
}
public VectorFunc getTrap2() {
if (vectors.length < 35) {
return null;
}
return vectors[34];
}
public VectorFunc getTrap3() {
if (vectors.length < 36) {
return null;
}
return vectors[35];
}
public VectorFunc getTrap4() {
if (vectors.length < 37) {
return null;
}
return vectors[36];
}
public VectorFunc getTrap5() {
if (vectors.length < 38) {
return null;
}
return vectors[37];
}
public VectorFunc getTrap6() {
if (vectors.length < 39) {
return null;
}
return vectors[38];
}
public VectorFunc getTrap7() {
if (vectors.length < 40) {
return null;
}
return vectors[39];
}
public VectorFunc getTrap8() {
if (vectors.length < 41) {
return null;
}
return vectors[40];
}
public VectorFunc getTrap9() {
if (vectors.length < 42) {
return null;
}
return vectors[41];
}
public VectorFunc getTrap10() {
if (vectors.length < 43) {
return null;
}
return vectors[42];
}
public VectorFunc getTrap11() {
if (vectors.length < 44) {
return null;
}
return vectors[43];
}
public VectorFunc getTrap12() {
if (vectors.length < 45) {
return null;
}
return vectors[44];
}
public VectorFunc getTrap13() {
if (vectors.length < 46) {
return null;
}
return vectors[45];
}
public VectorFunc getTrap14() {
if (vectors.length < 47) {
return null;
}
return vectors[46];
}
public VectorFunc getTrap15() {
if (vectors.length < 48) {
return null;
}
return vectors[47];
}
public VectorFunc getReserv30() {
if (vectors.length < 49) {
return null;
}
return vectors[48];
}
public VectorFunc getReserv31() {
if (vectors.length < 50) {
return null;
}
return vectors[49];
}
public VectorFunc getReserv32() {
if (vectors.length < 51) {
return null;
}
return vectors[50];
}
public VectorFunc getReserv33() {
if (vectors.length < 52) {
return null;
}
return vectors[51];
}
public VectorFunc getReserv34() {
if (vectors.length < 53) {
return null;
}
return vectors[52];
}
public VectorFunc getReserv35() {
if (vectors.length < 54) {
return null;
}
return vectors[53];
}
public VectorFunc getReserv36() {
if (vectors.length < 55) {
return null;
}
return vectors[54];
}
public VectorFunc getReserv37() {
if (vectors.length < 56) {
return null;
}
return vectors[55];
}
public VectorFunc getReserv38() {
if (vectors.length < 57) {
return null;
}
return vectors[56];
}
public VectorFunc getReserv39() {
if (vectors.length < 58) {
return null;
}
return vectors[57];
}
public VectorFunc getReserv3A() {
if (vectors.length < 59) {
return null;
}
return vectors[58];
}
public VectorFunc getReserv3B() {
if (vectors.length < 60) {
return null;
}
return vectors[59];
}
public VectorFunc getReserv3C() {
if (vectors.length < 61) {
return null;
}
return vectors[60];
}
public VectorFunc getReserv3D() {
if (vectors.length < 62) {
return null;
}
return vectors[61];
}
public VectorFunc getReserv3E() {
if (vectors.length < 63) {
return null;
}
return vectors[62];
}
public VectorFunc getReserv3F() {
if (vectors.length < 64) {
return null;
}
return vectors[63];
}
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
для чтения данных, и создаём структуру. Итоговый код получается следующим:
package sega;
import java.io.IOException;
import ghidra.app.util.bin.BinaryReader;
import ghidra.app.util.bin.StructConverter;
import ghidra.program.flatapi.FlatProgramAPI;
import ghidra.program.model.address.Address;
import ghidra.program.model.data.DataType;
import ghidra.program.model.data.Structure;
import ghidra.program.model.data.StructureDataType;
public class GameHeader implements StructConverter {
private byte[] consoleName = null;
private byte[] releaseDate = null;
private byte[] domesticName = null;
private byte[] internationalName = null;
private byte[] version = null;
private short checksum = 0;
private byte[] ioSupport = null;
private Address romStart = null, romEnd = null;
private Address ramStart = null, ramEnd = null;
private byte[] sramCode = null;
private byte unused = 0;
private Address sramStart = null, sramEnd = null;
private byte[] notes = null;
FlatProgramAPI fpa;
public GameHeader(FlatProgramAPI fpa, BinaryReader reader) throws IOException {
this.fpa = fpa;
if (reader.length() < 0x200) {
return;
}
reader.setPointerIndex(0x100);
consoleName = reader.readNextByteArray(0x10);
releaseDate = reader.readNextByteArray(0x10);
domesticName = reader.readNextByteArray(0x30);
internationalName = reader.readNextByteArray(0x30);
version = reader.readNextByteArray(0x0E);
checksum = (short) reader.readNextUnsignedShort();
ioSupport = reader.readNextByteArray(0x10);
romStart = fpa.toAddr(reader.readNextUnsignedInt());
romEnd = fpa.toAddr(reader.readNextUnsignedInt());
ramStart = fpa.toAddr(reader.readNextUnsignedInt());
ramEnd = fpa.toAddr(reader.readNextUnsignedInt());
sramCode = reader.readNextByteArray(0x03);
unused = reader.readNextByte();
sramStart = fpa.toAddr(reader.readNextUnsignedInt());
sramEnd = fpa.toAddr(reader.readNextUnsignedInt());
notes = reader.readNextByteArray(0x44);
}
@Override
public DataType toDataType() {
Structure s = new StructureDataType("GameHeader", 0);
s.add(STRING, 0x10, "ConsoleName", null);
s.add(STRING, 0x10, "ReleaseDate", null);
s.add(STRING, 0x30, "DomesticName", null);
s.add(STRING, 0x30, "InternationalName", null);
s.add(STRING, 0x0E, "Version", null);
s.add(WORD, 0x02, "Checksum", null);
s.add(STRING, 0x10, "IoSupport", null);
s.add(POINTER, 0x04, "RomStart", null);
s.add(POINTER, 0x04, "RomEnd", null);
s.add(POINTER, 0x04, "RamStart", null);
s.add(POINTER, 0x04, "RamEnd", null);
s.add(STRING, 0x03, "SramCode", null);
s.add(BYTE, 0x01, "Unused", null);
s.add(POINTER, 0x04, "SramStart", null);
s.add(POINTER, 0x04, "SramEnd", null);
s.add(STRING, 0x44, "Notes", null);
return s;
}
public byte[] getConsoleName() {
return consoleName;
}
public byte[] getReleaseDate() {
return releaseDate;
}
public byte[] getDomesticName() {
return domesticName;
}
public byte[] getInternationalName() {
return internationalName;
}
public byte[] getVersion() {
return version;
}
public short getChecksum() {
return checksum;
}
public byte[] getIoSupport() {
return ioSupport;
}
public Address getRomStart() {
return romStart;
}
public Address getRomEnd() {
return romEnd;
}
public Address getRamStart() {
return ramStart;
}
public Address getRamEnd() {
return ramEnd;
}
public byte[] getSramCode() {
return sramCode;
}
public byte getUnused() {
return unused;
}
public Address getSramStart() {
return sramStart;
}
public Address getSramEnd() {
return sramEnd;
}
public boolean hasSRAM() {
if (sramCode == null) {
return false;
}
return sramCode[0] == 'R' && sramCode[1] == 'A' && sramCode[2] == 0xF8;
}
public byte[] getNotes() {
return notes;
}
}
Создаём объекты для заголовка:
vectors = new VectorsTable(fpa, reader);
header = new GameHeader(fpa, reader);
Сегменты
У Сеги есть вполне известная карта регионов памяти, которую я, пожалуй, тут приводить не буду в виде таблицы, а приведу лишь код, который используется для создания сегментов.
Итак, у объекта класса FlatProgramAPI
есть метод createMemoryBlock()
, с помощью которого удобно создавать регионы памяти. На вход он принимает следующие аргументы:
name
: имя регионаaddress
: адрес начала регионаstream
: объект типаInputStream
, который будет являться основой для данных в регионе памяти. Если указатьnull
, то будет создан неинициализированный регион (например, для68K RAM
илиZ80 RAM
нам именно такой и будет нуженsize
: размер создаваемого регионаisOverlay
: принимаетtrue
илиfalse
, и указывает, что регион памяти оверлейный. Где это нужно кроме исполняемых файлов я не знаю
На выходе createMemoryBlock()
возвращает объект типа MemoryBlock
, которому дополнительно можно установить флаги прав доступа (Read
, Write
, Execute
).
В итоге, получится функция следующего вида:
private void createSegment(FlatProgramAPI fpa, InputStream stream, String name, Address address, long size, boolean read, boolean write, boolean execute) {
MemoryBlock block = null;
try {
block = fpa.createMemoryBlock(name, address, stream, size, false);
block.setRead(read);
block.setWrite(read);
block.setExecute(execute);
} catch (Exception e) {
Msg.error(this, String.format("Error creating %s segment", name));
}
}
Здесь мы дополнительно вызвали статический метод error
класса Msg
, для вывода сообщения об ошибке.
Сегмент, содержащий игровой ром может иметь максимальный размер 0x3FFFFF
(всё остальное уже будет принадлежать другим регионам). Создадим его:
InputStream romStream = provider.getInputStream(0);
createSegment(fpa, romStream, "ROM", fpa.toAddr(0x000000), Math.min(romStream.available(), 0x3FFFFF), true, false, true);
Здесь мы создали InputStream
на основе входного файла, начиная со смещения 0.
Некоторые сегменты я бы не хотел создавать, не спросив у пользователя (это SegaCD
и Sega32X
сегменты). Для этого можно воспользоваться статическими методами класса OptionDialog
. Например, showYesNoDialogWithNoAsDefaultButton()
покажет диалоговое окно с кнопками YES
и NO
с активированной по-умолчанию кнопкой NO
.
Создаём указанные выше сегменты:
if (OptionDialog.YES_OPTION == OptionDialog.showYesNoDialogWithNoAsDefaultButton(null, "Question", "Create Sega CD segment?")) {
if (romStream.available() > 0x3FFFFF) {
InputStream epaStream = provider.getInputStream(0x400000);
createSegment(fpa, epaStream, "EPA", fpa.toAddr(0x400000), 0x400000, true, true, false);
} else {
createSegment(fpa, null, "EPA", fpa.toAddr(0x400000), 0x400000, true, true, false);
}
}
if (OptionDialog.YES_OPTION == OptionDialog.showYesNoDialogWithNoAsDefaultButton(null, "Question", "Create Sega 32X segment?")) {
createSegment(fpa, null, "32X", fpa.toAddr(0x800000), 0x200000, true, true, false);
}
Теперь можно создать все остальные сегменты:
createSegment(fpa, null, "Z80", fpa.toAddr(0xA00000), 0x10000, true, true, false);
createSegment(fpa, null, "SYS1", fpa.toAddr(0xA10000), 16 * 2, true, true, false);
createSegment(fpa, null, "SYS2", fpa.toAddr(0xA11000), 2, true, true, false);
createSegment(fpa, null, "Z802", fpa.toAddr(0xA11100), 2, true, true, false);
createSegment(fpa, null, "Z803", fpa.toAddr(0xA11200), 2, true, true, false);
createSegment(fpa, null, "FDC", fpa.toAddr(0xA12000), 0x100, true, true, false);
createSegment(fpa, null, "TIME", fpa.toAddr(0xA13000), 0x100, true, true, false);
createSegment(fpa, null, "TMSS", fpa.toAddr(0xA14000), 4, true, true, false);
createSegment(fpa, null, "VDP", fpa.toAddr(0xC00000), 2 * 9, true, true, false);
createSegment(fpa, null, "RAM", fpa.toAddr(0xFF0000), 0x10000, true, true, true);
if (header.hasSRAM()) {
Address sramStart = header.getSramStart();
Address sramEnd = header.getSramEnd();
if (sramStart.getOffset() >= 0x200000 && sramEnd.getOffset() <= 0x20FFFF && sramStart.getOffset() < sramEnd.getOffset()) {
createSegment(fpa, null, "SRAM", sramStart, sramEnd.getOffset() - sramStart.getOffset() + 1, true, true, false);
}
}
Массивы, метки и конкретные адреса
Для создания массивов имеется специальный класс CreateArrayCmd
. Создаём объект класса, указывая в конструкторе следующие поля:
address
: адрес, по которому будет создан массивnumElements
: количество элементов массиваdataType
: тип данных у элементов в массивеelementSize
: размер одного элемента
Далее достаточно вызвать у объекта класса метод applyTo(program)
, чтобы создать массив.
Для некоторых адресов мне требуется создать не массив, а конкретный тип данных, например BYTE
, WORD
, DWORD
или структура. Для этого, у объекта класса FlatProgramAPI
есть методы createByte()
, createWord()
, createDword()
и т.д.
Так же, кроме указания типа данных, необходимо дать имя каждому конкретному адресу (например, это могут быть порты VDP
). Для этого, используется следующая хитрая конструкция:
- У объекта типа
Program
вызываем методgetSymbolTable()
, который предоставляет нам доступ к таблице символов, меток, и т.д. - У таблицы символов дёргаем метод
createLabel()
, который принимает на вход адрес, имя и тип символа. С типа символов не очень понятно, но, в имеющихся примерах используетсяSourceType.IMPORTED
, и я поступил так же
В итоге получаем парочку шаблонных методов для создания именованных массивов, либо одиночных данных:
private void createNamedByteArray(FlatProgramAPI fpa, Program program, Address address, String name, int numElements) {
if (numElements > 1) {
CreateArrayCmd arrayCmd = new CreateArrayCmd(address, numElements, ByteDataType.dataType, ByteDataType.dataType.getLength());
arrayCmd.applyTo(program);
} else {
try {
fpa.createByte(address);
} catch (Exception e) {
Msg.error(this, "Cannot create byte. " + e.getMessage());
}
}
try {
program.getSymbolTable().createLabel(address, name, SourceType.IMPORTED);
} catch (InvalidInputException e) {
Msg.error(this, String.format("%s : Error creating array %s", getName(), name));
}
}
private void createNamedWordArray(FlatProgramAPI fpa, Program program, Address address, String name, int numElements) {
if (numElements > 1) {
CreateArrayCmd arrayCmd = new CreateArrayCmd(address, numElements, WordDataType.dataType, WordDataType.dataType.getLength());
arrayCmd.applyTo(program);
} else {
try {
fpa.createWord(address);
} catch (Exception e) {
Msg.error(this, "Cannot create word. " + e.getMessage());
}
}
try {
program.getSymbolTable().createLabel(address, name, SourceType.IMPORTED);
} catch (InvalidInputException e) {
Msg.error(this, String.format("%s : Error creating array %s", getName(), name));
}
}
private void createNamedDwordArray(FlatProgramAPI fpa, Program program, Address address, String name, int numElements) {
if (numElements > 1) {
CreateArrayCmd arrayCmd = new CreateArrayCmd(address, numElements, DWordDataType.dataType, DWordDataType.dataType.getLength());
arrayCmd.applyTo(program);
} else {
try {
fpa.createDWord(address);
} catch (Exception e) {
Msg.error(this, "Cannot create dword. " + e.getMessage());
}
}
try {
program.getSymbolTable().createLabel(address, name, SourceType.IMPORTED);
} catch (InvalidInputException e) {
Msg.error(this, String.format("%s : Error creating array %s", getName(), name));
}
}
createNamedDwordArray(fpa, program, fpa.toAddr(0xA04000), "Z80_YM2612", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA10000), "IO_PCBVER", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA10002), "IO_CT1_DATA", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA10004), "IO_CT2_DATA", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA10006), "IO_EXT_DATA", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA10008), "IO_CT1_CTRL", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA1000A), "IO_CT2_CTRL", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA1000C), "IO_EXT_CTRL", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA1000E), "IO_CT1_RX", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA10010), "IO_CT1_TX", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA10012), "IO_CT1_SMODE", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA10014), "IO_CT2_RX", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA10016), "IO_CT2_TX", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA10018), "IO_CT2_SMODE", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA1001A), "IO_EXT_RX", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA1001C), "IO_EXT_TX", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA1001E), "IO_EXT_SMODE", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA11000), "IO_RAMMODE", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA11100), "IO_Z80BUS", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xA11200), "IO_Z80RES", 1);
createNamedByteArray(fpa, program, fpa.toAddr(0xA12000), "IO_FDC", 0x100);
createNamedByteArray(fpa, program, fpa.toAddr(0xA13000), "IO_TIME", 0x100);
createNamedDwordArray(fpa, program, fpa.toAddr(0xA14000), "IO_TMSS", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xC00000), "VDP_DATA", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xC00002), "VDP__DATA", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xC00004), "VDP_CTRL", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xC00006), "VDP__CTRL", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xC00008), "VDP_CNTR", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xC0000A), "VDP__CNTR", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xC0000C), "VDP___CNTR", 1);
createNamedWordArray(fpa, program, fpa.toAddr(0xC0000E), "VDP____CNTR", 1);
createNamedByteArray(fpa, program, fpa.toAddr(0xC00011), "VDP_PSG", 1);
Применение структур заголовка
Для применения структур на конкретные адреса я воспользуюсь статическим методом createData()
класса DataUtilities
. Данный метод принимает на вход следующие аргументы:
program
: объект классаProgram
address
: адрес, на который будет применена структураdataType
: тип структурыdataLength
: размер структуры. Можно указать-1
для автоматического подсчётаstackPointers
: еслиtrue
, происходит какая-то магия с подсчётом глубины указателей. Ставлюfalse
clearDataMode
: если вдруг на месте создания структуры уже есть объявленные данные, выбираем метод их андефайна (простите, не смог придумать русское слово)
Остался ещё один момент: т.к. мы применяем структуру с векторами (читай, адресами функций), было бы логично по этим адресам объявить функции. Для этого у объекта типа FlatProgramAPI
можно вызвать метод createFunction()
, принимающий на вход адрес и имя функции.
Теперь у нас всё есть для создания структур заголовка и обозначения данных по адресам векторов как функций:
private void markVectorsTable(Program program, FlatProgramAPI fpa) {
try {
DataUtilities.createData(program, fpa.toAddr(0), vectors.toDataType(), -1, false, ClearDataMode.CLEAR_ALL_UNDEFINED_CONFLICT_DATA);
for (VectorFunc func : vectors.getVectors()) {
fpa.createFunction(func.getAddress(), func.getName());
}
} catch (CodeUnitInsertionException e) {
Msg.error(this, "Vectors mark conflict at 0x000000");
}
}
private void markHeader(Program program, FlatProgramAPI fpa) {
try {
DataUtilities.createData(program, fpa.toAddr(0x100), header.toDataType(), -1, false, ClearDataMode.CLEAR_ALL_UNDEFINED_CONFLICT_DATA);
} catch (CodeUnitInsertionException e) {
Msg.error(this, "Vectors mark conflict at 0x000100");
}
}
Завершаем метод load()
Для красивого уведомления пользователя о ходе работы метода load()
можно воспользоваться методом setMessage()
объекта типа TaskMonitor
, который у нас уже есть.
monitor.setMessage(String.format("%s : Start loading", getName()));
Собираем воедино получившийся набор функций, и получаем такой вот код:
@Override
protected void load(ByteProvider provider, LoadSpec loadSpec, List<Option> options,
Program program, MemoryConflictHandler handler, TaskMonitor monitor, MessageLog log)
throws CancelledException, IOException {
monitor.setMessage(String.format("%s : Start loading", getName()));
BinaryReader reader = new BinaryReader(provider, false);
FlatProgramAPI fpa = new FlatProgramAPI(program, monitor);
vectors = new VectorsTable(fpa, reader);
header = new GameHeader(fpa, reader);
createSegments(fpa, provider, program, monitor);
markVectorsTable(program, fpa);
markHeader(program, fpa);
monitor.setMessage(String.format("%s : Loading done", getName()));
}
getDefaultOptions и validateOptions
В данной статье я их не рассматриваю, потому как пока не пригодились
Отлаживаем результаты наших трудов
Для отладки достаточно поставить бряки и нажать Run
-> Debug As
-> 1 Ghidra
. Тут всё просто.
Экспорт дистрибутива и установка в GHIDRA
Прежде чем экспортировать, давайте добавим какое-то описание для нашего проекта. Для этого в корне проекта в Eclipse
находим файл extension.properties
, и редактируем поля:
description=Loader for Sega Mega Drive / Genesis ROMs
author=Dr. MefistO
createdOn=20.03.2019
Для создания дистрибутива вашего плагина жмём GhidraDev
-> Export
-> Ghidra Module Extension...
и следуем подсказкам мастера создания дистрибутива:
После всех манипуляций, в папке dist
вашего проекта получим zip-архив (что-то типа ghidra_9.0_PUBLIC_20190320_Sega.zip
с готовым к употреблению плагином для GHIDRA
.
Давайте теперь установим наш плагин. Запускаем Гидру
, жмём File
-> Install Extensions...
, жмём значок с зелёным плюсом, и выбираем созданный ранее архив. Вуаля...
Исходники и прочее
Все исходники вы сможете найти в github-репозитории, включая готовый релиз.
А вывод можно сделать такой: Ильфак очень много времени тратил на борьбу с пиратством и всякие там Hall of Shame
вместо того, чтобы пилить адекватный функционал. Да, у каждого из этих продуктов есть своё легаси, и, когда проект пилит полтора землекопа (Ильфак и Скочински) и когда его пилит целая крупная организация, разница, конечно, есть, но, почему-то легаси у IDA
уж очень долго тянется. Плюс, сейчас образуется серьёзное и хорошее комьюнити вокруг Гидры
, чего никак нельзя сказать об IDA
. Спасибо
Автор: DrMefistO