Added simplistic certainty analysis

This commit is contained in:
Smallhacker 2019-01-15 00:54:54 -05:00
parent 3322f2460f
commit 1dd32a2de8
10 changed files with 311 additions and 84 deletions

View File

@ -12,6 +12,7 @@ class Grid {
private val cellClasses = HashMap<Pair<Int, Int>, String>()
private val addresses = HashMap<SnesAddress, Int>()
private val rowClasses = HashMap<Int, String>()
private val rowCertainties = HashMap<Int, String>()
private val rowId = HashMap<Int, String>()
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])
}
}
}

View File

@ -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)
}
}

View File

@ -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),
}

View File

@ -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()

View File

@ -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()

View File

@ -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<SnesAddress>()
val queue = ArrayDeque<State>()
val origins = mutableMultiMap<SnesAddress, SnesAddress>()
val instructionMap = HashMap<SnesAddress, MutableInstruction>()
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<CodeUnit>()
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<PointerTableLength>()?.entries
localFlags.forFlag<JmpIndirectLongInterleavedTable> {
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<JslTableRoutine> {
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<NonReturningRoutine> {
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<SnesAddress>()
val fatalQueue = ArrayDeque<SnesAddress>()
fun tryAddFatal(snesAddress: SnesAddress) {
if (fatalSeen.add(snesAddress)) {
fatalQueue.addLast(snesAddress)
}
}
instructions.asSequence()
.filterIsInstance<Instruction>()
.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)
}

View File

@ -62,11 +62,22 @@ class GameData {
}
}
fun GameData.flagsAt(snesAddress: SnesAddress?): MetadataLineFlags =
MetadataLineFlags(get(snesAddress)?.flags ?: emptyList())
inline class MetadataLineFlags(val flags: List<InstructionFlag>) {
inline fun <reified F> findFlag() = flags.asSequence().filterIsInstance<F>().firstOrNull()
inline fun <reified F> forFlag(action: F.() -> Unit) = findFlag<F>()?.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<DataBlock> {
fun generatePointerTable(jumpInstruction: Instruction): Sequence<DataBlock> {
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<SnesAddress?> {
class JslTableRoutine : InstructionFlag {
fun readTable(postJsr: State, entryCount: Int): Sequence<SnesAddress?> {
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<DataBlock> {
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)"
}

View File

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

View File

@ -0,0 +1,10 @@
package com.smallhacker.disbrowser.util
typealias MultiMap<K, V> = Map<K, List<V>>
typealias MutableMultiMap<K, V> = MutableMap<K, MutableList<V>>
fun <K, V> mutableMultiMap(): MutableMultiMap<K, V> = HashMap()
fun <K, V> MutableMultiMap<K, V>.putSingle(key: K, value: V) {
computeIfAbsent(key) { ArrayList() }.add(value)
}

View File

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