diff --git a/src/main/java/com/smallhacker/disbrowser/Grid.kt b/src/main/java/com/smallhacker/disbrowser/Grid.kt index b6e751c..ff75a03 100644 --- a/src/main/java/com/smallhacker/disbrowser/Grid.kt +++ b/src/main/java/com/smallhacker/disbrowser/Grid.kt @@ -12,6 +12,7 @@ class Grid { private val cellClasses = HashMap, String>() private val addresses = HashMap() private val rowClasses = HashMap() + private val rowCertainties = HashMap() private val rowId = HashMap() private var height = 0 private var nextAddress: SnesAddress? = null @@ -116,9 +117,11 @@ class Grid { editableField(game, indicativeAddress, "comment", comment) ) - if (ins.opcode.continuation == Continuation.NO) { + if (ins.opcode.continuation.shouldStop) { rowClasses[y] = "routine-end" } + + rowCertainties[y] = ins.certainty.value.toString() } private fun editableField(game: Game, address: SnesAddress, type: String, value: String?): HtmlNode { @@ -194,7 +197,9 @@ class Grid { content[x to y]?.appendTo(parent) }.addClass(cssClass) } - }.addClass(rowClasses[y]).attr("id", rowId[y]) + }.addClass(rowClasses[y]) + .attr("id", rowId[y]) + .attr("row-certainty", rowCertainties[y]) } } } diff --git a/src/main/java/com/smallhacker/disbrowser/asm/Certainty.kt b/src/main/java/com/smallhacker/disbrowser/asm/Certainty.kt new file mode 100644 index 0000000..b358b45 --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/asm/Certainty.kt @@ -0,0 +1,17 @@ +package com.smallhacker.disbrowser.asm + +inline class Certainty(val value: UInt) { + operator fun minus(value: Int): Certainty { + val signed = this.value.toInt() - value + return if (signed < 0) { + PROBABLY_WRONG + } else Certainty(signed.toUInt()) + } + + companion object { + val PROBABLY_CORRECT = Certainty(100u) + val UNCERTAIN = Certainty(50u) + val PROBABLY_WRONG = Certainty(0u) + + } +} \ No newline at end of file diff --git a/src/main/java/com/smallhacker/disbrowser/asm/Continuation.kt b/src/main/java/com/smallhacker/disbrowser/asm/Continuation.kt index ff48f1f..d2ee001 100644 --- a/src/main/java/com/smallhacker/disbrowser/asm/Continuation.kt +++ b/src/main/java/com/smallhacker/disbrowser/asm/Continuation.kt @@ -1,5 +1,9 @@ package com.smallhacker.disbrowser.asm -enum class Continuation { - NO, YES, MAYBE +enum class Continuation(val shouldStop: Boolean) { + CONTINUE(false), + MAY_STOP(false), + STOP(true), + FATAL_ERROR(true), + INSUFFICIENT_DATA(true), } \ 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 1de63dd..6cea94d 100644 --- a/src/main/java/com/smallhacker/disbrowser/asm/Instruction.kt +++ b/src/main/java/com/smallhacker/disbrowser/asm/Instruction.kt @@ -19,6 +19,7 @@ interface CodeUnit { val lengthSuffix: String? val memory: SnesMemory + val certainty: Certainty fun operandByte(index: UInt): UByte = bytes[opcode.operandIndex + index] @@ -56,23 +57,64 @@ interface CodeUnit { class DataBlock( override val opcode: Opcode, override val bytes: ValidMemorySpace, + override val address: SnesAddress?, override val indicativeAddress: SnesAddress, override val sortedAddress: SnesAddress, override val relativeAddress: SnesAddress, override val linkedState: State?, - override val memory: SnesMemory + override val memory: SnesMemory, + override val certainty: Certainty ) : CodeUnit { + constructor( + opcode: Opcode, + bytes: ValidMemorySpace, + indicativeAddress: SnesAddress, + sortedAddress: SnesAddress, + relativeAddress: SnesAddress, + linkedState: State?, + memory: SnesMemory, + certainty: Certainty + ) : this(opcode, bytes, null, indicativeAddress, sortedAddress, relativeAddress, linkedState, memory, certainty) + + constructor( + opcode: Opcode, + bytes: ValidMemorySpace, + address: SnesAddress, + relativeAddress: SnesAddress, + linkedState: State?, + memory: SnesMemory, + certainty: Certainty + ) : this(opcode, bytes, address, address, address, relativeAddress, linkedState, memory, certainty) + override val nextSortedAddress: SnesAddress get() = sortedAddress + operandLength.toInt() override val operandLength get() = bytes.size - override val address: SnesAddress? = null override val preState: State? = null override val postState: State? = null override val lengthSuffix: String? = null } -class Instruction(override val bytes: ValidMemorySpace, override val opcode: Opcode, override val preState: State) : CodeUnit { +interface Instruction : CodeUnit { + override val preState: State + override val address: SnesAddress + override val postState: State + + val continuation: Continuation + val showLengthSuffix: Boolean + fun link(): SnesAddress? + fun referencedAddress(): SnesAddress? + + override fun toString(): String +} + +class MutableInstruction( + override val bytes: ValidMemorySpace, + override val opcode: Opcode, + override val preState: State, + override var continuation: Continuation, + override var certainty: Certainty +) : Instruction { override val memory = preState.memory override val address: SnesAddress get() = preState.address override val indicativeAddress get() = address @@ -89,7 +131,7 @@ class Instruction(override val bytes: ValidMemorySpace, override val opcode: Opc .withOrigin(this) } - private val showLengthSuffix get() = opcode.mode.showLengthSuffix and opcode.mnemonic.showLengthSuffix + override val showLengthSuffix get() = opcode.mode.showLengthSuffix and opcode.mnemonic.showLengthSuffix override val lengthSuffix: String? get() { @@ -109,7 +151,7 @@ class Instruction(override val bytes: ValidMemorySpace, override val opcode: Opc override val operandLength get() = opcode.mode.operandLength(preState) - private fun link(): SnesAddress? { + override fun link(): SnesAddress? { if (!opcode.link) { return null } @@ -117,7 +159,7 @@ class Instruction(override val bytes: ValidMemorySpace, override val opcode: Opc return referencedAddress() } - private fun referencedAddress() = opcode.mode.referencedAddress(this) + override fun referencedAddress() = opcode.mode.referencedAddress(this) override fun toString(): String { val (address, bytes, _, primaryMnemonic, _, suffix, operands, _, _) = print() diff --git a/src/main/java/com/smallhacker/disbrowser/asm/Opcode.kt b/src/main/java/com/smallhacker/disbrowser/asm/Opcode.kt index 0c03447..624e2ed 100644 --- a/src/main/java/com/smallhacker/disbrowser/asm/Opcode.kt +++ b/src/main/java/com/smallhacker/disbrowser/asm/Opcode.kt @@ -7,15 +7,13 @@ import com.smallhacker.disbrowser.asm.Mnemonic.* typealias SegmentEnder = Instruction.() -> SegmentEnd? class Opcode private constructor(val mnemonic: Mnemonic, val mode: Mode, val ender: SegmentEnder, val mutate: (Instruction) -> State) { - private var _continuation = Continuation.YES private var _link = false private var _branch = false val operandIndex get() = if (mode.dataMode) 0u else 1u - val continuation: Continuation - get() = _continuation + var continuation: Continuation = Continuation.CONTINUE val link: Boolean get() = _link @@ -23,15 +21,13 @@ class Opcode private constructor(val mnemonic: Mnemonic, val mode: Mode, val end val branch: Boolean get() = _branch - private fun stop(): Opcode { - this._continuation = Continuation.NO - return this - } + private fun insufficientData() = also { this.continuation = Continuation.INSUFFICIENT_DATA } - private fun mayStop(): Opcode { - this._continuation = Continuation.MAYBE - return this - } + private fun fatal() = also { this.continuation = Continuation.FATAL_ERROR } + + private fun stop() = also { this.continuation = Continuation.STOP } + + private fun mayStop() = also { this.continuation = Continuation.MAY_STOP } private fun linking(): Opcode { this._link = true @@ -80,15 +76,14 @@ class Opcode private constructor(val mnemonic: Mnemonic, val mode: Mode, val end val dynamicSubJumping: SegmentEnder = { stoppingSegmentEnd(address) } val returning: SegmentEnder = { returnSegmentEnd(address) } - UNKNOWN_OPCODE = Opcode(UNKNOWN, Implied, alwaysStop, Instruction::preState).stop() + UNKNOWN_OPCODE = Opcode(UNKNOWN, Implied, alwaysStop, Instruction::preState).insufficientData() - add(0x00, BRK, Immediate8, alwaysStop).stop() - add(0x02, COP, Immediate8, alwaysStop).stop() - add(0x42, WDM, Immediate8, alwaysStop).stop() + add(0x00, BRK, Immediate8, alwaysStop).fatal() + add(0x02, COP, Immediate8, alwaysStop).fatal() + add(0x42, WDM, Immediate8, alwaysStop).fatal() + add(0xDB, STP, Implied, alwaysStop).fatal() add(0xEA, NOP, Implied, alwaysContinue) - - add(0xDB, STP, Implied, alwaysStop).stop() add(0xCB, WAI, Implied, alwaysContinue) add(0x10, BPL, Relative, branching).branching() diff --git a/src/main/java/com/smallhacker/disbrowser/disassembler/Disassembler.kt b/src/main/java/com/smallhacker/disbrowser/disassembler/Disassembler.kt index d5f101c..15d1e17 100644 --- a/src/main/java/com/smallhacker/disbrowser/disassembler/Disassembler.kt +++ b/src/main/java/com/smallhacker/disbrowser/disassembler/Disassembler.kt @@ -1,24 +1,29 @@ package com.smallhacker.disbrowser.disassembler import com.smallhacker.disbrowser.asm.* -import com.smallhacker.disbrowser.game.GameData -import com.smallhacker.disbrowser.game.JmpIndirectLongInterleavedTable -import com.smallhacker.disbrowser.game.JslTableRoutine -import com.smallhacker.disbrowser.game.NonReturningRoutine +import com.smallhacker.disbrowser.game.* +import com.smallhacker.disbrowser.util.mutableMultiMap +import com.smallhacker.disbrowser.util.putSingle import java.util.* import kotlin.collections.ArrayList +import kotlin.collections.HashMap object Disassembler { fun disassemble(initialState: State, gameData: GameData, global: Boolean): Disassembly { val seen = HashSet() val queue = ArrayDeque() + val origins = mutableMultiMap() + val instructionMap = HashMap() - fun tryAdd(state: State) { + fun tryAdd(state: State, origin: SnesAddress?) { + if (origin != null) { + origins.putSingle(state.address, origin) + } if (seen.add(state.address)) { queue.add(state) } } - tryAdd(initialState) + tryAdd(initialState, null) val instructions = ArrayList() while (queue.isNotEmpty()) { @@ -26,57 +31,94 @@ object Disassembler { val ins = disassembleInstruction(state) instructions.add(ins) + instructionMap[ins.address] = ins - var stop = (ins.opcode.continuation == Continuation.NO) or - (ins.opcode.mode.instructionLength(state) == null) - - gameData[ins.address]?.flags?.forEach { flag -> - if (flag is JmpIndirectLongInterleavedTable) { - if (global) { - flag.readTable(state.memory) - .filterNotNull() - .map { ins.postState.copy(address = it) } - .forEach { tryAdd(it) } - } - - flag.generateCode(ins) - .forEach { instructions.add(it) } - - stop = true - } else if (flag is JslTableRoutine) { - if (global) { - flag.readTable(ins.postState) - .filterNotNull() - .map { ins.postState.copy(address = it) } - .forEach { tryAdd(it) } - } - stop = true - } + if (ins.opcode.mode.instructionLength(state) == null) { + ins.continuation = Continuation.INSUFFICIENT_DATA } val linkedState = ins.linkedState - if (linkedState != null) { - gameData[linkedState.address]?.flags?.forEach { - if (it === NonReturningRoutine) { - stop = true - println(ins.address.toFormattedString()) - } + val localAddress = ins.address + val remoteAddress = linkedState?.address + + val localFlags = gameData.flagsAt(localAddress) + val remoteFlags = gameData.flagsAt(remoteAddress) + + val pointerTableEntries = localFlags.findFlag()?.entries + + localFlags.forFlag { + ins.continuation = Continuation.STOP + + if (global) { + readTable(state.memory) + .filterNotNull() + .map { ins.postState.copy(address = it) } + .forEach { tryAdd(it, ins.address) } + } + + generatePointerTable(ins).forEach { + instructions.add(it) } } - if (!stop) { - tryAdd(ins.postState) + remoteFlags.forFlag { + ins.continuation = Continuation.STOP + + if (pointerTableEntries != null) { + if (global) { + readTable(ins.postState, pointerTableEntries) + .filterNotNull() + .map { ins.postState.copy(address = it) } + .forEach { tryAdd(it, ins.address) } + } + } + + generatePointerTable(ins, pointerTableEntries?.toUInt()).forEach { + instructions.add(it) + } + + } + + remoteFlags.forFlag { + ins.continuation = Continuation.STOP + } + + + if (!ins.continuation.shouldStop) { + tryAdd(ins.postState, ins.address) } if (linkedState != null) { if (ins.opcode.branch || global) { - tryAdd(linkedState) + tryAdd(linkedState, ins.address) } } } + val fatalSeen = HashSet() + val fatalQueue = ArrayDeque() + fun tryAddFatal(snesAddress: SnesAddress) { + if (fatalSeen.add(snesAddress)) { + fatalQueue.addLast(snesAddress) + } + } + + instructions.asSequence() + .filterIsInstance() + .filter { it.continuation == Continuation.FATAL_ERROR } + .forEach { tryAddFatal(it.address) } + + while (fatalQueue.isNotEmpty()) { + val badAddress = fatalQueue.removeFirst()!! + val instruction = instructionMap[badAddress] ?: continue + val mnemonic = instruction.opcode.mnemonic + if (mnemonic == Mnemonic.JSL || mnemonic == Mnemonic.JSR) continue + instruction.certainty = Certainty.PROBABLY_WRONG + origins[badAddress]?.forEach{tryAddFatal(it)} + } + val instructionList = instructions .sortedBy { it.sortedAddress } .toList() @@ -118,8 +160,6 @@ object Disassembler { end.remote.forEach { } - - } return segments @@ -167,15 +207,17 @@ object Disassembler { return finalize(Segment(initialState.address, continuationSegmentEnd(lastState), instructions)) } - private fun disassembleInstruction(state: State): Instruction { + private fun disassembleInstruction(state: State): MutableInstruction { val opcodeValue = state.memory[state.address] ?: return unreadableInstruction(state) val opcode = Opcode.opcode(opcodeValue) val length = opcode.mode.instructionLength(state) ?: 1u val bytes = state.memory.range(state.address.value.toUInt(), length).validate() - ?: return unreadableInstruction(state) - return Instruction(bytes, opcode, state) + ?: return unreadableInstruction(state) + val continuation = opcode.continuation + val certainty = Certainty.PROBABLY_CORRECT + return MutableInstruction(bytes, opcode, state, continuation, certainty) } private fun unreadableInstruction(state: State) = - Instruction(EmptyMemorySpace, Opcode.UNKNOWN_OPCODE, state) + MutableInstruction(EmptyMemorySpace, Opcode.UNKNOWN_OPCODE, state, Continuation.INSUFFICIENT_DATA, Certainty.PROBABLY_WRONG) } diff --git a/src/main/java/com/smallhacker/disbrowser/game/GameData.kt b/src/main/java/com/smallhacker/disbrowser/game/GameData.kt index 3612375..d981fe8 100644 --- a/src/main/java/com/smallhacker/disbrowser/game/GameData.kt +++ b/src/main/java/com/smallhacker/disbrowser/game/GameData.kt @@ -62,11 +62,22 @@ class GameData { } } +fun GameData.flagsAt(snesAddress: SnesAddress?): MetadataLineFlags = + MetadataLineFlags(get(snesAddress)?.flags ?: emptyList()) + +inline class MetadataLineFlags(val flags: List) { + inline fun findFlag() = flags.asSequence().filterIsInstance().firstOrNull() + inline fun forFlag(action: F.() -> Unit) = findFlag()?.run(action) +} + +operator fun GameData.get(address: SnesAddress?) = if (address == null) null else this[address] + @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") + Type(value = JslTableRoutine::class, name = "JslTableRoutine"), + Type(value = PointerTableLength::class, name = "PointerTableLength") ) interface InstructionFlag @@ -88,7 +99,7 @@ class JmpIndirectLongInterleavedTable @JsonCreator constructor( .map { pointer -> pointer?.let { SnesAddress(it) } } } - fun generateCode(jumpInstruction: Instruction): Sequence { + fun generatePointerTable(jumpInstruction: Instruction): Sequence { val table = jumpInstruction.preState.memory.deinterleave(uEntries, start.value.toUInt(), (start + entries).value.toUInt(), @@ -110,7 +121,8 @@ class JmpIndirectLongInterleavedTable @JsonCreator constructor( jumpInstruction.opcode.mutate(jumpInstruction) .mutateAddress { SnesAddress(target) } .withOrigin(jumpInstruction), - jumpInstruction.memory + jumpInstruction.memory, + Certainty.PROBABLY_CORRECT ) } } @@ -118,18 +130,68 @@ class JmpIndirectLongInterleavedTable @JsonCreator constructor( override fun toString() = "JmpIndirectLongInterleavedTable($start, $entries)" } -class JslTableRoutine @JsonCreator constructor( - @field:JsonProperty @JsonProperty private val entries: Int -) : InstructionFlag { - - fun readTable(postJsr: State): Sequence { +class JslTableRoutine : InstructionFlag { + fun readTable(postJsr: State, entryCount: Int): Sequence { val data = postJsr.memory - return (0 until entries) + return (0 until entryCount) .asSequence() .map { postJsr.address + (it * 3) } .map { address -> joinNullableBytes(data[address], data[address + 1], data[address + 2])?.toUInt24() } .map { pointer -> pointer?.let { SnesAddress(it) } } } - override fun toString() = "JslTableRoutine($entries)" + fun generatePointerTable(jumpInstruction: Instruction, entryCount: UInt?): Sequence { + val count: UInt + var certainty: Certainty + val certaintyDecrease: Int + + if (entryCount == null) { + count = 30u + certainty = Certainty.UNCERTAIN + certaintyDecrease = 5 + } else { + count = entryCount + certainty = Certainty.PROBABLY_CORRECT + certaintyDecrease = 0 + } + + val start = jumpInstruction.postState.address + val memory = jumpInstruction.memory + + return (0u until count.toUInt()) + .asSequence() + .map { index -> index * 3u } + .mapNotNull { offset -> + val pointerLoc= start + offset.toInt() + val addressRange = memory.range(pointerLoc, 3u).validate() + + if (addressRange == null) { + null + } else { + val target = addressRange.getLong(0u) + + val block = DataBlock( + Opcode.CODE_POINTER_LONG, + addressRange, + pointerLoc, + jumpInstruction.relativeAddress, + jumpInstruction.opcode.mutate(jumpInstruction) + .mutateAddress { SnesAddress(target) } + .withOrigin(jumpInstruction), + memory, + certainty + ) + certainty -= certaintyDecrease + block + } + } + } + + override fun toString() = "JslTableRoutine" +} + +class PointerTableLength @JsonCreator constructor( + @field:JsonProperty @JsonProperty val entries: Int +) : InstructionFlag { + override fun toString() = "PointerTableLength($entries)" } \ No newline at end of file diff --git a/src/main/java/com/smallhacker/disbrowser/resource/DisassemblyResource.kt b/src/main/java/com/smallhacker/disbrowser/resource/DisassemblyResource.kt index 1b29e83..a0735f3 100644 --- a/src/main/java/com/smallhacker/disbrowser/resource/DisassemblyResource.kt +++ b/src/main/java/com/smallhacker/disbrowser/resource/DisassemblyResource.kt @@ -88,7 +88,7 @@ class DisassemblyResource { return Response.ok(html.toString().toByteArray(StandardCharsets.UTF_8)) .encoding("UTF-8") .build() - } catch (e: Exception) { + } catch (e: Throwable) { e.printStackTrace() throw e } diff --git a/src/main/java/com/smallhacker/disbrowser/util/multiMap.kt b/src/main/java/com/smallhacker/disbrowser/util/multiMap.kt new file mode 100644 index 0000000..3b19d01 --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/util/multiMap.kt @@ -0,0 +1,10 @@ +package com.smallhacker.disbrowser.util + +typealias MultiMap = Map> +typealias MutableMultiMap = MutableMap> + +fun mutableMultiMap(): MutableMultiMap = HashMap() + +fun MutableMultiMap.putSingle(key: K, value: V) { + computeIfAbsent(key) { ArrayList() }.add(value) +} \ No newline at end of file diff --git a/src/main/resources/public/style.css b/src/main/resources/public/style.css index cc3d044..73b72d6 100644 --- a/src/main/resources/public/style.css +++ b/src/main/resources/public/style.css @@ -127,4 +127,54 @@ tr.line-active { } .field-editable-popup-icon::before { content: "[e]" +} + +[row-certainty="0"],[row-certainty="1"],[row-certainty="2"],[row-certainty="3"],[row-certainty="4"], +[row-certainty="5"],[row-certainty="6"],[row-certainty="7"],[row-certainty="8"],[row-certainty="9"] { + color: rgb(200,200,200); +} + +[row-certainty="10"],[row-certainty="11"],[row-certainty="12"],[row-certainty="13"],[row-certainty="14"], +[row-certainty="15"],[row-certainty="16"],[row-certainty="17"],[row-certainty="18"],[row-certainty="19"] { + color: rgb(180,180,180); +} + +[row-certainty="20"],[row-certainty="21"],[row-certainty="22"],[row-certainty="23"],[row-certainty="24"], +[row-certainty="25"],[row-certainty="26"],[row-certainty="27"],[row-certainty="28"],[row-certainty="29"] { + color: rgb(160,160,160); +} + +[row-certainty="30"],[row-certainty="31"],[row-certainty="32"],[row-certainty="33"],[row-certainty="34"], +[row-certainty="35"],[row-certainty="36"],[row-certainty="37"],[row-certainty="38"],[row-certainty="39"] { + color: rgb(140,140,140); +} + +[row-certainty="40"],[row-certainty="41"],[row-certainty="42"],[row-certainty="43"],[row-certainty="44"], +[row-certainty="45"],[row-certainty="46"],[row-certainty="47"],[row-certainty="48"],[row-certainty="49"] { + color: rgb(120,120,120); +} + +[row-certainty="50"],[row-certainty="51"],[row-certainty="52"],[row-certainty="53"],[row-certainty="54"], +[row-certainty="55"],[row-certainty="56"],[row-certainty="57"],[row-certainty="58"],[row-certainty="59"] { + color: rgb(100,100,100); +} + +[row-certainty="60"],[row-certainty="61"],[row-certainty="62"],[row-certainty="63"],[row-certainty="64"], +[row-certainty="65"],[row-certainty="66"],[row-certainty="67"],[row-certainty="68"],[row-certainty="69"] { + color: rgb(80,80,80); +} + +[row-certainty="70"],[row-certainty="71"],[row-certainty="72"],[row-certainty="73"],[row-certainty="74"], +[row-certainty="75"],[row-certainty="76"],[row-certainty="77"],[row-certainty="78"],[row-certainty="79"] { + color: rgb(60,60,60); +} + +[row-certainty="80"],[row-certainty="81"],[row-certainty="82"],[row-certainty="83"],[row-certainty="84"], +[row-certainty="85"],[row-certainty="86"],[row-certainty="87"],[row-certainty="88"],[row-certainty="89"] { + color: rgb(40,40,40); +} + +[row-certainty="90"],[row-certainty="91"],[row-certainty="92"],[row-certainty="93"],[row-certainty="94"], +[row-certainty="95"],[row-certainty="96"],[row-certainty="97"],[row-certainty="98"],[row-certainty="99"] { + color: rgb(20,20,20); } \ No newline at end of file