Skip to content

Commit

Permalink
Add emulation tests, tidy up code
Browse files Browse the repository at this point in the history
  • Loading branch information
Gekkio committed Nov 6, 2020
1 parent 040e531 commit 13e6a13
Show file tree
Hide file tree
Showing 15 changed files with 685 additions and 67 deletions.
27 changes: 15 additions & 12 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,24 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.time.LocalDate
import java.time.format.DateTimeFormatter.BASIC_ISO_DATE
import java.util.Properties
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
kotlin("jvm") version "1.4.10"
java
kotlin("jvm") version "1.4.10"
id("org.jlleitschuh.gradle.ktlint") version "9.4.1"
}

repositories {
jcenter()
}

val ghidraDir = System.getenv("GHIDRA_INSTALL_DIR")
?: (project.findProperty("ghidra.dir") as? String)
?: throw IllegalStateException("Can't find Ghidra installation")
?: (project.findProperty("ghidra.dir") as? String)
?: throw IllegalStateException("Can't find Ghidra installation")

val ghidraProps = Properties().apply { file("$ghidraDir/Ghidra/application.properties").inputStream().use { load(it) } }
val ghidraVersion = ghidraProps.getProperty("application.version")!!
Expand All @@ -42,6 +43,7 @@ java {
tasks.withType<KotlinCompile> {
kotlinOptions {
jvmTarget = "11"
freeCompilerArgs += "-Xopt-in=kotlin.ExperimentalUnsignedTypes"
}
}

Expand All @@ -57,6 +59,7 @@ dependencies {
testImplementation(kotlin("stdlib-jdk8"))
testImplementation(platform("org.junit:junit-bom:5.7.0"))
testImplementation("org.junit.jupiter:junit-jupiter-api")
testImplementation("org.junit.jupiter:junit-jupiter-params")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
}

Expand All @@ -67,11 +70,11 @@ val generateExtensionProps by tasks.registering() {
output.outputStream().use {
val props = Properties()
props += mapOf(
("name" to "GhidraBoy"),
("description" to "Support for Sharp SM83 / Game Boy"),
("author" to "Gekkio"),
("createdOn" to LocalDate.now().toString()),
("version" to ghidraVersion)
("name" to "GhidraBoy"),
("description" to "Support for Sharp SM83 / Game Boy"),
("author" to "Gekkio"),
("createdOn" to LocalDate.now().toString()),
("version" to ghidraVersion)
)
props.store(it, null)
}
Expand All @@ -83,10 +86,10 @@ val compileSleigh by tasks.registering(JavaExec::class) {
val slaFile = file("data/languages/sm83.sla")

inputs.files(fileTree("data/languages").include("*.slaspec", "*.sinc"))
.withPropertyName("sourceFiles")
.withPathSensitivity(PathSensitivity.RELATIVE)
.withPropertyName("sourceFiles")
.withPathSensitivity(PathSensitivity.RELATIVE)
outputs.files(slaFile)
.withPropertyName("outputFile")
.withPropertyName("outputFile")

classpath = configurations["ghidra"]
main = "ghidra.pcodeCPort.slgh_compile.SleighCompile"
Expand Down
25 changes: 11 additions & 14 deletions src/main/java/fi/gekkio/ghidraboy/BootRomUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,38 +14,35 @@
package fi.gekkio.ghidraboy;

import ghidra.app.util.bin.ByteProvider;
import ghidra.util.HashUtilities;

import java.io.IOException;
import java.util.Arrays;
import java.util.Optional;

import static ghidra.util.HashUtilities.getHash;

public final class BootRomUtils {
private BootRomUtils() {
}

private static final String[] GB_ROMS = {
"26e71cf01e301e5dc40e987cd2ecbf6d0276245890ac829db2a25323da86818e", // DMG0
"cf053eccb4ccafff9e67339d4e78e98dce7d1ed59be819d2a1ba2232c6fce1c7", // DMG
"0e4ddff32fc9d1eeaae812a157dd246459b00c9e14f2f61751f661f32361e360", // SGB
"a8cb5f4f1f16f2573ed2ecd8daedb9c5d1dd2c30a481f9b179b5d725d95eafe2", // MGB
"fd243c4fb27008986316ce3df29e9cfbcdc0cd52704970555a8bb76edbec3988", // SGB2
private static final Sha256[] GB_ROMS = {
Sha256.parse("26e71cf01e301e5dc40e987cd2ecbf6d0276245890ac829db2a25323da86818e"), // DMG0
Sha256.parse("cf053eccb4ccafff9e67339d4e78e98dce7d1ed59be819d2a1ba2232c6fce1c7"), // DMG
Sha256.parse("0e4ddff32fc9d1eeaae812a157dd246459b00c9e14f2f61751f661f32361e360"), // SGB
Sha256.parse("a8cb5f4f1f16f2573ed2ecd8daedb9c5d1dd2c30a481f9b179b5d725d95eafe2"), // MGB
Sha256.parse("fd243c4fb27008986316ce3df29e9cfbcdc0cd52704970555a8bb76edbec3988"), // SGB2
};
private static final String[] CGB_ROMS = {
"3a307a41689bee99a9a32ea021bf45136906c86b2e4f06c806738398e4f92e45", // CGB0
"b4f2e416a35eef52cba161b159c7c8523a92594facb924b3ede0d722867c50c7", // CGB
private static final Sha256[] CGB_ROMS = {
Sha256.parse("3a307a41689bee99a9a32ea021bf45136906c86b2e4f06c806738398e4f92e45"), // CGB0
Sha256.parse("b4f2e416a35eef52cba161b159c7c8523a92594facb924b3ede0d722867c50c7"), // CGB
};

public static Optional<GameBoyKind> detectBootRom(ByteProvider provider) throws IOException {
if (provider.length() == 0x100) {
var hash = getHash(HashUtilities.SHA256_ALGORITHM, provider.getInputStream(0));
var hash = Sha256.of(provider);
if (Arrays.stream(GB_ROMS).anyMatch(known -> known.equals(hash))) {
return Optional.of(GameBoyKind.GB);
}
} else if (provider.length() == 0x900) {
var hash = getHash(HashUtilities.SHA256_ALGORITHM, provider.getInputStream(0));
var hash = Sha256.of(provider);
if (Arrays.stream(CGB_ROMS).anyMatch(known -> known.equals(hash))) {
return Optional.of(GameBoyKind.CGB);
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/fi/gekkio/ghidraboy/DataTypes.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ private DataTypes() {
TITLE_BLOCK.add(TITLE_BLOCK_OLD, "old_format", null);
TITLE_BLOCK.add(TITLE_BLOCK_NEW, "new_format", null);

SGB_FLAG = new EnumDataType("sflag", 1);
SGB_FLAG = new EnumDataType("sgb_flag", 1);
SGB_FLAG.add("NONE", 0x00);
SGB_FLAG.add("SUPPORT", 0x03);

Expand Down
9 changes: 2 additions & 7 deletions src/main/java/fi/gekkio/ghidraboy/RomUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,20 @@
package fi.gekkio.ghidraboy;

import ghidra.app.util.bin.ByteProvider;
import ghidra.util.HashUtilities;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Optional;

import static ghidra.util.HashUtilities.getHash;

public final class RomUtils {
private RomUtils() {
}

private static final String LOGO_HASH = "daf4cabdc852baa0291849203f0b41fd0b4ecd58e0d7aff4a509f5de4d7f9a2e";
private static final Sha256 LOGO_HASH = Sha256.parse("daf4cabdc852baa0291849203f0b41fd0b4ecd58e0d7aff4a509f5de4d7f9a2e");

public static Optional<GameBoyKind> detectRom(ByteProvider provider) throws IOException {
if (provider.length() >= 0x150) {
var logo = provider.readBytes(0x0104, 0x30);
var hash = getHash(HashUtilities.SHA256_ALGORITHM, new ByteArrayInputStream(logo));
if (LOGO_HASH.equals(hash)) {
if (LOGO_HASH.equals(Sha256.of(logo))) {
var cgbFlag = (short) provider.readByte(0x0143);
return Optional.of((cgbFlag & 0x80) == 0 ? GameBoyKind.GB : GameBoyKind.CGB);
}
Expand Down
68 changes: 68 additions & 0 deletions src/main/java/fi/gekkio/ghidraboy/Sha256.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright 2019-2020 Joonas Javanainen <[email protected]>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package fi.gekkio.ghidraboy;

import ghidra.app.util.bin.ByteProvider;
import ghidra.util.HashUtilities;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.regex.Pattern;

public final class Sha256 {
public final String value;

private Sha256(String value) {
this.value = value;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Sha256 sha256 = (Sha256) o;
return value.equals(sha256.value);
}

@Override
public int hashCode() {
return value.hashCode();
}

@Override
public String toString() {
return this.value;
}

private static final Pattern SHA256_HEX = Pattern.compile("^[0-9a-z]{64}$");

public static Sha256 parse(String sha256Hex) {
if (!SHA256_HEX.matcher(sha256Hex).matches()) {
throw new IllegalArgumentException("Invalid SHA256 " + sha256Hex);
}
return new Sha256(sha256Hex);
}

public static Sha256 of(ByteProvider provider) throws IOException {
try (var stream = provider.getInputStream(0)) {
return new Sha256(HashUtilities.getHash(HashUtilities.SHA256_ALGORITHM, stream));
}
}

public static Sha256 of(byte[] bytes) throws IOException {
try (var stream = new ByteArrayInputStream(bytes)) {
return new Sha256(HashUtilities.getHash(HashUtilities.SHA256_ALGORITHM, stream));
}
}
}
40 changes: 9 additions & 31 deletions src/test/kotlin/fi/gekkio/ghidraboy/DisassemblyTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,15 @@
// limitations under the License.
package fi.gekkio.ghidraboy

import generic.jar.ResourceFile
import ghidra.app.plugin.processors.sleigh.SleighLanguageProvider
import ghidra.app.emulator.EmulatorHelper
import ghidra.program.database.ProgramDB
import ghidra.program.disassemble.Disassembler
import ghidra.program.model.address.Address
import ghidra.program.model.lang.Language
import ghidra.program.model.listing.CodeUnit
import ghidra.util.task.TaskMonitor
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.extension.ExtendWith
import java.io.File

@ExtendWith(GhidraApplication::class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class DisassemblyTest {
class DisassemblyTest : IntegrationTest() {
@Test
fun `can disassemble NOP`() = test(0x00, "NOP")

Expand Down Expand Up @@ -767,7 +757,13 @@ class DisassemblyTest {
fun `can disassemble XOR n`() = test(0xee, "XOR 0x55", 0x55)

@Test
fun `can disassemble RST 0x28`() = test(0xef, "RST 0x0028")
fun `can disassemble RST 0x28`() = test(0xef, "RST 0x0028") {
val helper = EmulatorHelper(it.program)
helper.writeRegister("SP", 0xffff)
helper.step(TaskMonitor.DUMMY)
println(helper.readRegister("SP"))
println(helper.readRegister("PC"))
}

@Test
fun `can disassemble LDH A, (n)`() = test(0xf0, "LDH A,(0x55)", 0x55)
Expand Down Expand Up @@ -820,22 +816,6 @@ class DisassemblyTest {
@Test
fun `can disassemble RST 0x38`() = test(0xff, "RST 0x0038")


private lateinit var language: Language

@BeforeAll
fun beforeAll() {
val defs = ResourceFile(File("data/languages/sm83.ldefs"))
val provider = SleighLanguageProvider(defs)
assertFalse(provider.hadLoadFailure())
val languageDescription = provider.languageDescriptions.single()

assertEquals("Sharp SM83", languageDescription.description)
assertEquals("SM83", languageDescription.processor.toString())

language = provider.getLanguage(languageDescription.languageID)
}

private fun test(opcode: Int, expected: String, vararg args: Int, assertions: (codeUnit: CodeUnit) -> Unit = {}) {
val codeUnit = disassemble(byteArrayOf(opcode.toByte(), *(args.map { it.toByte() }).toByteArray()))
assertEquals(expected, codeUnit.toString())
Expand All @@ -854,6 +834,4 @@ class DisassemblyTest {
program.codeManager.getCodeUnitAt(block.start)
}
}

private fun address(offset: Long): Address = language.addressFactory.defaultAddressSpace.getAddress(offset)
}
2 changes: 1 addition & 1 deletion src/test/kotlin/fi/gekkio/ghidraboy/GhidraApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class GhidraApplication : Extension {
private var initialized = false

fun initialize() = synchronized(this) {
check(!initialized) { "Can't initialize Ghidra more than once" }
if (initialized) return
val layout = GhidraApplicationLayout()
val configuration = HeadlessGhidraApplicationConfiguration()
Application.initializeApplication(layout, configuration)
Expand Down
45 changes: 45 additions & 0 deletions src/test/kotlin/fi/gekkio/ghidraboy/IntegrationTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright 2019-2020 Joonas Javanainen <[email protected]>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package fi.gekkio.ghidraboy

import generic.jar.ResourceFile
import ghidra.app.plugin.processors.sleigh.SleighLanguageProvider
import ghidra.program.model.address.Address
import ghidra.program.model.lang.Language
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.extension.ExtendWith
import java.io.File

@ExtendWith(GhidraApplication::class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
open class IntegrationTest {
protected lateinit var language: Language

@BeforeAll
private fun beforeAll() {
val defs = ResourceFile(File("data/languages/sm83.ldefs"))
val provider = SleighLanguageProvider(defs)
Assertions.assertFalse(provider.hadLoadFailure())
val languageDescription = provider.languageDescriptions.single()

Assertions.assertEquals("Sharp SM83", languageDescription.description)
Assertions.assertEquals("SM83", languageDescription.processor.toString())

language = provider.getLanguage(languageDescription.languageID)
}

protected fun address(offset: Long): Address = language.addressFactory.defaultAddressSpace.getAddress(offset)
}
2 changes: 1 addition & 1 deletion src/test/kotlin/fi/gekkio/ghidraboy/MemoryExt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ import ghidra.program.model.mem.MemoryBlock
import ghidra.util.task.TaskMonitor

fun Memory.loadBytes(name: String, start: Address, bytes: ByteArray, overlay: Boolean = false): MemoryBlock =
createInitializedBlock(name, start, bytes.inputStream(), bytes.size.toLong(), TaskMonitor.DUMMY, overlay)
createInitializedBlock(name, start, bytes.inputStream(), bytes.size.toLong(), TaskMonitor.DUMMY, overlay)
Loading

0 comments on commit 13e6a13

Please sign in to comment.