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

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

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

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

А что же IDA?

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

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

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

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

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

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

А где же отладчик приложений?

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

0. The current version of the tool is 9. 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 >. Жмём Next >. В моём случае это только Loader.

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

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

Пишем код

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

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

getName()

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

@Override
public String getName() { return "Sega Mega Drive / Genesis Loader";
}

findSupportedLoadSpecs()

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

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

Формат конструктора у 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. Собственно, его и указываем

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

@Override
public Collection<LoadSpec> findSupportedLoadSpecs(ByteProvider provider) throws IOException return loadSpecs;
}

Различие между IDA и GHIDRA в плане модулей

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

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

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

load()

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

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

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

BinaryReader reader = new BinaryReader(provider, false);

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

FlatProgramAPI fpa = new FlatProgramAPI(program, monitor);

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

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

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

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

VectorsTable

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

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

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 у нас будет принимать:

  1. FlatProgramAPI fpa для преобразования long адресов в тип данных Address Гидры (по сути, этот тип данных дополняет простое числовое значение адреса привязкой его к ещё одной фишке — адресному пространству)
  2. 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, реализующий интерфейс 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 для чтения данных, и создаём структуру.

GameHeader

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(), с помощью которого удобно создавать регионы памяти.

  1. name: имя региона
  2. address: адрес начала региона
  3. stream: объект типа InputStream, который будет являться основой для данных в регионе памяти. Если указать null, то будет создан неинициализированный регион (например, для 68K RAM или Z80 RAM нам именно такой и будет нужен
  4. size: размер создаваемого региона
  5. 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.

Для этого можно воспользоваться статическими методами класса OptionDialog. Некоторые сегменты я бы не хотел создавать, не спросив у пользователя (это SegaCD и Sega32X сегменты). Например, 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.

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

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

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

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

  1. У объекта типа Program вызываем метод getSymbolTable(), который предоставляет нам доступ к таблице символов, меток и т.д.
  2. У таблицы символов дёргаем метод 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.

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

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

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

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

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

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


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Импортозамещение на практике. Часть 3. Операционные системы

В предыдущих публикациях были рассмотрены варианты замещения развернутых систем на «отечественные», и конкретно гипервизоры «отечественного производства». Продолжаем наш цикл статей про импортозамещение. Теперь настала очередь поговорить об «отечественных» операционных системах, входящих в реестр Минкомсвязи на настоящий момент. 0. Отправная точка ...

Последний IRM — апгрейдим Siebel до IP17+

В этом посте вы не найдете брызг радости или намека на легкость бытия. Ну всё, шутки в сторону — поговорим о вечном. Начиная с 2013 года, Oracle проводит кампанию по принципиальной модернизации CRM-системы. Потому что он для тех, кто боролся ...