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/ RedoAnd 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.


But what about the IDA?

IDAI 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 GHIDRAthought 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 Eclipsefor 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 GhidraDevis 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 GhidraDevfor 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.



And where is the application debugger?

What is very enraging in the situation with GHIDRAis 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 GHIDRAare source codes of components that were written in Java, but if you look at the Gradlescripts, 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 >.



GHIDRAallows 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 javacode 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, GHIDRAthis 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 0x100heading 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 providerthrough which we will work with the file.


We create an object BinaryReaderfor the convenience of reading data from a file:


BinaryReader reader = new BinaryReader(provider, false);

The argument falsein this case indicates use Big Endianwhen 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 loadSpecs = new ArrayList<>();m68kLoadSpecthisImageBaseLanguageCompilerSpecPairLoadSpecLoadSpec


The constructor format is LanguageCompilerSpecPairas follows:


  1. The first argument is languageIDa string of the form " ProcessorName: Endianness: Bits: ExactCpu ". In my case, it should be the line " 68000: BE: 32: MC68020 " (unfortunately MC68000, it’s not exactly in the supply, but that’s not such a problem). ExactCpumaybedefault
  2. 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 file 68000.opinion. We see that only are indicated here default. 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;
}

The difference between IDA and GHIDRA in terms of modules

There is a difference, and it is still very strong. You GHIDRAcan 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 IDAsame 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:


  1. ByteProvider provider: we already know him. Working with binary file data
  2. LoadSpec loadSpec: An architecture specification that was selected during the file import phase of the method findSupportedLoadSpecs. It is necessary if, for example, we are able to work with several data formats in one module. Conveniently
  3. List: список опций (включая кастомные). С ними я пока не научился работать
  4. Program program: основной объект, который предоставляет доступ ко всему необходимому функционалу: листинг, адресное пространство, сегменты, метки, создание массивов и прочее
  5. MemoryConflictHandler handler и TaskMonitor monitor: напрямую с ними нам редко придётся работать (обычно, достаточно передавать эти объекты в уже готовые методы)
  6. 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 у нас будет принимать:


  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


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


Структура заголовка игрового рома
Start OffsetEnd OffsetDescription
$100$10FConsole name (usually 'SEGA MEGA DRIVE ' or 'SEGA GENESIS ')
$110$11FRelease date (usually '©XXXX YYYY.MMM' where XXXX is the company code, YYYY is the year and MMM — month)
$120$14FDomestic name
$150$17FInternational name
$180$18DVersion ('XX YYYYYYYYYYYY' where XX is the game type and YY the game code)
$18E$18FChecksum
$190$19FI/O support
$1A0$1A3ROM start
$1A4$1A7ROM end
$1A8$1ABRAM start (usually $00FF0000)
$1AC$1AFRAM end (usually $00FFFFFF)
$1B0$1B2'RA' and $F8 enables SRAM
$1B3----unused ($20)
$1B4$ 1B7SRAM start (default $ 00200000)
$ 1B8$ 1BBSRAM end (default $ 0020FFFF)
$ 1BC$ 1FFNotes (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 readerfor reading data, and create a structure. The resulting code is as follows:


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;
    }
}

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 FlatProgramAPIhas a method createMemoryBlock()with which it is convenient to create memory regions. At the input, it takes the following arguments:


  1. name: region name
  2. address: address of the beginning of the region
  3. stream: An object of the type InputStreamthat will be the basis for the data in the memory region. If you specify null, then an uninitialized region will be created (for example, for 68K RAMor Z80 RAMwe just need this
  4. size: size of the created region
  5. isOverlay: Accepts trueor false, 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 errorclass method Msgto 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 InputStreambased on the input file, starting at offset 0.


Some segments, I would not want to create, without asking the user (this SegaCDand Sega32Xsegments). To do this, you can use the static methods of the class OptionDialog. For example, it showYesNoDialogWithNoAsDefaultButton()will show a dialog box with buttons YESand NOwith 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:


  1. address: the address at which the array will be created
  2. numElements: number of array elements
  3. dataType: data type of elements in an array
  4. elementSize: 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, DWORDor structure. For this purpose, a class object FlatProgramAPIhas 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:


  1. We Programcall a method on an object of type getSymbolTable()that gives us access to a table of characters, labels, etc.
  2. 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 used SourceType.IMPORTEDand I did the same

As a result, we get a couple of template methods for creating named arrays, or single data:


Creating Arrays and Single Elements
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));
    }
}

Elements themselves
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:


  1. program: class object Program
  2. address: address to which the structure will be applied
  3. dataType: structure type
  4. dataLength: size of the structure. You can specify -1for automatic calculation
  5. stackPointers: if true, some kind of magic happens with the calculation of the depth of the pointers. I putfalse
  6. 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, FlatProgramAPIyou 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 TaskMonitorthat 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, Eclipsefind the file in the project root extension.propertiesand 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 distof 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 IDAand GHIDRAslowly begins to be lost by one of the parties. It seems to me.


Also popular now: