Modernization of GHIDRA. Loader for rum Sega Mega Drive
- Tutorial
Greetings, comrades. I have not heard about the yet-not-open-source GHIDRA
, probably only a deaf / blind / dumb / no-Internet reverse engineer. Its capabilities out of the box are amazing: decompilers for all supported processors, simple addition of new architectures (with immediate active decompilation due to competent conversion to IR), a bunch of scripts simplifying life, the ability to ... Undo
/ Redo
And this is only a very small part of all the provided features. To say that I was impressed is to say almost nothing.
So, in this article I would like to tell you how I wrote my first module for GHIDRA
- a bootloader of roms for games Sega Mega Drive / Genesis
. To write it I needed ... just a couple of hours! Go.
IDA
I spent some days understanding the process of writing downloaders for some time. Then it was the version 6.5
, it seems, and in those days there were a lot of problems with the SDK documentation.
We prepare the development environment
The developers GHIDRA
thought through almost everything ( Ilfak , where have you been before?). And, just to simplify the implementation of the new functionality, they developed a plugin for Eclipse
- GhidraDev
, which actually " helps " write code. The plugin integrates into the development environment, and allows you to create project templates for scripts, loaders, processor modules and extensions for them, as well as export modules (as I understand it, this is some kind of export of data from a project) with a few clicks.
In order to install the plugin, download Eclipse
for Java
, press Help
-> Install New Software...
, then press the button Add
, and open the archive selection dialog with the plugin with a button Archive...
. The archive with GhidraDev
is in the catalog $(GHIDRA)/Extensions/Eclipse/GhidraDev
. Select it, press the button Add
.
In the list that appears, put a daw on Ghidra
, click Next >
, agree to the agreements, click Install Anyway
(because the plugin does not have a signature), and restart Eclipse
.
In total, a new item will appear in the IDE menu GhidraDev
for conveniently creating and distributing your projects (of course, you can also create through the usual wizard for new projects Eclipse
). In addition, we have the opportunity to debug the developed plug-in or script.
What is very enraging in the situation with GHIDRA
is the fucking copy-cracked hype articles containing almost the same material, which, moreover, is not true. Example? Yes please:
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.
And where is all this? No!
The second point: openness. In fact, it is almost there, but it is practically nonexistent. There GHIDRA
are source codes of components that were written in Java
, but if you look at the Gradle
scripts, you can see that there are dependencies on a bunch of external projects from secret oneslaboratoriesrepositories NSA
.
At the time of writing, there are no decompiler sources and SLEIGH
(this is a utility for compiling descriptions of processor modules and conversions to IR).
Well oh well, I'm distracted by something.
So, let's create a new project in Eclipse
.
Create a loader project
Click GhidraDev
-> New
->Ghidra Module Project...
We indicate the name of the project (we take into account that words of the type will be glued to the file names Loader
, and in order not to get something of the type sega_loaderLoader.java
, we name them accordingly).
Click Next >
. Here we put daws in front of the categories that we need. In my case it is only Loader
. Click Next >
.
Here we indicate the path to the directory c Гидрой
. Click Next >
.
GHIDRA
allows you to write scripts in python (via Jython
). I will write on Java
, so I do not put a daw. I press Finish
.
Writing a code
The empty project tree looks impressive:
All files with the java
code are in the branch /src/main/java
:
getName ()
To get started, let's choose a name for the bootloader. The method returns it getName()
:
@Override
public String getName() {
return "Sega Mega Drive / Genesis Loader";
}
findSupportedLoadSpecs ()
The method findSupportedLoadSpecs()
decides (based on the data contained in the binary file) which processor module should be used for disassembly (as well as in IDA
). In terminology, GHIDRA
this is called Compiler Language
. It includes: processor, endianness, bitness and compiler (if known).
This method returns a list of supported architectures and languages. If the data is in the wrong format, we simply return an empty list.
So, in the case of Sega Mega Drive
, by the 0x100
heading offset the word " SEGA
" is most often present (this is not a prerequisite, but is fulfilled in 99% of cases). You need to check if this line is in the imported file. To do this, the input findSupportedLoadSpecs()
is fed ByteProvider provider
through which we will work with the file.
We create an object BinaryReader
for the convenience of reading data from a file:
BinaryReader reader = new BinaryReader(provider, false);
The argument false
in this case indicates use Big Endian
when reading. Now let's read the line. To do this, use the method of readAsciiString(offset, size)
the object reader
:
reader.readAsciiString(0x100, 4).equals(new String("SEGA"))
If he equals()
returns true
, then we are dealing with Segov’s rum, and Motorolovskiy can be added to the list . To do this, create a new type object , the constructor of which takes the loader object as input (in our case it is ), into which ROM will be loaded, the type object and flag - is this one preferred among the others in the list (yes, there can be more than one in the list ) .List
m68k
LoadSpec
this
ImageBase
LanguageCompilerSpecPair
LoadSpec
LoadSpec
The constructor format is LanguageCompilerSpecPair
as follows:
- The first argument is
languageID
a string of the form " ProcessorName: Endianness: Bits: ExactCpu ". In my case, it should be the line " 68000: BE: 32: MC68020 " (unfortunatelyMC68000
, it’s not exactly in the supply, but that’s not such a problem).ExactCpu
maybedefault
- The second argument -
compilerSpecID
- you can find what you need to specify here in the directory with processor descriptionsГидры
($(GHIDRA)/Ghidra/Processors/68000/data/languages
) in the file68000.opinion
. We see that only are indicated heredefault
. Actually, we indicate it
As a result, we have the following code (as you can see, nothing complicated so far):
@Override
public Collection findSupportedLoadSpecs(ByteProvider provider) throws IOException {
List 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;
}
There is a difference, and it is still very strong. You GHIDRA
can write in one project that will understand different architectures, different data formats, be a loader, a processor module, an extension of the decompiler functionality, and other goodies.
At the IDA
same it is a separate project for each type of supplement.
How much more convenient? In my opinion, y GHIDRA
- at times!
load ()
In this method, we will create segments, process input data, create code and previously known labels. To do this, the following objects are submitted to help us at the entrance:
ByteProvider provider
: we already know him. Working with binary file dataLoadSpec loadSpec
: An architecture specification that was selected during the file import phase of the methodfindSupportedLoadSpecs
. It is necessary if, for example, we are able to work with several data formats in one module. ConvenientlyList
: список опций (включая кастомные). С ними я пока не научился работать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) |
We set up the fields, check for a sufficient length of the input data, use two methods new to us readNextByteArray()
, an readNextUnsignedShort()
object reader
for reading data, and create a structure. The resulting code is as follows:
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;
}
}
Create objects for the header:
vectors = new VectorsTable(fpa, reader);
header = new GameHeader(fpa, reader);
Segments
Sega has a well-known map of memory regions, which I, perhaps, will not give here in the form of a table, but only the code that is used to create the segments.
So, the class object FlatProgramAPI
has a method createMemoryBlock()
with which it is convenient to create memory regions. At the input, it takes the following arguments:
name
: region nameaddress
: address of the beginning of the regionstream
: An object of the typeInputStream
that will be the basis for the data in the memory region. If you specifynull
, then an uninitialized region will be created (for example, for68K RAM
orZ80 RAM
we just need thissize
: size of the created regionisOverlay
: Acceptstrue
orfalse
, and indicates that the memory region is overlay. I don’t know where it is needed except for executable files
At the output createMemoryBlock()
returns an object of type MemoryBlock
, which can be further set of access rights flags ( Read
, Write
, Execute
).
As a result, we get a function of the following form:
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));
}
}
Here we additionally called a static error
class method Msg
to display an error message.
A segment containing game rum can have a maximum size 0x3FFFFF
(everything else will already belong to other regions). Let's create it:
InputStream romStream = provider.getInputStream(0);
createSegment(fpa, romStream, "ROM", fpa.toAddr(0x000000), Math.min(romStream.available(), 0x3FFFFF), true, false, true);
Here we created InputStream
based on the input file, starting at offset 0.
Some segments, I would not want to create, without asking the user (this SegaCD
and Sega32X
segments). To do this, you can use the static methods of the class OptionDialog
. For example, it showYesNoDialogWithNoAsDefaultButton()
will show a dialog box with buttons YES
and NO
with a button activated by default NO
.
Create the above segments:
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);
}
Now you can create all the other segments:
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);
}
}
Arrays, tags, and specific addresses
There is a special class for creating arrays CreateArrayCmd
. We create a class object, specifying the following fields in the constructor:
address
: the address at which the array will be creatednumElements
: number of array elementsdataType
: data type of elements in an arrayelementSize
: single item size
Next, just call the method on the class object applyTo(program)
to create an array.
I need to create some addresses of an array, and the specific type of data, for example BYTE
, WORD
, DWORD
or structure. For this purpose, a class object FlatProgramAPI
has methods createByte()
, createWord()
, createDword()
etc.
Also, in addition to specifying the data type, it is necessary to give a name to each specific address (for example, these may be ports VDP
). For this, the following tricky construction is used:
- We
Program
call a method on an object of typegetSymbolTable()
that gives us access to a table of characters, labels, etc. - At the symbol table we pull the method
createLabel()
that accepts the input address, name and type of symbol. With the type of characters it is not very clear, but in the existing examples it is usedSourceType.IMPORTED
and I did the same
As a result, we get a couple of template methods for creating named arrays, or single data:
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);
Apply header structures
To apply structures to specific addresses, I will use the static createData()
class method DataUtilities
. This method accepts the following arguments:
program
: class objectProgram
address
: address to which the structure will be applieddataType
: structure typedataLength
: size of the structure. You can specify-1
for automatic calculationstackPointers
: iftrue
, some kind of magic happens with the calculation of the depth of the pointers. I putfalse
clearDataMode
: if suddenly at the place of creation of the structure there is already announced data, we choose the method of their definition (sorry, I could not come up with a Russian word)
There is one more thing left: Since we use a structure with vectors (read, addresses of functions), it would be logical to declare functions at these addresses. To do this, FlatProgramAPI
you can call a method createFunction()
on a type object that takes an address and a function name as input.
Now we have everything for creating header structures and designating data at the addresses of vectors as functions:
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");
}
}
We complete the load () method
For a beautiful notification of the user about the progress of the method, load()
you can use the method of the setMessage()
object of the type TaskMonitor
that we already have.
monitor.setMessage(String.format("%s : Start loading", getName()));
We put together the resulting set of functions, and we get the following code:
@Override
protected void load(ByteProvider provider, LoadSpec loadSpec, List
getDefaultOptions and validateOptions
In this article I do not consider them, because I haven’t come in handy yet
We are debugging the results of our work
For debugging, just put the breaks and click Run
-> Debug As
-> 1 Ghidra
. Everything is simple here.
Export distribution and installation in GHIDRA
Before exporting our distribution, let's add some description for our project. To do this, Eclipse
find the file in the project root extension.properties
and edit the fields:
description=Loader for Sega Mega Drive / Genesis ROMs
author=Dr. MefistO
createdOn=20.03.2019
To create the distribution of your plugin, click GhidraDev
-> Export
-> Ghidra Module Extension...
and follow the prompts of the distribution distribution wizard:
After all the manipulations in the folder dist
of your project get a zip-archive (something like ghidra_9.0_PUBLIC_20190320_Sega.zip
) with ready-to-use plug-in for GHIDRA
.
Let's install our plugin now. Launch Гидру
, click File
-> Install Extensions...
, click the icon with a green plus, and select the archive created earlier. Voila ...
Sources and stuff
You can find all the sources in the github repository , including the finished release.
And the conclusion can be made as follows: the race between IDA
and GHIDRA
slowly begins to be lost by one of the parties. It seems to me.