From 2d78a20141fe2636a1c43785cca27f690365028f Mon Sep 17 00:00:00 2001 From: kotcrab <4594081+kotcrab@users.noreply.github.com> Date: Thu, 27 Feb 2025 20:27:50 +0100 Subject: [PATCH] Add initial relocatable kernel object exporter --- CHANGES.md | 1 + README.md | 9 +- src/main/kotlin/allegrex/export/ExportElf.kt | 253 +++++++ .../export/RelocatableKernelObjectExporter.kt | 638 ++++++++++++++++++ .../allegrex/util/LeRandomAccessFile.kt | 228 +++++++ 5 files changed, 1128 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/allegrex/export/ExportElf.kt create mode 100644 src/main/kotlin/allegrex/export/RelocatableKernelObjectExporter.kt create mode 100644 src/main/kotlin/allegrex/util/LeRandomAccessFile.kt diff --git a/CHANGES.md b/CHANGES.md index 673d3bd..875d182 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,7 @@ #### Version 21.1 - Added build for Ghidra 11.3.1 - Added missing implementation for `eret` instruction +- Added exporter for relocatable kernel objects #### Version 21 - Added build for Ghidra 11.3 diff --git a/README.md b/README.md index 8e3b9a6..f02604e 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ your Ghidra version. Then depending on the version you're installing: ### Version 19 or newer In main Ghidra window: + 1. Select `File -> Install Extensions`. 2. Press the green plus button. 3. Select downloaded ZIP. @@ -55,9 +56,15 @@ file in PPSSPP. ### Kernel modules Usage is very similar as when importing games though kernel modules are usually loaded starting from address `88000000`. -Note that for some files (e.g. `sysmem`, `loadcore`) you will need to click `Options...` during import and select checkbox to +Note that for some files (e.g. `sysmem`, `loadcore`) you will need to click `Options...` during import and select checkbox to use `reboot.bin` type B relocation mapping. +#### Exporting relocatable kernel objects + +Kernel modules can be exported as object files (`.o`). +To learn more about this feature see +[here](https://github.com/kotcrab/ghidra-allegrex/wiki/Exporting-relocatable-kernel-objects). + ### Raw binaries Raw binaries are also supported. In that case you will need to manually select Allegrex as the processor and set image base. diff --git a/src/main/kotlin/allegrex/export/ExportElf.kt b/src/main/kotlin/allegrex/export/ExportElf.kt new file mode 100644 index 0000000..1551fa3 --- /dev/null +++ b/src/main/kotlin/allegrex/export/ExportElf.kt @@ -0,0 +1,253 @@ +package allegrex.export + +import allegrex.export.ExportElf.SectionName.COMBINED_0 +import allegrex.export.ExportElf.SectionName.COMBINED_1 +import allegrex.export.ExportElf.SectionName.REL_COMBINED_0 +import allegrex.export.ExportElf.SectionName.REL_COMBINED_1 +import allegrex.export.ExportElf.SectionName.SHSTRTAB +import allegrex.export.ExportElf.SectionName.STRTAB +import allegrex.export.ExportElf.SectionName.SYMTAB +import allegrex.util.LeRandomAccessFile +import allegrex.util.align +import allegrex.util.writeString +import java.io.ByteArrayOutputStream +import java.io.File + +object ExportElf { + fun writeElf( + outFile: File, + sections: List
, + shStrTab: StringAllocator, + symbols: List, + strTab: StringAllocator, + programBytes: List, + programRelocations: List>, + ) { + LeRandomAccessFile(outFile).use { + it.setLength(0) + // 0x00 + it.writeByte(0x7F) // magic number + it.writeString("ELF") + it.write(byteArrayOf(0x01, 0x01, 0x01, 0x00)) // 32-bit, little-endian, version 1, ABI none + it.writeInt(0) + it.writeInt(0) + + // 0x10 + it.writeShort(0x01) // ET_REL + it.writeShort(0x08) // MIPS + it.writeInt(1) // version 1 + it.writeInt(0) // entry point offset + it.writeInt(0) // program headers offset + + // 0x20 + val sectionHeaderOffset = it.filePointer + it.writeInt(0) // section header offset (written later) + it.writeInt(0x10A23001) // flags + it.writeShort(0x34) // header size + it.writeShort(0) // program header size + it.writeShort(0) // program header count + it.writeShort(0x28) // section header size + + // 0x30 + it.writeShort(sections.size) + it.writeShort(sections.indexOfFirst { e -> shStrTab.lookupOrNull(e.name) == ".shstrtab" }) + + // 0x34 + // Write program bytes + val programBytesOffsets = programBytes.map { bytes -> + val start = it.filePointer + it.write(bytes) + it.align(4) + start + } + + // Write relocations + val programRelocationsOffsets = programRelocations.map { relocations -> + val start = it.filePointer + relocations.forEach { relocation -> + it.writeRelocation(relocation) + } + it.align(4) + start + } + + // Write symbols + val symTabStart = it.filePointer + symbols.forEach { symbol -> + it.writeSymbol(symbol) + } + it.align(4) + + // Write strtab + val strTabStart = it.filePointer + val strTabBytes = strTab.toByteArray() + it.write(strTabBytes) + it.align(4) + + // Write shstrtab + val shStrTabStart = it.filePointer + val shStrTabBytes = shStrTab.toByteArray() + it.write(shStrTabBytes) + it.align(4) + + // Write sections + val sectionsStart = it.filePointer + + it.seek(sectionHeaderOffset) + it.writeInt(sectionsStart.toInt()) + it.seek(sectionsStart) + + sections.forEach { section -> + it.writeInt(section.name) + it.writeInt(section.type) + it.writeInt(section.flags) + it.writeInt(section.address) + + if (section.offset == -1) { + it.writeInt( + when (shStrTab.lookupOrNull(section.name)) { + COMBINED_0 -> programBytesOffsets[0] + COMBINED_1 -> programBytesOffsets[1] + REL_COMBINED_0 -> programRelocationsOffsets[0] + REL_COMBINED_1 -> programRelocationsOffsets[1] + SYMTAB -> symTabStart + STRTAB -> strTabStart + SHSTRTAB -> shStrTabStart + else -> error("Unknown section: ${section.name}") + }.toInt() + ) + } else { + it.writeInt(section.offset) + } + + if (section.size == -1) { + it.writeInt( + when (shStrTab.lookupOrNull(section.name)) { + COMBINED_0 -> programBytes[0].size + COMBINED_1 -> programBytes[1].size + REL_COMBINED_0 -> programRelocations[0].size * 8 + REL_COMBINED_1 -> programRelocations[1].size * 8 + SYMTAB -> symbols.size * 0x10 + STRTAB -> strTabBytes.size + SHSTRTAB -> shStrTabBytes.size + else -> error("Unknown section: ${section.name}") + }.toInt() + ) + } else { + it.writeInt(section.size) + } + + it.writeInt(section.link) + it.writeInt(section.info) + it.writeInt(section.addressAlign) + it.writeInt(section.entrySize) + } + } + } + + private fun LeRandomAccessFile.writeRelocation(relocation: AllegrexRelocation) { + writeInt(relocation.offset) + writeByte(relocation.type) + writeByte(relocation.symbol) + writeByte(relocation.symbol shr 8) + writeByte(relocation.symbol shr 16) + } + + private fun LeRandomAccessFile.writeSymbol(symbol: Symbol) { + writeInt(symbol.name) + writeInt(symbol.value) + writeInt(symbol.size) + writeByte(symbol.info.toInt()) + writeByte(symbol.other.toInt()) + writeShort(symbol.sectionIndex) + } + + object SectionName { + const val COMBINED_0 = ".combined0" + const val REL_COMBINED_0 = ".rel.combined0" + const val COMBINED_1 = ".combined1" + const val REL_COMBINED_1 = ".rel.combined1" + const val SYMTAB = ".symtab" + const val STRTAB = ".strtab" + const val SHSTRTAB = ".shstrtab" + } + + object Const { + const val SHT_PROGBITS = 0x1 + const val SHT_SYMTAB = 0x2 + const val SHT_STRTAB = 0x3 + const val SHT_REL = 0x9 + + const val SHF_WRITE = 0x1 + const val SHF_ALLOC = 0x2 + const val SHF_EXECINSTR = 0x4 + const val SHF_INFO_LINK = 0x40 + + const val STB_LOCAL = 0x0 shl 4 + const val STB_GLOBAL = 0x1 shl 4 + const val STB_WEAK = 0x2 shl 4 + + const val STT_NOTYPE = 0x0 + const val STT_FUNC = 0x2 + const val STT_SECTION = 0x3 + } + + class StringAllocator { + private val cache = mutableMapOf() + private val bytes = ByteArrayOutputStream() + + init { + getOrPut("") + } + + fun getOrPut(text: String): Int { + return cache.getOrPut(text) { + val pos = bytes.size() + bytes.write(text.toByteArray(Charsets.US_ASCII)) + bytes.write(byteArrayOf(0)) + pos + } + } + + fun lookup(offset: Int): String { + return lookupOrNull(offset) + ?: error("Can't find string in table for offset: $offset") + } + + fun lookupOrNull(offset: Int): String? { + return cache.entries.firstOrNull { it.value == offset }?.key + } + + fun toByteArray(): ByteArray { + return bytes.toByteArray() + } + } + + data class AllegrexRelocation( + val offset: Int, + val type: Int, + val symbol: Int, + ) + + data class Symbol( + val name: Int, + val value: Int, + val size: Int, + val info: Byte, + val other: Byte, + val sectionIndex: Int + ) + + data class Section( + val name: Int, + val type: Int, + val flags: Int, + val address: Int, + val offset: Int, + val size: Int, + val link: Int, + val info: Int, + val addressAlign: Int, + val entrySize: Int, + ) +} diff --git a/src/main/kotlin/allegrex/export/RelocatableKernelObjectExporter.kt b/src/main/kotlin/allegrex/export/RelocatableKernelObjectExporter.kt new file mode 100644 index 0000000..24f5db8 --- /dev/null +++ b/src/main/kotlin/allegrex/export/RelocatableKernelObjectExporter.kt @@ -0,0 +1,638 @@ +package allegrex.export + +import allegrex.export.ExportElf.Const.SHF_ALLOC +import allegrex.export.ExportElf.Const.SHF_EXECINSTR +import allegrex.export.ExportElf.Const.SHF_INFO_LINK +import allegrex.export.ExportElf.Const.SHF_WRITE +import allegrex.export.ExportElf.Const.SHT_PROGBITS +import allegrex.export.ExportElf.Const.SHT_REL +import allegrex.export.ExportElf.Const.SHT_STRTAB +import allegrex.export.ExportElf.Const.SHT_SYMTAB +import allegrex.export.ExportElf.Const.STB_GLOBAL +import allegrex.export.ExportElf.Const.STB_LOCAL +import allegrex.export.ExportElf.Const.STB_WEAK +import allegrex.export.ExportElf.Const.STT_FUNC +import allegrex.export.ExportElf.Const.STT_NOTYPE +import allegrex.export.ExportElf.Const.STT_SECTION +import allegrex.export.ExportElf.SectionName.COMBINED_0 +import allegrex.export.ExportElf.SectionName.COMBINED_1 +import allegrex.export.ExportElf.SectionName.REL_COMBINED_0 +import allegrex.export.ExportElf.SectionName.REL_COMBINED_1 +import allegrex.export.ExportElf.SectionName.SHSTRTAB +import allegrex.export.ExportElf.SectionName.STRTAB +import allegrex.export.ExportElf.SectionName.SYMTAB +import allegrex.format.elf.relocation.AllegrexElfRelocationConstants +import allegrex.format.elf.relocation.AllegrexRelocation +import ghidra.app.decompiler.DecompInterface +import ghidra.app.decompiler.component.DecompilerUtils +import ghidra.app.util.DomainObjectService +import ghidra.app.util.Option +import ghidra.app.util.OptionException +import ghidra.app.util.OptionUtils +import ghidra.app.util.exporter.Exporter +import ghidra.app.util.exporter.ExporterException +import ghidra.framework.model.DomainObject +import ghidra.program.model.address.Address +import ghidra.program.model.address.AddressRange +import ghidra.program.model.address.AddressSetView +import ghidra.program.model.listing.Function +import ghidra.program.model.listing.Program +import ghidra.program.model.mem.Memory +import ghidra.program.model.mem.MemoryBlock +import ghidra.program.model.pcode.PcodeOp +import ghidra.program.model.pcode.PcodeOpAST +import ghidra.program.model.pcode.Varnode +import ghidra.program.model.reloc.Relocation +import ghidra.program.model.reloc.RelocationTable +import ghidra.program.model.symbol.SymbolType +import ghidra.util.task.TaskMonitor +import java.io.File +import java.io.IOException + +@Suppress("unused") +class RelocatableKernelObjectExporter : Exporter( + "Relocatable PSP kernel object", "o", null +) { + companion object { + private object Options { + object GlobalDataSymbolsPattern { + private const val NAME = "Global data symbols names (regex)" + const val DEFAULT = "^g_.+\$" + + fun toOption() = Option(NAME, DEFAULT) + + fun getValue(options: List