From 188d53a7685b8025a5c489792c3bed63f7144bb7 Mon Sep 17 00:00:00 2001 From: Smallhacker Date: Thu, 10 Jan 2019 22:19:08 -0500 Subject: [PATCH] Stuff --- .gitignore | 1 + ...nsetsu - Kamigami no Triforce (Japan).json | 64 +++++++ pom.xml | 21 +++ .../java/com/smallhacker/disbrowser/Grid.kt | 47 +++-- .../java/com/smallhacker/disbrowser/Html.kt | 2 + .../java/com/smallhacker/disbrowser/Main.java | 4 +- .../com/smallhacker/disbrowser/MyResource.kt | 159 ---------------- .../com/smallhacker/disbrowser/Service.kt | 78 ++++++++ .../com/smallhacker/disbrowser/asm/Address.kt | 33 +++- .../smallhacker/disbrowser/asm/Disassembly.kt | 17 +- .../smallhacker/disbrowser/asm/Instruction.kt | 173 +++++++++-------- .../smallhacker/disbrowser/asm/Metadata.kt | 122 ++++++++---- .../disbrowser/asm/MetadataLine.kt | 3 +- .../smallhacker/disbrowser/asm/Mnemonic.kt | 6 +- .../com/smallhacker/disbrowser/asm/Mode.kt | 175 +++++++++++------- .../smallhacker/disbrowser/asm/ModeFormat.kt | 19 -- .../com/smallhacker/disbrowser/asm/Opcode.kt | 20 +- .../com/smallhacker/disbrowser/asm/RomData.kt | 56 ++++-- .../com/smallhacker/disbrowser/asm/State.kt | 43 +++-- .../smallhacker/disbrowser/asm/VagueNumber.kt | 27 +-- .../disbrowser/disassembler/Disassembler.kt | 20 +- .../resource/DisassemblyResource.kt | 72 +++++++ .../disbrowser/resource/RestResource.kt | 40 ++++ .../disbrowser/resource/StaticResource.kt | 31 ++++ .../com/smallhacker/disbrowser/util/UVal.kt | 20 +- .../com/smallhacker/disbrowser/util/json.kt | 22 +++ .../com/smallhacker/disbrowser/util/misc.kt | 47 +++++ src/main/resources/main.js | 48 ----- src/main/resources/{ => public}/style.css | 12 ++ src/main/ts/main.ts | 62 +++++++ src/main/ts/tsconfig.json | 14 ++ 31 files changed, 941 insertions(+), 517 deletions(-) create mode 100644 Zelda no Densetsu - Kamigami no Triforce (Japan).json delete mode 100644 src/main/java/com/smallhacker/disbrowser/MyResource.kt create mode 100644 src/main/java/com/smallhacker/disbrowser/Service.kt delete mode 100644 src/main/java/com/smallhacker/disbrowser/asm/ModeFormat.kt create mode 100644 src/main/java/com/smallhacker/disbrowser/resource/DisassemblyResource.kt create mode 100644 src/main/java/com/smallhacker/disbrowser/resource/RestResource.kt create mode 100644 src/main/java/com/smallhacker/disbrowser/resource/StaticResource.kt create mode 100644 src/main/java/com/smallhacker/disbrowser/util/json.kt delete mode 100644 src/main/resources/main.js rename src/main/resources/{ => public}/style.css (89%) create mode 100644 src/main/ts/main.ts create mode 100644 src/main/ts/tsconfig.json diff --git a/.gitignore b/.gitignore index f419dc7..d5b6ff3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /.idea /target +/src/main/resources/public/disbrowser.* \ No newline at end of file diff --git a/Zelda no Densetsu - Kamigami no Triforce (Japan).json b/Zelda no Densetsu - Kamigami no Triforce (Japan).json new file mode 100644 index 0000000..536e3ec --- /dev/null +++ b/Zelda no Densetsu - Kamigami no Triforce (Japan).json @@ -0,0 +1,64 @@ +[ + { + "address": 32768, + "label": "ResetVector", + "comment": null, + "flags": [] + }, + { + "address": 32820, + "label": "MainGameLoop", + "comment": null, + "flags": [] + }, + { + "address": 32949, + "label": "JumpToGameMode", + "comment": null, + "flags": [] + }, + { + "address": 165840, + "label": null, + "comment": null, + "flags": [ + { + "flagType": "JslTableRoutine", + "entries": 4 + } + ] + }, + { + "address": 32966, + "label": null, + "comment": null, + "flags": [ + { + "flagType": "JmpIndirectLongInterleavedTable", + "start": 32865, + "entries": 28 + } + ] + }, + { + "address": 835861, + "label": null, + "comment": null, + "flags": [ + { + "flagType": "JslTableRoutine", + "entries": 12 + } + ] + }, + { + "address": 34716, + "label": null, + "comment": null, + "flags": [ + { + "flagType": "NonReturningRoutine" + } + ] + } +] \ No newline at end of file diff --git a/pom.xml b/pom.xml index 2ea31ea..736c4a8 100644 --- a/pom.xml +++ b/pom.xml @@ -22,6 +22,26 @@ + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + + + com.fasterxml.jackson.module + jackson-module-kotlin + ${jackson.version} + org.glassfish.jersey.containers jersey-container-grizzly2-http @@ -134,5 +154,6 @@ 2.27 UTF-8 1.3.11 + 2.9.8 diff --git a/src/main/java/com/smallhacker/disbrowser/Grid.kt b/src/main/java/com/smallhacker/disbrowser/Grid.kt index 328e359..b4600ec 100644 --- a/src/main/java/com/smallhacker/disbrowser/Grid.kt +++ b/src/main/java/com/smallhacker/disbrowser/Grid.kt @@ -59,28 +59,34 @@ class Grid { .first() } - fun add(ins: Instruction, metadata: MetadataLine?, disassembly: Disassembly) { - val address = ins.address + fun add(ins: CodeUnit, metadata: Metadata, disassembly: Disassembly) { + val insMetadata = ins.address ?.let { metadata[it] } + + val actualAddress = ins.address + val presentedAddress = ins.presentedAddress if (nextAddress != null) { - if (address != nextAddress) { + if (presentedAddress != nextAddress) { addDummy() } } - nextAddress = ins.postState.address + nextAddress = ins.nextPresentedAddress val y = (height++) - addresses[address] = y + addresses[presentedAddress] = y add(y, ins.address, - text(ins.address.toFormattedString()), + text(actualAddress?.toFormattedString() ?: ""), text(ins.bytesToString()), - text(metadata?.label?.plus(":") ?: ""), + input(value = insMetadata?.label ?: "") + .addClass("field-label") + .addClass("field-editable") + .attr("data-field", "label") + .attr("data-address", presentedAddress.value.toString()), fragment { - text("${ins.opcode.mnemonic}") - text(ins.lengthSuffix) + text(ins.printOpcodeAndSuffix()) text(" ") - val operands = ins.opcode.mode.print(ins) + var operands = ins.printOperands() val link = ins.linkedState if (link == null) { @@ -93,13 +99,19 @@ class Grid { else -> "/${link.address.toSimpleString()}/${link.urlString}" } + operands = metadata[link.address]?.label ?: operands a { text(operands) }.attr("href", url) } }, - text(ins.postState.toString()) + text(ins.postState?.toString() ?: ""), + input(value = insMetadata?.comment ?: "") + .addClass("field-comment") + .addClass("field-editable") + .attr("data-field", "comment") + .attr("data-address", presentedAddress.value.toString()) ) if (ins.opcode.continuation == Continuation.NO) { @@ -109,7 +121,7 @@ class Grid { private fun addDummy() { val y = (height++) - add(y, null, null, null, null, text("..."), null) + add(y, null, null, null, null, text("..."), null, null) } private fun add(y: Int, address: Address?, @@ -117,7 +129,8 @@ class Grid { cBytes: HtmlNode?, cLabel: HtmlNode?, cCode: HtmlNode?, - cState: HtmlNode? + cState: HtmlNode?, + cComment: HtmlNode? ) { if (address != null) { rowId[y] = address.toSimpleString() @@ -127,9 +140,15 @@ class Grid { content[2 to y] = cLabel content[3 to y] = cCode content[4 to y] = cState + content[5 to y] = cComment } fun output(): HtmlNode { + val contentMaxX = content.keys.asSequence() + .map { it.first } + .max() + ?: -1 + return table { for (y in 0 until height) { tr { @@ -145,7 +164,7 @@ class Grid { arrowCells[x to y]?.appendTo(parent) }.addClass(cssClass) } - for (x in 4..4) { + for (x in 4..contentMaxX) { val cssClass = cellClasses[x to y] td { content[x to y]?.appendTo(parent) diff --git a/src/main/java/com/smallhacker/disbrowser/Html.kt b/src/main/java/com/smallhacker/disbrowser/Html.kt index 44cce89..3986e2b 100644 --- a/src/main/java/com/smallhacker/disbrowser/Html.kt +++ b/src/main/java/com/smallhacker/disbrowser/Html.kt @@ -107,6 +107,8 @@ fun a(inner: InnerHtml = {}) = parent("a", inner) fun HtmlArea.a(inner: InnerHtml = {}) = com.smallhacker.disbrowser.a(inner).appendTo(parent) fun script(inner: InnerHtml = {}) = parent("script", inner) fun HtmlArea.script(inner: InnerHtml = {}) = com.smallhacker.disbrowser.script(inner).appendTo(parent) +fun input(type: String = "text", value: String = "") = leaf("input").attr("type", type).attr("value", value) +fun HtmlArea.input(type: String = "text", value: String = "") = com.smallhacker.disbrowser.input(type, value).appendTo(parent) fun HtmlNode.appendTo(node: HtmlNode) = apply { node.append(this) } fun HtmlNode.addClass(c: String?) = attrAdd("class", c) diff --git a/src/main/java/com/smallhacker/disbrowser/Main.java b/src/main/java/com/smallhacker/disbrowser/Main.java index 6e177c4..b790f02 100644 --- a/src/main/java/com/smallhacker/disbrowser/Main.java +++ b/src/main/java/com/smallhacker/disbrowser/Main.java @@ -21,8 +21,8 @@ public class Main { */ public static HttpServer startServer() { // create a resource config that scans for JAX-RS resources and providers - // in com.smallhacker.disbrowser package - final ResourceConfig rc = new ResourceConfig().packages("com.smallhacker.disbrowser"); + // in com.smallhacker.disbrowser.resource package + final ResourceConfig rc = new ResourceConfig().packages("com.smallhacker.disbrowser.resource"); // create and start a new instance of grizzly http server // exposing the Jersey application at BASE_URI diff --git a/src/main/java/com/smallhacker/disbrowser/MyResource.kt b/src/main/java/com/smallhacker/disbrowser/MyResource.kt deleted file mode 100644 index c6eff6b..0000000 --- a/src/main/java/com/smallhacker/disbrowser/MyResource.kt +++ /dev/null @@ -1,159 +0,0 @@ -package com.smallhacker.disbrowser - -import com.smallhacker.disbrowser.asm.* -import com.smallhacker.disbrowser.disassembler.Disassembler -import com.smallhacker.disbrowser.util.tryParseInt -import java.nio.file.Paths -import javax.ws.rs.GET -import javax.ws.rs.Path -import javax.ws.rs.PathParam -import javax.ws.rs.Produces -import javax.ws.rs.core.MediaType -import javax.ws.rs.core.Response - -private val RESET_VECTOR_LOCATION = Address(0x00_FFFC) - -@Path("/") -class MyResource { - private val romData = lazy { - //RomData.load(Paths.get("P:\\Emulation\\ROMs\\SNES\\Zelda no Densetsu - Kamigami no Triforce (Japan).sfc")) - RomData.load(Paths.get("""P:\Emulation\ROMs\SNES\Super Mario World.sfc""")) - } - - @GET - fun getIt(): Response { - return handle { - val resetVectorLocation = RESET_VECTOR_LOCATION - val initialAddress = Address(romData.value.getWord(resetVectorLocation.pc).value) - val flags = parseState("MX") - showDisassembly(initialAddress, flags) - } - } - - @GET - @Path("{address}") - fun getIt(@PathParam("address") address: String): Response { - return handle { - parseAddress(address)?.let { - val flags = parseState("") - showDisassembly(it, flags) - } - } - } - - - @GET - @Path("{address}/{state}") - @Produces(MediaType.TEXT_HTML) - fun getIt(@PathParam("address") address: String, @PathParam("state") state: String): Response { - return handle { - parseAddress(address)?.let { - val flags = parseState(state) - showDisassembly(it, flags) - } - } - } - - private fun parseAddress(address: String): Address? { - return tryParseInt(address, 16) - ?.let { Address(it) } - } - - private fun handle(runner: () -> HtmlNode?): Response { - try { - val disassembly = runner() - - return if (disassembly == null) - Response.status(404).build() - else - Response.ok(disassembly.toString(), MediaType.TEXT_HTML).build() - } catch (e: Exception) { - e.printStackTrace() - throw e - } - } - - private fun showDisassembly(initialAddress: Address, flags: VagueNumber): HtmlNode? { - val rom = romData.value - - val metadata = getMetadata() - - val initialState = State(data = rom, address = initialAddress, flags = flags) - val disassembly = Disassembler.disassemble(initialState, metadata, false) - - return print(disassembly, metadata) - } - - private fun parseState(state: String): VagueNumber { - var flags = VagueNumber() - flags = parseFlag(state, flags, 'M', 'm', 0x20) - flags = parseFlag(state, flags, 'X', 'x', 0x10) - return flags - } - - @GET - @Path("resources/{file}.{ext}") - fun getCss(@PathParam("file") file: String, @PathParam("ext") ext: String): Response { - val mime = when (ext) { - "js" -> "application/javascript" - "css" -> "text/css" - else -> null - } - - if (mime != null) { - javaClass.getResourceAsStream("/$file.$ext") - ?.bufferedReader() - ?.use { - return Response.ok(it.readText()) - .type(mime) - .build() - } - } - - return Response.status(404).build() - } - - - private fun parseFlag(state: String, flags: VagueNumber, set: Char, clear: Char, mask: Int): VagueNumber = when { - state.contains(set) -> flags.withBits(mask) - state.contains(clear) -> flags.withoutBits(mask) - else -> flags - } - - private fun print(disassembly: Disassembly, metadata: Metadata): HtmlNode { - val grid = Grid() - disassembly.forEach { grid.add(it, metadata[it.address], disassembly) } - disassembly.asSequence() - .mapNotNull { - it.linkedState - ?.let { link -> - it.address to link.address - } - } - .sortedBy { Math.abs(it.first.value - it.second.value) } - .forEach { grid.arrow(it.first, it.second) } - - return html { - head { - title { text("Disassembly Browser") } - link {}.attr("rel", "stylesheet").attr("href", "/resources/style.css") - } - body { - grid.output().appendTo(parent) - script().attr("src", "/resources/main.js") - } - } - } - - private fun getMetadata(): Metadata { - return metadata { - at(0x00_8000) { label = "RESET_VECTOR" } - at(0x00_80c6) { flags.add(JmpIndirectLongInterleavedTable(Address(0x00_8061), 28)) } - at(0x00_879c) { flags.add(NonReturningRoutine) } - at(0x02_87d0) { flags.add(JslTableRoutine(4)) } - at(0x0c_c115) { flags.add(JslTableRoutine(12)) } - //at(0x0c_c115) { stop = true } - } - } -} - diff --git a/src/main/java/com/smallhacker/disbrowser/Service.kt b/src/main/java/com/smallhacker/disbrowser/Service.kt new file mode 100644 index 0000000..bade567 --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/Service.kt @@ -0,0 +1,78 @@ +package com.smallhacker.disbrowser + +import com.smallhacker.disbrowser.asm.* +import com.smallhacker.disbrowser.disassembler.Disassembler +import com.smallhacker.disbrowser.util.jsonFile +import com.smallhacker.disbrowser.util.toUInt24 +import java.nio.file.Paths +import kotlin.reflect.KMutableProperty1 + +private val RESET_VECTOR_LOCATION = address(0x00_FFFC) + +object Service { + private val romName = "Zelda no Densetsu - Kamigami no Triforce (Japan)" + private val romDir = Paths.get("""P:\Emulation\ROMs\SNES""") + private val metaDir = Paths.get("""P:\Programming\dis-browser""") + private val metaFile = jsonFile(metaDir.resolve("$romName.json")) + private val metadata by lazy { metaFile.load() } + + private val romData = lazy { + val path = romDir.resolve("$romName.sfc") + RomData.load(path) + } + + fun showDisassemblyFromReset(): HtmlNode? { + val resetVectorLocation = RESET_VECTOR_LOCATION + val initialAddress = Address(romData.value.getWord(resetVectorLocation.pc).toUInt24()) + val flags = VagueNumber(0x30u) + return showDisassembly(initialAddress, flags) + } + + fun showDisassembly(initialAddress: Address, flags: VagueNumber): HtmlNode? { + val rom = romData.value + + val initialState = State(data = rom, address = initialAddress, flags = flags) + val disassembly = Disassembler.disassemble(initialState, metadata, false) + + return print(disassembly, metadata) + } + + + private fun print(disassembly: Disassembly, metadata: Metadata): HtmlNode { + val grid = Grid() + disassembly.forEach { + grid.add(it, metadata, disassembly) + } + disassembly.asSequence() + .mapNotNull { + it.linkedState + ?.let { link -> + it.presentedAddress to link.address + } + } + .sortedBy { it.first distanceTo it.second } + .forEach { grid.arrow(it.first, it.second) } + + return html { + head { + title { text("Disassembly Browser") } + link {}.attr("rel", "stylesheet").attr("href", "/resources/style.css") + } + body { + grid.output().appendTo(parent) + script().attr("src", "/resources/disbrowser.js") + } + } + } + + fun updateMetadata(address: Address, field: KMutableProperty1, value: String) { + if (value.isEmpty()) { + metadata[address]?.run { + field.set(this, null) + } + } else { + field.set(metadata.getOrAdd(address), value) + } + } + +} diff --git a/src/main/java/com/smallhacker/disbrowser/asm/Address.kt b/src/main/java/com/smallhacker/disbrowser/asm/Address.kt index 02d2d43..cf37600 100644 --- a/src/main/java/com/smallhacker/disbrowser/asm/Address.kt +++ b/src/main/java/com/smallhacker/disbrowser/asm/Address.kt @@ -1,19 +1,36 @@ package com.smallhacker.disbrowser.asm -data class Address(val value: Int): Comparable
{ - val rom = (value and 0x8000) == 0 - val pc = (value and 0x7FFF) or ((value and 0x7F_0000) shr 1) +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonValue +import com.smallhacker.disbrowser.util.UInt24 +import com.smallhacker.disbrowser.util.toUInt24 + +data class Address(@JsonValue val value: UInt24): Comparable
{ + @JsonIgnore + val rom = (value and 0x8000u).toUInt() == 0u + @JsonIgnore + val pc = snesToPc(value) operator fun plus(offset: Int) = Address(value + offset) operator fun minus(offset: Int) = Address(value - offset) operator fun inc() = plus(1) - operator fun dec() = plus(1) + operator fun dec() = minus(1) override fun toString(): String = toFormattedString() - fun toFormattedString(): String = String.format("$%02x:%04x", value shr 16, value and 0xFFFF) - fun toSimpleString(): String = String.format("%06x", value) + fun toFormattedString(): String = String.format("$%02x:%04x", (value shr 16).toInt(), (value and 0xFFFFu).toInt()) + fun toSimpleString(): String = String.format("%06x", value.toInt()) - fun withinBank(value: Int): Address = Address((this.value and 0xFF_0000) or value) + fun withinBank(value: UShort): Address = Address((this.value and 0xFF_0000u) or value.toUInt24()) - override fun compareTo(other: Address) = value.compareTo(other.value) + override fun compareTo(other: Address) = value.toUInt().compareTo(other.value.toUInt()) + + infix fun distanceTo(other: Address)= Math.abs(value.toInt() - other.value.toInt()).toUInt() +} + +fun address(snesAddress: Int) = Address(snesAddress.toUInt24()) + +private fun snesToPc(value: UInt24): UInt { + // TODO: This is incredibly oversimplified. Anything that isn't a small LoROM will crash and burn + val intVal = value.toUInt() + return (intVal and 0x7FFFu) or ((intVal and 0x7F_0000u) shr 1) } diff --git a/src/main/java/com/smallhacker/disbrowser/asm/Disassembly.kt b/src/main/java/com/smallhacker/disbrowser/asm/Disassembly.kt index b9c6172..ebbe791 100644 --- a/src/main/java/com/smallhacker/disbrowser/asm/Disassembly.kt +++ b/src/main/java/com/smallhacker/disbrowser/asm/Disassembly.kt @@ -1,13 +1,20 @@ package com.smallhacker.disbrowser.asm -class Disassembly(lines: List) : Iterable { - override fun iterator() = lines.values.iterator() as Iterator +class Disassembly(lines: List) : Iterable { + override fun iterator() = lineList.iterator() as Iterator - private val lines = LinkedHashMap() + private val knownAddresses = HashSet
() + private val lineList = ArrayList() init { - lines.forEach { this.lines[it.address] = it } + lines.forEach { + val address = it.address + if (address != null) { + knownAddresses += address + } + lineList.add(it) + } } - operator fun contains(address: Address) = address in lines + operator fun contains(address: Address) = address in knownAddresses } \ No newline at end of file diff --git a/src/main/java/com/smallhacker/disbrowser/asm/Instruction.kt b/src/main/java/com/smallhacker/disbrowser/asm/Instruction.kt index 823eb79..164cd79 100644 --- a/src/main/java/com/smallhacker/disbrowser/asm/Instruction.kt +++ b/src/main/java/com/smallhacker/disbrowser/asm/Instruction.kt @@ -2,86 +2,107 @@ package com.smallhacker.disbrowser.asm import com.smallhacker.disbrowser.util.* -class Instruction(val bytes: RomData, val opcode: Opcode, val preState: State) { - val address: Address - get() = preState.address +interface CodeUnit { + val address: Address? + val relativeAddress: Address + val presentedAddress: Address + val nextPresentedAddress: Address - val postState = opcode.mutate(this) - .mutateAddress { it + bytes.size } + val linkedState: State? + val preState: State? + val postState: State? + + val bytes: RomData + val opcode: Opcode + val lengthSuffix: String? + + fun operandByte(index: UInt): UByte = bytes[opcode.operandIndex + index] + fun printOpcodeAndSuffix(): String { + val mnemonic = opcode.mnemonic.displayName + val suffix = lengthSuffix ?: "" + return "$mnemonic$suffix" + } + fun printOperands() = opcode.mode.print(this) + + fun bytesToString(): String { + return bytes.asSequence() + .map { toHex(it.toUInt(), 1u) } + .joinToString(" ") + .padEnd(11, ' ') + } + + val operandLength: UInt? + + val signedByte get() = byte.toByte() + + val signedWord get() = word.toShort() + + val byte get() = operandByte(0u) + + val byte2 get() = operandByte(1u) + + val word get() = joinBytes(operandByte(0u), operandByte(1u)).toUShort() + + val long get() = joinBytes(operandByte(0u), operandByte(1u), operandByte(2u)).toUInt24() + + val value + get() = when (operandLength?.toInt()) { + 0 -> 0u + 1 -> byte.toUInt() + 2 -> word.toUInt() + 3 -> long.toUInt() + else -> null + } +} + +class DataBlock( + override val opcode: Opcode, + override val bytes: RomData, + override val presentedAddress: Address, + override val relativeAddress: Address, + override val linkedState: State? +) : CodeUnit { + override val nextPresentedAddress: Address + get() = presentedAddress + operandLength.toInt() + override val operandLength get() = bytes.size + + override val address: Address? = null + override val preState: State? = null + override val postState: State? = null + override val lengthSuffix: String? = null +} + +class Instruction(override val bytes: RomData, override val opcode: Opcode, override val preState: State) : CodeUnit { + override val address: Address get() = preState.address + override val relativeAddress get() = address + override val presentedAddress get() = address + override val nextPresentedAddress get() = postState.address + + override val postState = opcode.mutate(this) + .mutateAddress { it + bytes.size.toInt() } .withOrigin(this) - val linkedState = link()?.let { link -> + override val linkedState = link()?.let { link -> opcode.mutate(this) .mutateAddress { link } .withOrigin(this) } - operator fun get(index: Int): UByte { - return bytes[index] - } - - val signedByte: Int - get() = byte.value.toByte().toInt() - - val signedWord: Int - get() = word.value.toShort().toInt() - - val byte: UByte - get() = get(1) - - val byte2: UByte - get() = get(2) - - val dataByte: UByte - get() = get(0) - - val word: UWord - get() = (get(2).toWord() left 8) or get(1).toWord() - - val dataWord: UWord - get() = (get(1).toWord() left 8) or get(0).toWord() - - val long: ULong - get() = (get(3).toLong() left 16) or (get(2).toLong() left 8) or get(1).toLong() - - val dataLong: ULong - get() = (get(2).toLong() left 16) or (get(1).toLong() left 8) or get(0).toLong() - - fun bytesToString(): String { - return bytes.asSequence().map { it.toHex() }.joinToString(" ").padEnd(11, ' ') - } - - //val value: ULong - // get() { - // val len = operandLength - // val start = opcode.operandIndex - - //} - - //fun getOperand(index: Int, length: Int) { - // val v = uLong(0) - // for (i in (index + length) downTo index) - //} - - val lengthSuffix: String + override val lengthSuffix: String? get() { - return when (opcode.mode) { - Mode.IMPLIED -> "" - Mode.IMMEDIATE_8 -> "" - Mode.IMMEDIATE_16 -> "" - Mode.RELATIVE -> "" - Mode.RELATIVE_LONG -> "" - Mode.BLOCK_MOVE -> "" - else -> when (operandLength) { - null -> ".?" - 1 -> ".b" - 2 -> ".w" - 3 -> ".l" - else -> "" - } + if (!opcode.mode.showLengthSuffix) { + return null + } + + return when (operandLength?.toInt()) { + null -> ".?" + 1 -> ".b" + 2 -> ".w" + 3 -> ".l" + else -> null } } - private val operandLength + override val operandLength get() = opcode.mode.operandLength(preState) private fun link(): Address? { @@ -90,17 +111,17 @@ class Instruction(val bytes: RomData, val opcode: Opcode, val preState: State) { } return when (opcode.mode) { - Mode.ABSOLUTE -> address.withinBank(word.value) - Mode.ABSOLUTE_LONG -> Address(long.value) - Mode.RELATIVE -> address + 2 + signedByte - Mode.RELATIVE_LONG -> address + 3 + signedWord - Mode.DATA_WORD -> address.withinBank(dataWord.value) - Mode.DATA_LONG -> Address(dataLong.value) + Mode.ABSOLUTE -> relativeAddress.withinBank(word) + Mode.ABSOLUTE_LONG -> Address(long) + Mode.RELATIVE -> relativeAddress + 2 + signedByte.toInt() + Mode.RELATIVE_LONG -> relativeAddress + 3 + signedWord.toInt() + Mode.DATA_WORD -> relativeAddress.withinBank(word) + Mode.DATA_LONG -> Address(long) else -> null } } override fun toString(): String { - return "$address ${bytesToString()} ${opcode.mnemonic} ${opcode.mode.print(this).padEnd(100, ' ')} ($preState -> $postState)" + return "$address ${bytesToString()} ${opcode.mnemonic.displayName} ${opcode.mode.print(this).padEnd(100, ' ')} ($preState -> $postState)" } } diff --git a/src/main/java/com/smallhacker/disbrowser/asm/Metadata.kt b/src/main/java/com/smallhacker/disbrowser/asm/Metadata.kt index 971c71c..f1bc10c 100644 --- a/src/main/java/com/smallhacker/disbrowser/asm/Metadata.kt +++ b/src/main/java/com/smallhacker/disbrowser/asm/Metadata.kt @@ -1,12 +1,21 @@ package com.smallhacker.disbrowser.asm -import com.smallhacker.disbrowser.util.left -import com.smallhacker.disbrowser.util.or -import com.smallhacker.disbrowser.util.toLong +import com.fasterxml.jackson.annotation.* +import com.fasterxml.jackson.annotation.JsonSubTypes.Type +import com.smallhacker.disbrowser.util.joinBytes +import com.smallhacker.disbrowser.util.toUInt24 -class Metadata { +class Metadata() { private val lines = HashMap() + @JsonValue + private fun linesAsList() = lines.values.toList() + + @JsonCreator + private constructor(@JsonProperty lines: List) : this() { + lines.forEach { add(it) } + } + fun add(line: MetadataLine): Metadata { lines[line.address] = line return this @@ -15,10 +24,20 @@ class Metadata { operator fun get(address: Address): MetadataLine? { return lines[address] } + + fun getOrAdd(address: Address): MetadataLine { + val line = this[address] + if (line != null) { + return line + } + val newLine = MetadataLine(address) + add(newLine) + return newLine + } } fun Metadata.at(address: Int, runner: MetadataLine.() -> Unit) { - val line = MetadataLine(Address(address)) + val line = MetadataLine(address(address)) this.add(line) runner(line) } @@ -29,38 +48,77 @@ fun metadata(runner: Metadata.() -> Unit): Metadata { return metadata } +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "flagType") +@JsonSubTypes( + Type(value = NonReturningRoutine::class, name = "NonReturningRoutine"), + Type(value = JmpIndirectLongInterleavedTable::class, name = "JmpIndirectLongInterleavedTable"), + Type(value = JslTableRoutine::class, name = "JslTableRoutine") +) interface InstructionFlag -object NonReturningRoutine : InstructionFlag - -class JmpIndirectLongInterleavedTable(private val start: Address, private val entries: Int) : InstructionFlag { - fun readTable(data: RomData): Sequence
{ - return (0 until entries) - .asSequence() - .map { it + start.pc } - .map { pc -> data[pc].toLong() or (data[pc + entries].toLong() left 8) or (data[pc + entries + entries].toLong() left 16) } - .map { Address(it.value) } - - } +object NonReturningRoutine : InstructionFlag { + override fun toString() = "NonReturningRoutine" } -class JslTableRoutine(private val entries: Int) : InstructionFlag { - fun readTable(postJsr: State): Sequence
{ - val data = postJsr.data - return (0 until entries) +class JmpIndirectLongInterleavedTable @JsonCreator constructor( + @field:JsonProperty @JsonProperty private val start: Address, + @field:JsonProperty @JsonProperty private val entries: Int +) : InstructionFlag { + private val uEntries get() = entries.toUInt() + + fun readTable(data: RomData): Sequence
{ + return (0u until uEntries) .asSequence() - .map { postJsr.address.pc + (it * 3) } - .map { pc -> data[pc].toLong() or (data[pc + 1].toLong() left 8) or (data[pc + 2].toLong() left 16) } - .map { Address(it.value) } + .map { it + start.pc } + .map { pc -> joinBytes(data[pc], data[pc + uEntries], data[pc + uEntries + uEntries]).toUInt24() } + .map { Address(it) } } -// fun dataInstructions(postJsr: State) { -// val data = postJsr.data -// return (0 until entries) -// .asSequence() -// .map { -// val offset = it * 3 -// Instruction(data.range(postJsr.address.pc + offset, 3), Opcode.POINTER_LONG, postJsr.mutateAddress { it + offset }) -// } -// } + fun generateCode(jumpInstruction: Instruction): Sequence { + val table = jumpInstruction.preState.data.deinterleave(uEntries, + start.pc, + (start + entries).pc, + (start + (2u * uEntries).toInt()).pc + ) + + return (0u until uEntries) + .asSequence() + .map { index -> index * 3u } + .map { offset -> + val target = table.getLong(offset) + + DataBlock( + Opcode.POINTER_LONG, + table.range(offset, 3u), + jumpInstruction.postState.address + offset.toInt(), + jumpInstruction.relativeAddress, + jumpInstruction.opcode.mutate(jumpInstruction) + .mutateAddress { Address(target) } + .withOrigin(jumpInstruction) + ) +// Instruction( +// data.range((start.value + offset).toUInt(), 3u), +// Opcode.POINTER_LONG, +// preState.mutateAddress { start -> start + offset.toInt() } +// ) + } + } + + override fun toString() = "JmpIndirectLongInterleavedTable($start, $entries)" +} + +class JslTableRoutine @JsonCreator constructor( + @field:JsonProperty @JsonProperty private val entries: Int +) : InstructionFlag { + + fun readTable(postJsr: State): Sequence
{ + val data = postJsr.data + return (0u until entries.toUInt()) + .asSequence() + .map { postJsr.address.pc + (it * 3u) } + .map { pc -> joinBytes(data[pc], data[pc + 1u], data[pc + 2u]).toUInt24() } + .map { Address(it) } + } + + override fun toString() = "JslTableRoutine($entries)" } \ No newline at end of file diff --git a/src/main/java/com/smallhacker/disbrowser/asm/MetadataLine.kt b/src/main/java/com/smallhacker/disbrowser/asm/MetadataLine.kt index 4fbe15d..0e21225 100644 --- a/src/main/java/com/smallhacker/disbrowser/asm/MetadataLine.kt +++ b/src/main/java/com/smallhacker/disbrowser/asm/MetadataLine.kt @@ -4,5 +4,6 @@ data class MetadataLine( val address: Address, var label: String? = null, var comment: String? = null, + var preComment: String? = null, val flags: MutableList = ArrayList() -) \ No newline at end of file +) diff --git a/src/main/java/com/smallhacker/disbrowser/asm/Mnemonic.kt b/src/main/java/com/smallhacker/disbrowser/asm/Mnemonic.kt index 3706785..c6811d4 100644 --- a/src/main/java/com/smallhacker/disbrowser/asm/Mnemonic.kt +++ b/src/main/java/com/smallhacker/disbrowser/asm/Mnemonic.kt @@ -1,6 +1,6 @@ package com.smallhacker.disbrowser.asm -enum class Mnemonic { +enum class Mnemonic(private val nameOverride: String? = null) { ADC, AND, ASL, BCC, BCS, BEQ, BIT, BMI, BNE, BPL, BRA, BRK, BRL, BVC, BVS, CLC, CLD, CLI, CLV, CMP, COP, CPX, CPY, DEC, DEX, DEY, EOR, INC, INX, INY, JMP, JML, JSL, @@ -11,5 +11,7 @@ enum class Mnemonic { TCS, TDC, TRB, TSB, TSC, TSX, TXA, TXS, TXY, TYA, TYX, WAI, WDM, XBA, XCE, - DB, DW, DL + DB(".db"), DW(".dw"), DL(".dl"); + + val displayName get() = nameOverride ?: name } diff --git a/src/main/java/com/smallhacker/disbrowser/asm/Mode.kt b/src/main/java/com/smallhacker/disbrowser/asm/Mode.kt index fe051c0..dae4d9e 100644 --- a/src/main/java/com/smallhacker/disbrowser/asm/Mode.kt +++ b/src/main/java/com/smallhacker/disbrowser/asm/Mode.kt @@ -2,76 +2,112 @@ package com.smallhacker.disbrowser.asm import com.smallhacker.disbrowser.util.* -fun format(format: String, value: UVal<*>): String { - return format.replace(Regex("[0]+"), value.toHex()) -} +private val ZEROES = Regex("[0]+") +private fun countBytes(format: String) = (ZEROES.find(format)?.groupValues?.firstOrNull()?.length?.toUInt() ?: 0u) / 2u + +private const val ACCUMULATOR_SIZE = -1 +private const val INDEX_SIZE = -2 + +fun format(format: String, value: UInt, operandBytes: UInt = countBytes(format)) = + format.replace(ZEROES, toHex(value, operandBytes)) enum class Mode { - DATA_BYTE(1, "$00", { dataByte }, dataMode = true), - DATA_WORD(2, "$0000", { dataWord }, dataMode = true), - DATA_LONG(3, "$000000", { dataLong }, dataMode = true), + DATA_BYTE("$00", dataMode = true, showLengthSuffix = false), + DATA_WORD("$0000", dataMode = true, showLengthSuffix = false), + DATA_LONG("$000000", dataMode = true, showLengthSuffix = false), - IMPLIED(1, { "" }), - IMMEDIATE_8(2, "#$00", { byte }), - IMMEDIATE_16(3, "#$0000", { word }), - IMMEDIATE_M(-1, { - when (preState.m) { - null -> "????" - true -> format("#$00", byte) - false -> format("#$0000", word) - } - }), - IMMEDIATE_X(-2, { - when (preState.x) { - null -> "???" - true -> format("#$00", byte) - false -> format("#$0000", word) - } - }), - ABSOLUTE(3, "$0000", { word }), - ABSOLUTE_X(3, "$0000,x", { word }), - ABSOLUTE_Y(3, "$0000,y", { word }), - ABSOLUTE_LONG(4, "$000000", { long }), - ABSOLUTE_LONG_X(4, "$000000,x", { long }), - ABSOLUTE_INDIRECT(3, "($0000)", { word }), - ABSOLUTE_INDIRECT_LONG(3, "[$0000]", { word }), - ABSOLUTE_X_INDIRECT(3, "($0000,x)", { word }), - DIRECT(2, "$00", { byte }), - DIRECT_X(2, "$00,x", { byte }), - DIRECT_Y(2, "$00,y", { byte }), - DIRECT_S(2, "$00,s", { byte }), - DIRECT_INDIRECT(2, "($00)", { byte }), - DIRECT_INDIRECT_Y(2, "($00),y", { byte }), - DIRECT_X_INDIRECT(2, "($00,x)", { byte }), - DIRECT_S_INDIRECT_Y(2, "($00,s),y", { byte }), - DIRECT_INDIRECT_LONG(2, "[$00]", { byte }), - DIRECT_INDIRECT_LONG_Y(2, "[$00],y", { byte }), - RELATIVE(2, { - val rel = signedByte.toInt() + 2 - format("$000000", uLong((address + rel).value)) - }), - //RELATIVE_LONG(3, "$0000", { word }), - RELATIVE_LONG(3, { - val rel = signedWord.toInt() + 3 - format("$000000", uLong((address + rel).value)) - }), - BLOCK_MOVE(3, { String.format("#$%02x,#$%02x", byte.value, byte2.value) }) + IMPLIED("", showLengthSuffix = false), + IMMEDIATE_8("#$00", showLengthSuffix = false), + IMMEDIATE_16("#$0000", showLengthSuffix = false), + ABSOLUTE("$0000"), + ABSOLUTE_X("$0000,x"), + ABSOLUTE_Y("$0000,y"), + ABSOLUTE_LONG("$000000"), + ABSOLUTE_LONG_X("$000000,x"), + ABSOLUTE_INDIRECT("($0000)"), + ABSOLUTE_INDIRECT_LONG("[$0000]"), + ABSOLUTE_X_INDIRECT("($0000,x)"), + DIRECT("$00"), + DIRECT_X("$00,x"), + DIRECT_Y("$00,y"), + DIRECT_S("$00,s"), + DIRECT_INDIRECT("($00)"), + DIRECT_INDIRECT_Y("($00),y"), + DIRECT_X_INDIRECT("($00,x)"), + DIRECT_S_INDIRECT_Y("($00,s),y"), + DIRECT_INDIRECT_LONG("[$00]"), + DIRECT_INDIRECT_LONG_Y("[$00],y"), + + IMMEDIATE_M( + operandLength = ACCUMULATOR_SIZE, + print = { + when (preState?.m) { + null -> "????" + true -> format("#$00", byte.toUInt()) + false -> format("#$0000", word.toUInt()) + } + } + ), + IMMEDIATE_X( + operandLength = INDEX_SIZE, + print = { + when (preState?.x) { + null -> "???" + true -> format("#$00", byte.toUInt()) + false -> format("#$0000", word.toUInt()) + } + } + ), + + RELATIVE( + format = "$000000", + operandLength = 1u, + valueGetter = { + val rel = signedByte.toInt() + 2 + (relativeAddress + rel).value.toUInt() + }, + showLengthSuffix = false + ), + RELATIVE_LONG( + format = "$000000", + operandLength = 2u, + valueGetter = { + val rel = signedWord.toInt() + 3 + (relativeAddress + rel).value.toUInt() + }, + showLengthSuffix = false + ), + BLOCK_MOVE( + operandLength = 2, + print = { String.format("#$%02x,#$%02x", byte.toInt(), byte2.toInt()) }, + showLengthSuffix = false + ) ; - private val length: Int - val print: Instruction.() -> String + private val operandLength: Int + val print: CodeUnit.() -> String val dataMode: Boolean + val showLengthSuffix: Boolean - constructor(length: Int, print: Instruction.() -> String) { - this.length = length + constructor(operandLength: Int, print: CodeUnit.() -> String, showLengthSuffix: Boolean = true) { + this.operandLength = operandLength this.print = print this.dataMode = false + this.showLengthSuffix = showLengthSuffix } - constructor(length: Int, format: String, valueGetter: Instruction.() -> UVal<*>, dataMode: Boolean = false) { - this.length = length - this.print = { format(format, valueGetter(this)) } + constructor( + format: String, + printedLength: UInt = countBytes(format), + operandLength: UInt = printedLength, + valueGetter: CodeUnit.() -> UInt = { value!! }, + dataMode: Boolean = false, + showLengthSuffix: Boolean = true + ) { + this.operandLength = operandLength.toInt() + this.print = { format(format, valueGetter(this), printedLength) } this.dataMode = dataMode + this.showLengthSuffix = showLengthSuffix } /** @@ -82,12 +118,10 @@ enum class Mode { * * If the length cannot be determined based on the current [State], `null` is returned. */ - fun instructionLength(state: State): Int? { - return when (length) { - -1 -> state.mWidth?.plus(1) - -2 -> state.xWidth?.plus(1) - else -> length - } + fun instructionLength(state: State): UInt? { + val operatorLength = if (this.dataMode) 0u else 1u + return operandLength(state) + ?.plus(operatorLength) } /** @@ -98,14 +132,11 @@ enum class Mode { * * If the length cannot be determined based on the current [State], `null` is returned. */ - fun operandLength(state: State): Int? { - val len = instructionLength(state) ?: return null - - return when(this) { - DATA_BYTE, - DATA_WORD, - DATA_LONG -> len - else -> len - 1 + fun operandLength(state: State): UInt? { + return when (operandLength) { + ACCUMULATOR_SIZE -> state.mWidth + INDEX_SIZE -> state.xWidth + else -> operandLength.toUInt() } } } diff --git a/src/main/java/com/smallhacker/disbrowser/asm/ModeFormat.kt b/src/main/java/com/smallhacker/disbrowser/asm/ModeFormat.kt deleted file mode 100644 index 6a3b7db..0000000 --- a/src/main/java/com/smallhacker/disbrowser/asm/ModeFormat.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.smallhacker.disbrowser.asm - -abstract class ModeFormat { - abstract fun print(instruction: Instruction, metadata: Metadata): String - - //fun wrap(prefix: String = "", suffix: String = ""): ModeFormat { - // - //} -} - -//private class AddressModeFormat: ModeFormat() { -// override fun print(instruction: Instruction, metadata: Metadata): String { -// val mode = instruction.opcode.mode -// val operandLength = mode.operandLength(instruction.preState) -// instruction. -// -// TODO("not implemented") //To change body of created functions use File | Settings | File Templates. -// } -//} \ No newline at end of file diff --git a/src/main/java/com/smallhacker/disbrowser/asm/Opcode.kt b/src/main/java/com/smallhacker/disbrowser/asm/Opcode.kt index 91055f9..a9afe1b 100644 --- a/src/main/java/com/smallhacker/disbrowser/asm/Opcode.kt +++ b/src/main/java/com/smallhacker/disbrowser/asm/Opcode.kt @@ -4,7 +4,7 @@ import java.util.HashMap import com.smallhacker.disbrowser.asm.Mnemonic.* import com.smallhacker.disbrowser.asm.Mode.* -import com.smallhacker.disbrowser.util.UByte +import com.smallhacker.disbrowser.util.OldUByte typealias SegmentEnder = Instruction.() -> SegmentEnd? @@ -14,7 +14,7 @@ class Opcode private constructor(val mnemonic: Mnemonic, val mode: Mode, val end private var _branch = false val operandIndex - get() = if (mode.dataMode) 0 else 1 + get() = if (mode.dataMode) 0u else 1u val continuation: Continuation get() = _continuation @@ -57,7 +57,7 @@ class Opcode private constructor(val mnemonic: Mnemonic, val mode: Mode, val end private val OPCODES: Array fun opcode(byteValue: UByte): Opcode { - return OPCODES[byteValue.value] + return OPCODES[byteValue.toInt()] } init { @@ -135,8 +135,8 @@ class Opcode private constructor(val mnemonic: Mnemonic, val mode: Mode, val end add(0xF8, SED, IMPLIED, alwaysContinue) add(0xD8, CLD, IMPLIED, alwaysContinue) add(0xB8, CLV, IMPLIED, alwaysContinue) - add(0xE2, SEP, IMMEDIATE_8, alwaysContinue) { it.preState.sep(it.bytes[1]) } - add(0xC2, REP, IMMEDIATE_8, alwaysContinue) { it.preState.rep(it.bytes[1]) } + add(0xE2, SEP, IMMEDIATE_8, alwaysContinue) { it.preState.sep(it.bytes[1u]) } + add(0xC2, REP, IMMEDIATE_8, alwaysContinue) { it.preState.rep(it.bytes[1u]) } add(0xFB, XCE, IMPLIED, alwaysContinue) add(0xC1, CMP, DIRECT_X_INDIRECT, alwaysContinue) @@ -218,11 +218,11 @@ class Opcode private constructor(val mnemonic: Mnemonic, val mode: Mode, val end add(0xFA, PLX, IMPLIED, alwaysContinue) { it.preState.pull(it.preState.xWidth) } add(0x7A, PLY, IMPLIED, alwaysContinue) { it.preState.pull(it.preState.xWidth) } - add(0x8B, PHB, IMPLIED, alwaysContinue) { it.preState.pushUnknown(1) } - add(0xAB, PLB, IMPLIED, alwaysContinue) { it.preState.pull(1) } - add(0x0B, PHD, IMPLIED, alwaysContinue) { it.preState.pushUnknown(1) } - add(0x2B, PLD, IMPLIED, alwaysContinue) { it.preState.pull(1) } - add(0x4B, PHK, IMPLIED, alwaysContinue) { it.preState.push(it.address.value shr 16) } + add(0x8B, PHB, IMPLIED, alwaysContinue) { it.preState.pushUnknown(1u) } + add(0xAB, PLB, IMPLIED, alwaysContinue) { it.preState.pull(1u) } + add(0x0B, PHD, IMPLIED, alwaysContinue) { it.preState.pushUnknown(1u) } + add(0x2B, PLD, IMPLIED, alwaysContinue) { it.preState.pull(1u) } + add(0x4B, PHK, IMPLIED, alwaysContinue) { it.preState.push((it.address.value shr 16).toUInt()) } add(0x08, PHP, IMPLIED, alwaysContinue) { it.preState.push(it.preState.flags) } add(0x28, PLP, IMPLIED, alwaysContinue) { it.preState.pull { copy(flags = it) } } diff --git a/src/main/java/com/smallhacker/disbrowser/asm/RomData.kt b/src/main/java/com/smallhacker/disbrowser/asm/RomData.kt index 9519667..4abb5d4 100644 --- a/src/main/java/com/smallhacker/disbrowser/asm/RomData.kt +++ b/src/main/java/com/smallhacker/disbrowser/asm/RomData.kt @@ -5,40 +5,62 @@ import java.nio.file.Files import java.nio.file.Path interface RomData { - val size: Int + val size: UInt - operator fun get(address: Int): UByte + operator fun get(address: UInt): UByte - fun range(start: Int, length: Int): RomData = RomRange(this, start, length) + fun range(start: UInt, length: UInt): RomData = RomRange(this, start, length) - fun asSequence(): Sequence = (0 until size).asSequence().map { get(it) } + fun asSequence(): Sequence = (0u until size).asSequence().map { this[it] } companion object { - fun load(path: Path): RomData = Rom(Files.readAllBytes(path)) + fun load(path: Path): RomData = ArrayRomData(Files.readAllBytes(path).toUByteArray()) } } -fun RomData.getWord(address: Int): UWord = get(address).toWord() or (get(address + 1).toWord() left 8) +fun romData(vararg bytes: UByte): RomData = ArrayRomData(bytes) -private class Rom(private val bytes: ByteArray): RomData { - override val size = bytes.size +fun RomData.getWord(address: UInt) = joinBytes(this[address], this[address + 1u]).toUShort() +fun RomData.getLong(address: UInt) = joinBytes(this[address], this[address + 1u], this[address + 2u]).toUInt24() - override fun get(address: Int): UByte { - checkRange(address) - return uByte(bytes[address].toInt()) +private class ArrayRomData(private val bytes: UByteArray) : RomData { + override val size = bytes.size.toUInt() + + override fun get(address: UInt) = rangeChecked(address) { + bytes[address.toInt()] } } -private class RomRange(private val parent: RomData, private val start: Int, override val size: Int): RomData { - override fun get(address: Int): UByte { - checkRange(address) - return parent[start + address] +private class RomRange(private val parent: RomData, private val start: UInt, override val size: UInt) : RomData { + override fun get(address: UInt) = rangeChecked(address) { + parent[start + address] } } -private fun RomData.checkRange(address: Int) { - if (address < 0 || address >= size) { +private fun RomData.rangeChecked(address: UInt, action: () -> T): T { + if (address >= size) { throw IndexOutOfBoundsException("Index $address out of range: [0, $size)") } + return action() } + +private class ReindexedRomData( + private val parent: RomData, + override val size: UInt, + private val mapper: (UInt) -> UInt +) : RomData { + override fun get(address: UInt) = rangeChecked(address) { + val mapped = mapper(address) + parent[mapped] + } +} + +fun RomData.deinterleave(entries: UInt, vararg startOffsets: UInt): RomData { + val fieldCount = startOffsets.size.toUInt() + return ReindexedRomData(this, entries * fieldCount) { + val entry = it / fieldCount + val field = it.rem(fieldCount) + startOffsets[field.toInt()] + entry + } +} \ No newline at end of file diff --git a/src/main/java/com/smallhacker/disbrowser/asm/State.kt b/src/main/java/com/smallhacker/disbrowser/asm/State.kt index be2d4ea..0cb7749 100644 --- a/src/main/java/com/smallhacker/disbrowser/asm/State.kt +++ b/src/main/java/com/smallhacker/disbrowser/asm/State.kt @@ -2,17 +2,16 @@ package com.smallhacker.disbrowser.asm import com.smallhacker.disbrowser.ImmStack import com.smallhacker.disbrowser.immStack -import com.smallhacker.disbrowser.util.UByte data class State(val origin: Instruction? = null, val data: RomData, val address: Address, val flags: VagueNumber = VagueNumber(), val stack: ImmStack = immStack()) { - val m: Boolean? get() = flags.getBoolean(0x20) - val x: Boolean? get() = flags.getBoolean(0x10) + val m: Boolean? get() = flags.getBoolean(0x20u) + val x: Boolean? get() = flags.getBoolean(0x10u) - val mWidth: Int? get() = toWidth(m) - val xWidth: Int? get() = toWidth(x) + val mWidth: UInt? get() = toWidth(m) + val xWidth: UInt? get() = toWidth(x) - fun sep(i: UByte) = withFlags(flags.withBits(i.value)) - fun rep(i: UByte) = withFlags(flags.withoutBits(i.value)) + fun sep(i: UByte) = withFlags(flags.withBits(i.toUInt())) + fun rep(i: UByte) = withFlags(flags.withoutBits(i.toUInt())) fun uncertain() = withFlags(VagueNumber()) private fun withFlags(flags: VagueNumber) = copy(flags = flags) @@ -20,28 +19,28 @@ data class State(val origin: Instruction? = null, val data: RomData, val address fun mutateAddress(mutator: (Address) -> Address) = copy(address = mutator(address)) fun withOrigin(instruction: Instruction?) = copy(origin = instruction) - fun push(value: Int) = push(VagueNumber(value)) + fun push(value: UInt) = push(VagueNumber(value)) fun push(value: VagueNumber) = copy(stack = stack.push(value)) - fun pushUnknown(count: Int? = 1): State { + fun pushUnknown(count: UInt? = 1u): State { if (count == null) { return copy(stack = immStack()) } var stack = this.stack - for (i in 1..count) { + for (i in 1u..count) { stack = stack.push(VagueNumber()) } return copy(stack = stack) } fun pull() = (stack.top ?: VagueNumber()) to copy(stack = stack.pop()) - fun pull(count: Int?): State { + fun pull(count: UInt?): State { if (count == null) { return copy(stack = immStack()) } var stack = this.stack - for (i in 1..count) { + for (i in 1u..count) { stack = stack.pop() } return copy(stack = stack) @@ -61,20 +60,20 @@ data class State(val origin: Instruction? = null, val data: RomData, val address private fun stackByteToString(v: VagueNumber): String { if (v.certain) { - return String.format("%02x", v.value) + return String.format("%02x", v.value.toInt()) } - if (v.certainty == 0) { + if (v.certainty == 0u) { return "??" } val c = v.certainty - val high = (c and 0xF0) != 0 - val low = (c and 0x0F) != 0 + val high = (c and 0xF0u) != 0u + val low = (c and 0x0Fu) != 0u return StringBuilder() - .append(if (high) String.format("%x", (v.value ushr 4) and 0xF) else "?") - .append(if (low) String.format("%x", v.value and 0xF) else "?") + .append(if (high) String.format("%x", ((v.value shr 4) and 0xFu).toInt()) else "?") + .append(if (low) String.format("%x", (v.value and 0xFu).toInt()) else "?") .toString() } @@ -100,14 +99,14 @@ data class State(val origin: Instruction? = null, val data: RomData, val address return out.toString() } - private fun toWidth(flag: Boolean?): Int? = when (flag) { + private fun toWidth(flag: Boolean?): UInt? = when (flag) { null -> null - true -> 1 - false -> 2 + true -> 1u + false -> 2u } } -fun State.pushByte(value: Byte) = this.push(VagueNumber(value.toInt())) +fun State.pushByte(value: Byte) = this.push(VagueNumber(value.toUInt())) fun State.pull(consumer: State.(VagueNumber) -> State): State { val (value, state) = this.pull() return consumer(state, value) diff --git a/src/main/java/com/smallhacker/disbrowser/asm/VagueNumber.kt b/src/main/java/com/smallhacker/disbrowser/asm/VagueNumber.kt index 06f5cdd..b1539cb 100644 --- a/src/main/java/com/smallhacker/disbrowser/asm/VagueNumber.kt +++ b/src/main/java/com/smallhacker/disbrowser/asm/VagueNumber.kt @@ -1,39 +1,42 @@ package com.smallhacker.disbrowser.asm -@Suppress("DataClassPrivateConstructor") -data class VagueNumber private constructor(val value: Int, val certainty: Int){ - constructor() : this(0, 0) - constructor(value: Int) : this(value, -1) +inline class VagueNumber(private val valueAndCertainty: ULong) { + private constructor(value: UInt, certainty: UInt) : this(value.toULong() or (certainty.toULong() shl 32)) + constructor(value: UInt) : this(value, 0xFFFF_FFFFu) + constructor() : this(0u, 0u) - fun withBits(value: Int) = VagueNumber(this.value or value, this.certainty or value) - fun withoutBits(value: Int) = VagueNumber(this.value and value.inv(), this.certainty or value) + val value get() = valueAndCertainty.toUInt() + val certainty get() = (valueAndCertainty shr 32).toUInt() + + fun withBits(value: UInt) = VagueNumber(this.value or value, this.certainty or value) + fun withoutBits(value: UInt) = VagueNumber(this.value and value.inv(), this.certainty or value) val certain: Boolean - get() = certainty == -1 + get() = certainty == 0xFFFF_FFFFu - fun get(mask: Int): Int? { + fun get(mask: UInt): UInt? { if ((certainty and mask) != mask) { return null } return value and mask } - fun getBoolean(mask: Int): Boolean? { + fun getBoolean(mask: UInt): Boolean? { val value = get(mask) ?: return null return value == mask } override fun toString(): String { - var i = 1 shl 31 + var i = 1u shl 31 val out = StringBuilder() - while (i != 0) { + while (i != 0u) { val b = getBoolean(i) when (b) { true -> out.append('1') false -> out.append('0') null -> out.append('?') } - i = i ushr 1 + i = i shr 1 } return out.toString() } diff --git a/src/main/java/com/smallhacker/disbrowser/disassembler/Disassembler.kt b/src/main/java/com/smallhacker/disbrowser/disassembler/Disassembler.kt index 1f35453..66a1a8f 100644 --- a/src/main/java/com/smallhacker/disbrowser/disassembler/Disassembler.kt +++ b/src/main/java/com/smallhacker/disbrowser/disassembler/Disassembler.kt @@ -16,7 +16,7 @@ object Disassembler { } tryAdd(initialState) - val instructions = ArrayList() + val instructions = ArrayList() while (queue.isNotEmpty()) { val state = queue.remove() @@ -26,17 +26,21 @@ object Disassembler { var stop = (ins.opcode.continuation == Continuation.NO) or (ins.opcode.mode.instructionLength(state) == null) - metadata[ins.address]?.flags?.forEach { - if (it is JmpIndirectLongInterleavedTable) { + metadata[ins.address]?.flags?.forEach { flag -> + if (flag is JmpIndirectLongInterleavedTable) { if (global) { - it.readTable(state.data) + flag.readTable(state.data) .map { ins.postState.copy(address = it) } .forEach { tryAdd(it) } } + + flag.generateCode(ins) + .forEach { instructions.add(it) } + stop = true - } else if (it is JslTableRoutine) { + } else if (flag is JslTableRoutine) { if (global) { - it.readTable(ins.postState) + flag.readTable(ins.postState) .map { ins.postState.copy(address = it) } .forEach { tryAdd(it) } } @@ -68,7 +72,7 @@ object Disassembler { } val instructionList = instructions - .sortedBy { it.address } + .sortedBy { it.presentedAddress } .toList() return Disassembly(instructionList) @@ -160,7 +164,7 @@ object Disassembler { private fun disassembleInstruction(state: State): Instruction { val pc = state.address.pc val opcode = Opcode.opcode(state.data[pc]) - val length = opcode.mode.instructionLength(state) ?: 1 + val length = opcode.mode.instructionLength(state) ?: 1u val bytes = state.data.range(pc, length) return Instruction(bytes, opcode, state) } diff --git a/src/main/java/com/smallhacker/disbrowser/resource/DisassemblyResource.kt b/src/main/java/com/smallhacker/disbrowser/resource/DisassemblyResource.kt new file mode 100644 index 0000000..b580484 --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/resource/DisassemblyResource.kt @@ -0,0 +1,72 @@ +package com.smallhacker.disbrowser.resource + +import com.smallhacker.disbrowser.HtmlNode +import com.smallhacker.disbrowser.Service +import com.smallhacker.disbrowser.asm.Address +import com.smallhacker.disbrowser.asm.VagueNumber +import com.smallhacker.disbrowser.util.toUInt24 +import com.smallhacker.disbrowser.util.tryParseInt +import javax.ws.rs.GET +import javax.ws.rs.Path +import javax.ws.rs.PathParam +import javax.ws.rs.Produces +import javax.ws.rs.core.MediaType +import javax.ws.rs.core.Response + +@Path("/") +class DisassemblyResource { + @GET + @Produces(MediaType.TEXT_HTML) + fun getIt() = handle { + Service.showDisassemblyFromReset() + } + + @GET + @Path("{address}") + @Produces(MediaType.TEXT_HTML) + fun getIt(@PathParam("address") address: String) = getIt(address, "") + + @GET + @Path("{address}/{state}") + @Produces(MediaType.TEXT_HTML) + fun getIt(@PathParam("address") address: String, @PathParam("state") state: String): Response { + return handle { + parseAddress(address)?.let { + val flags = parseState(state) + Service.showDisassembly(it, flags) + } + } + } + + private fun parseAddress(address: String): Address? { + return tryParseInt(address, 16) + ?.let { Address(it.toUInt24()) } + } + + private fun handle(runner: () -> HtmlNode?): Response { + try { + val disassembly = runner() + + return if (disassembly == null) + Response.status(404).build() + else + Response.ok(disassembly.toString(), MediaType.TEXT_HTML).build() + } catch (e: Exception) { + e.printStackTrace() + throw e + } + } + + private fun parseState(state: String): VagueNumber { + var flags = VagueNumber() + flags = parseFlag(state, flags, 'M', 'm', 0x20u) + flags = parseFlag(state, flags, 'X', 'x', 0x10u) + return flags + } + + private fun parseFlag(state: String, flags: VagueNumber, set: Char, clear: Char, mask: UInt): VagueNumber = when { + state.contains(set) -> flags.withBits(mask) + state.contains(clear) -> flags.withoutBits(mask) + else -> flags + } +} diff --git a/src/main/java/com/smallhacker/disbrowser/resource/RestResource.kt b/src/main/java/com/smallhacker/disbrowser/resource/RestResource.kt new file mode 100644 index 0000000..ca1d34f --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/resource/RestResource.kt @@ -0,0 +1,40 @@ +package com.smallhacker.disbrowser.resource + +import com.smallhacker.disbrowser.Service +import com.smallhacker.disbrowser.asm.Address +import com.smallhacker.disbrowser.asm.MetadataLine +import com.smallhacker.disbrowser.util.toUInt24 +import com.smallhacker.disbrowser.util.tryParseInt +import javax.ws.rs.Consumes +import javax.ws.rs.POST +import javax.ws.rs.Path +import javax.ws.rs.PathParam +import javax.ws.rs.core.MediaType +import javax.ws.rs.core.Response + +@Path("/rest") +class RestResource { + @POST + @Path("{address}/{field}") + @Consumes(MediaType.TEXT_PLAIN) + fun getIt(@PathParam("address") address: String, @PathParam("field") fieldName: String, body: String): Response { + val parsedAddress = parseAddress(address) ?: return Response.status(400).build() + val field = when (fieldName) { + "preComment" -> MetadataLine::preComment + "comment" -> MetadataLine::comment + "label" -> MetadataLine::label + else -> return Response.status(404).build() + } + + Service.updateMetadata(parsedAddress, field, body) + + return Response.noContent().build() + } + + + private fun parseAddress(address: String): Address? { + return tryParseInt(address, 16) + ?.let { Address(it.toUInt24()) } + } + +} diff --git a/src/main/java/com/smallhacker/disbrowser/resource/StaticResource.kt b/src/main/java/com/smallhacker/disbrowser/resource/StaticResource.kt new file mode 100644 index 0000000..04b095c --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/resource/StaticResource.kt @@ -0,0 +1,31 @@ +package com.smallhacker.disbrowser.resource + +import javax.ws.rs.GET +import javax.ws.rs.Path +import javax.ws.rs.PathParam +import javax.ws.rs.core.Response + +@Path("/") +class StaticResource { + @GET + @Path("resources/{file}.{ext}") + fun getStatic(@PathParam("file") file: String, @PathParam("ext") ext: String): Response { + val mime = when (ext) { + "js" -> "application/javascript" + "css" -> "text/css" + else -> null + } + + if (mime != null) { + javaClass.getResourceAsStream("/public/$file.$ext") + ?.bufferedReader() + ?.use { + return Response.ok(it.readText()) + .type(mime) + .build() + } + } + + return Response.status(404).build() + } +} diff --git a/src/main/java/com/smallhacker/disbrowser/util/UVal.kt b/src/main/java/com/smallhacker/disbrowser/util/UVal.kt index 1dc636a..666ef83 100644 --- a/src/main/java/com/smallhacker/disbrowser/util/UVal.kt +++ b/src/main/java/com/smallhacker/disbrowser/util/UVal.kt @@ -27,27 +27,27 @@ class UVal(value: Int, val type: U) : Comparable> { //fun value(value: Int) = UVal(value, type) //fun mutate(mutator: (Int) -> Int) = UVal(mutator(value), type) - object U1 : UType(1, "UByte") - object U2 : UType(2, "UWord") - object U3 : UType(3, "ULong") + object U1 : UType(1, "OldUByte") + object U2 : UType(2, "OldUWord") + object U3 : UType(3, "OldULong") } inline fun > U.value(value: Int): U = UVal(value, type) as U inline infix fun > U.mutate(mutator: (Int) -> Int): U = value(mutator(value)) -typealias UByte = UVal -typealias UWord = UVal -typealias ULong = UVal +typealias OldUByte = UVal +typealias OldUWord = UVal +typealias OldULong = UVal fun UVal<*>.toByte() = uByte(value) fun UVal<*>.toWord() = uWord(value) fun UVal<*>.toLong() = uLong(value) -private val UBYTE_CACHE = Array(256) { UByte(it, UVal.U1) } +private val UBYTE_CACHE = Array(256) { OldUByte(it, UVal.U1) } -fun uByte(value: Int): UByte = UBYTE_CACHE[value and 0xFF] -fun uWord(value: Int): UWord = UVal(value, UVal.U2) -fun uLong(value: Int): ULong = UVal(value, UVal.U3) +fun uByte(value: Int): OldUByte = UBYTE_CACHE[value and 0xFF] +fun uWord(value: Int): OldUWord = UVal(value, UVal.U2) +fun uLong(value: Int): OldULong = UVal(value, UVal.U3) inline infix fun , V: UType> U.left(count: Int): U = mutate { it shl count } inline infix fun , V: UType> U.right(count: Int): U = mutate { it ushr count } diff --git a/src/main/java/com/smallhacker/disbrowser/util/json.kt b/src/main/java/com/smallhacker/disbrowser/util/json.kt new file mode 100644 index 0000000..a2d6b47 --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/util/json.kt @@ -0,0 +1,22 @@ +package com.smallhacker.disbrowser.util + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import java.nio.file.Path + +val jsonMapper: ObjectMapper by lazy { + jacksonObjectMapper() +} + +interface JsonFile { + fun load(): T + fun save(value: T) +} + +inline fun jsonFile(path: Path): JsonFile { + return object : JsonFile { + override fun load() = jsonMapper.readValue(path.toFile()) + override fun save(value: T) = jsonMapper.writeValue(path.toFile(), value) + } +} \ No newline at end of file diff --git a/src/main/java/com/smallhacker/disbrowser/util/misc.kt b/src/main/java/com/smallhacker/disbrowser/util/misc.kt index fb922f6..f974ad8 100644 --- a/src/main/java/com/smallhacker/disbrowser/util/misc.kt +++ b/src/main/java/com/smallhacker/disbrowser/util/misc.kt @@ -14,3 +14,50 @@ fun tryParseInt(s: String, radix: Int = 10): Int? { null } } + +fun toHex(value: UInt, bytes: UInt): String { + if (bytes == 0u) { + return "" + } + + val digits = 2u * bytes + val pattern = "%0${digits}x" + return String.format(pattern, value.toInt()) +} + +fun joinBytes(vararg bytes: UByte) = bytes + .asSequence() + .mapIndexed { index, v -> v.toUInt() shl (index * 8) } + .reduce { a, b -> a or b } + +inline class UInt24(private val data: UInt) { + fun toUInt() = data and 0x00FF_FFFFu + fun toUShort() = toUInt().toUShort() + fun toUByte() = toUInt().toUByte() + fun toInt() = toUInt().toInt() + fun toShort() = toUShort().toShort() + fun toByte() = toUByte().toByte() + + infix fun and(v: UInt24) = (data and v.data).toUInt24() + infix fun and(v: UInt) = (data and v).toUInt24() + infix fun or(v: UInt24) = (data or v.data).toUInt24() + infix fun or(v: UInt) = (data or v).toUInt24() + infix fun shl(v: Int) = (data shl v).toUInt24() + infix fun shr(v: Int)= (toUInt() shr v).toUInt24() + + operator fun plus(v: UInt24) = (toUInt() + v.toUInt()).toUInt24() + operator fun plus(v: UInt) = (toUInt() + v).toUInt24() + operator fun plus(v: Int) = (toInt() + v).toUInt24() + operator fun minus(v: UInt24) = (toUInt() - v.toUInt()).toUInt24() + operator fun minus(v: UInt) = (toUInt() - v).toUInt24() + operator fun minus(v: Int) = (toInt() - v).toUInt24() + + override fun toString() = data.toString() +} + +fun UInt.toUInt24() = UInt24(this and 0x00FF_FFFFu) +fun UShort.toUInt24() = this.toUInt().toUInt24() +fun UByte.toUInt24() = this.toUInt().toUInt24() +fun Int.toUInt24() = this.toUInt().toUInt24() +fun Short.toUInt24() = this.toUInt().toUInt24() +fun Byte.toUInt24() = this.toUInt().toUInt24() diff --git a/src/main/resources/main.js b/src/main/resources/main.js deleted file mode 100644 index 01b2fab..0000000 --- a/src/main/resources/main.js +++ /dev/null @@ -1,48 +0,0 @@ -function center(el) { - function documentOffsetTop (el) { - return el.offsetTop + ( el.offsetParent ? documentOffsetTop(el.offsetParent) : 0 ); - } - - var top = documentOffsetTop(el) - ( window.innerHeight / 2 ); - window.scrollTo( 0, top ); -} - -function highlight(el) { - if (el) { - //el.scrollIntoView(); - center(el); - var cl = el.classList; - var activeClass = "line-active"; - cl.add(activeClass); - setTimeout(cl.remove.bind(cl, activeClass), 100); - } -} - -function fromUrlPart(regex, input) { - var match = regex.exec(input); - - if (!match) { - return null; - } - - return document.getElementById(match[1].toLowerCase()); -} - - -function fromHash() { - return fromUrlPart(/^#([0-9A-Fa-f]{6})$/, location.hash); -} - -function fromPath() { - return fromUrlPart(/^\/([0-9A-Fa-f]{6})(?:\/.*)?/, location.pathname); -} - -function fromUrl() { - return fromHash() || fromPath(); -} - -highlight(fromUrl()); -window.addEventListener("hashchange", function () { highlight(fromHash()) }, false); - - - diff --git a/src/main/resources/style.css b/src/main/resources/public/style.css similarity index 89% rename from src/main/resources/style.css rename to src/main/resources/public/style.css index 1e714ee..3d86c1b 100644 --- a/src/main/resources/style.css +++ b/src/main/resources/public/style.css @@ -92,4 +92,16 @@ tr.line-active { .routine-end > td { border-bottom: 1px solid black; +} + +.field-editable { + font-family: monospace; + border: none; + background: none; + padding: 0; + width: 100%; +} + +.field-comment { + width: 500px; } \ No newline at end of file diff --git a/src/main/ts/main.ts b/src/main/ts/main.ts new file mode 100644 index 0000000..ad3d977 --- /dev/null +++ b/src/main/ts/main.ts @@ -0,0 +1,62 @@ +function center(el: HTMLElement) { + function documentOffsetTop (el: HTMLElement) { + let top = el.offsetTop; + let parent = el.offsetParent; + if (parent && parent instanceof HTMLElement) { + top += documentOffsetTop(parent); + } + return top; + } + + let top = documentOffsetTop(el) - (window.innerHeight / 2); + window.scrollTo( 0, top ); +} + +function highlight(el: HTMLElement | null) { + if (el) { + //el.scrollIntoView(); + center(el); + let cl = el.classList; + let activeClass = "line-active"; + cl.add(activeClass); + setTimeout(cl.remove.bind(cl, activeClass), 100); + } +} + +function fromUrlPart(regex: RegExp, input: string) { + let match = regex.exec(input); + + if (!match) { + return null; + } + + return document.getElementById(match[1].toLowerCase()); +} + +function fromHash() { + return fromUrlPart(/^#([0-9A-Fa-f]{6})$/, location.hash); +} + +function fromPath() { + return fromUrlPart(/^\/([0-9A-Fa-f]{6})(?:\/.*)?/, location.pathname); +} + +function fromUrl() { + return fromHash() || fromPath(); +} + +highlight(fromUrl()); +window.addEventListener("hashchange", function () { highlight(fromHash()) }, false); + +let comments = document.getElementsByClassName("field-editable"); +for (let i = 0; i < comments.length; i++) { + let comment = comments[i]; + comment.addEventListener("change", e => { + let target = (e.target); + let field = target.dataset.field || ""; + let address = parseInt(target.dataset.address || "-1"); + let value = (target).value; + alert(field + "/" + address + "=" + value); + return false; + }); +} \ No newline at end of file diff --git a/src/main/ts/tsconfig.json b/src/main/ts/tsconfig.json new file mode 100644 index 0000000..24f70f2 --- /dev/null +++ b/src/main/ts/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "amd", + "strictNullChecks": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "removeComments": true, + "sourceMap": true, + "outFile": "../resources/public/disbrowser.js" + }, + "include": [ + "*.ts" + ] +} \ No newline at end of file