commit 7a4b94e907bb3eea65b76e490b913d51e76dbb17 Author: Smallhacker Date: Mon Jan 7 13:19:37 2019 -0500 Overdue for a proper repo diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f419dc7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.idea +/target diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..2ea31ea --- /dev/null +++ b/pom.xml @@ -0,0 +1,138 @@ + + + 4.0.0 + + com.smallhacker.dis-browser + dis-browser + war + 1.0-SNAPSHOT + dis-browser + + + + + org.glassfish.jersey + jersey-bom + ${jersey.version} + pom + import + + + + + + + org.glassfish.jersey.containers + jersey-container-grizzly2-http + + + org.glassfish.jersey.inject + jersey-hk2 + + + + + junit + junit + 4.9 + test + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-test + ${kotlin.version} + test + + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.2.1 + + + + java + + + + + com.smallhacker.dis-browser.Main + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + 1.8 + + -XXLanguage:+InlineClasses + -Xexperimental=kotlin.ExperimentalUnsignedTypes + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + + compile + compile + + compile + + + + testCompile + test-compile + + testCompile + + + + true + + 1.8 + 1.8 + + + + + + + 2.27 + UTF-8 + 1.3.11 + + diff --git a/src/main/java/com/smallhacker/disbrowser/Grid.kt b/src/main/java/com/smallhacker/disbrowser/Grid.kt new file mode 100644 index 0000000..328e359 --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/Grid.kt @@ -0,0 +1,158 @@ +package com.smallhacker.disbrowser + +import com.smallhacker.disbrowser.asm.* + +class Grid { + private val arrowCells = HashMap, HtmlNode?>() + private val arrowClasses = HashMap, String>() + private var arrowWidth = 0 + + private val content = HashMap, HtmlNode?>() + private val cellClasses = HashMap, String>() + private val addresses = HashMap() + private val rowClasses = HashMap() + private val rowId = HashMap() + private var height = 0 + private var nextAddress: Address? = null + + fun arrow(from: Address, to: Address) { + val yStart = addresses[from] + val yEnd = addresses[to] + if (yStart == null || yEnd == null) { + return + } + + val y1: Int + val y2: Int + val dir: String + if (yStart > yEnd) { + dir = "up" + y1 = yEnd + y2 = yStart + } else { + dir = "down" + y1 = yStart + y2 = yEnd + } + + val x = nextArrowX(y1, y2) + if ((x + 1) > arrowWidth) { + arrowWidth = x + 1 + } + + arrowClasses[x to y1] = "arrow arrow-$dir-start" + for (y in (y1 + 1)..(y2 - 1)) { + arrowClasses[x to y] = "arrow arrow-$dir-middle" + } + arrowClasses[x to y2] = "arrow arrow-$dir-end" + //arrowCells[x to yStart] = a().addClass("arrow-link").attr("href", "#${to.toSimpleString()}") + arrowCells[x to yEnd] = div().addClass("arrow-head") + } + + private fun nextArrowX(y1: Int, y2: Int): Int { + return generateSequence(0) { it + 1 } + .filter { x -> + (y1..y2).asSequence() + .map { y -> arrowClasses[x to y] } + .all { it == null } + } + .first() + } + + fun add(ins: Instruction, metadata: MetadataLine?, disassembly: Disassembly) { + val address = ins.address + + if (nextAddress != null) { + if (address != nextAddress) { + addDummy() + } + } + nextAddress = ins.postState.address + + val y = (height++) + addresses[address] = y + + add(y, ins.address, + text(ins.address.toFormattedString()), + text(ins.bytesToString()), + text(metadata?.label?.plus(":") ?: ""), + fragment { + text("${ins.opcode.mnemonic}") + text(ins.lengthSuffix) + text(" ") + val operands = ins.opcode.mode.print(ins) + + val link = ins.linkedState + if (link == null) { + text(operands) + } else { + val local = link.address in disassembly + + val url = when { + local -> "#${link.address.toSimpleString()}" + else -> "/${link.address.toSimpleString()}/${link.urlString}" + } + + + a { + text(operands) + }.attr("href", url) + } + }, + text(ins.postState.toString()) + ) + + if (ins.opcode.continuation == Continuation.NO) { + rowClasses[y] = "routine-end" + } + } + + private fun addDummy() { + val y = (height++) + add(y, null, null, null, null, text("..."), null) + } + + private fun add(y: Int, address: Address?, + cAddress: HtmlNode?, + cBytes: HtmlNode?, + cLabel: HtmlNode?, + cCode: HtmlNode?, + cState: HtmlNode? + ) { + if (address != null) { + rowId[y] = address.toSimpleString() + } + content[0 to y] = cAddress + content[1 to y] = cBytes + content[2 to y] = cLabel + content[3 to y] = cCode + content[4 to y] = cState + } + + fun output(): HtmlNode { + return table { + for (y in 0 until height) { + tr { + for (x in 0..3) { + val cssClass = cellClasses[x to y] + td { + content[x to y]?.appendTo(parent) + }.addClass(cssClass) + } + for (x in 0 until arrowWidth) { + val cssClass = arrowClasses[x to y] + td { + arrowCells[x to y]?.appendTo(parent) + }.addClass(cssClass) + } + for (x in 4..4) { + val cssClass = cellClasses[x to y] + td { + content[x to y]?.appendTo(parent) + }.addClass(cssClass) + } + }.addClass(rowClasses[y]).attr("id", rowId[y]) + } + } + } +} diff --git a/src/main/java/com/smallhacker/disbrowser/Html.kt b/src/main/java/com/smallhacker/disbrowser/Html.kt new file mode 100644 index 0000000..44cce89 --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/Html.kt @@ -0,0 +1,126 @@ +package com.smallhacker.disbrowser + +interface HtmlNode { + fun print(): String { + val out = StringBuilder() + printTo(out) + return out.toString() + } + + fun printTo(out: StringBuilder): StringBuilder + + fun attr(key: String): String? = null + + fun attr(key: String, value: String?): HtmlNode = this + + fun append(node: HtmlNode): HtmlNode = this +} + +open class HtmlElement(protected val tag: String) : HtmlNode { + private val attributes = LinkedHashMap() + + final override fun toString(): String = print() + + override fun printTo(out: StringBuilder): StringBuilder { + out.append("<", tag) + attributes.forEach { key, value -> + out.append(" ", key, "=\"", value, "\"") + } + return out.append(">") + } + + final override fun attr(key: String): String? = attributes[key] + + final override fun attr(key: String, value: String?) = apply { + if (value != null) { + attributes[key] = value + } else { + attributes.remove("key") + } + } +} + +private class ParentHtmlElement(tag: String, inner: InnerHtml) : HtmlElement(tag) { + private val children = ArrayList() + + init { + inner(HtmlArea(this)) + } + + override fun printTo(out: StringBuilder): StringBuilder { + super.printTo(out) + children.forEach { it.printTo(out) } + out.append("") + return out + } + + override fun append(node: HtmlNode) = apply { children.add(node) } +} + +private fun parent(tag: String, inner: InnerHtml): HtmlNode = ParentHtmlElement(tag, inner) +private fun leaf(tag: String): HtmlNode = HtmlElement(tag) + +typealias InnerHtml = HtmlArea.() -> Unit + +class HtmlArea(val parent: HtmlNode) + +fun text(text: String): HtmlNode = object : HtmlNode { + override fun printTo(out: StringBuilder) = out.append(text) + override fun attr(key: String, value: String?) = this + override fun append(node: HtmlNode): HtmlNode = this +} + +fun HtmlArea.text(text: String) = com.smallhacker.disbrowser.text(text).appendTo(parent) + +fun fragment(inner: InnerHtml = {}) = object : HtmlNode { + private val children = ArrayList() + + init { + inner(HtmlArea(this)) + } + + override fun printTo(out: StringBuilder) = out.apply { children.forEach { it.printTo(out) } } + + override fun append(node: HtmlNode) = apply { children.add(node) } +} + +fun HtmlArea.fragment(inner: InnerHtml = {}) = com.smallhacker.disbrowser.fragment(inner).appendTo(parent) +fun html(inner: InnerHtml = {}) = parent("html", inner) +fun HtmlArea.html(inner: InnerHtml = {}) = com.smallhacker.disbrowser.html(inner).appendTo(parent) +fun head(inner: InnerHtml = {}) = parent("head", inner) +fun HtmlArea.head(inner: InnerHtml = {}) = com.smallhacker.disbrowser.head(inner).appendTo(parent) +fun title(inner: InnerHtml = {}) = parent("title", inner) +fun HtmlArea.title(inner: InnerHtml = {}) = com.smallhacker.disbrowser.title(inner).appendTo(parent) +fun link(inner: InnerHtml = {}) = leaf("link") +fun HtmlArea.link(inner: InnerHtml = {}) = com.smallhacker.disbrowser.link(inner).appendTo(parent) +fun body(inner: InnerHtml = {}) = parent("body", inner) +fun HtmlArea.body(inner: InnerHtml = {}) = com.smallhacker.disbrowser.body(inner).appendTo(parent) +fun div(inner: InnerHtml = {}) = parent("div", inner) +fun HtmlArea.div(inner: InnerHtml = {}) = com.smallhacker.disbrowser.div(inner).appendTo(parent) +fun table(inner: InnerHtml = {}) = parent("table", inner) +fun HtmlArea.table(inner: InnerHtml = {}) = com.smallhacker.disbrowser.table(inner).appendTo(parent) +fun tr(inner: InnerHtml = {}) = parent("tr", inner) +fun HtmlArea.tr(inner: InnerHtml = {}) = com.smallhacker.disbrowser.tr(inner).appendTo(parent) +fun td(inner: InnerHtml = {}) = parent("td", inner) +fun HtmlArea.td(inner: InnerHtml = {}) = com.smallhacker.disbrowser.td(inner).appendTo(parent) +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 HtmlNode.appendTo(node: HtmlNode) = apply { node.append(this) } +fun HtmlNode.addClass(c: String?) = attrAdd("class", c) + +fun HtmlNode.attrSet(name: String): MutableSet = (attr(name) ?: "") + .split(" ") + .asSequence() + .filterNot { it.isEmpty() } + .toMutableSet() + +fun HtmlNode.attrAdd(name: String, value: String?) = apply { + if (value != null) { + val set = attrSet(name) + set.add(value) + attr(name, set.joinToString(" ")) + } +} \ No newline at end of file diff --git a/src/main/java/com/smallhacker/disbrowser/HtmlBuilder.kt b/src/main/java/com/smallhacker/disbrowser/HtmlBuilder.kt new file mode 100644 index 0000000..60e34e8 --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/HtmlBuilder.kt @@ -0,0 +1,39 @@ +//package com.smallhacker.disbrowser +// +//class HtmlContext(val out: StringBuilder) +// +//fun html(inner: HtmlContext.() -> Unit = {}): String { +// val html = HtmlContext(StringBuilder().append("")) +// element(html, "html") { inner(html) } +// return html.out.toString() +//} +// +//private fun element(html: HtmlContext, tag: String, inner: HtmlContext.() -> Unit) = element(html, tag, inner, *emptyArray()) +// +//private fun element(html: HtmlContext, tag: String, inner: HtmlContext.() -> Unit, vararg args: String) { +// html.out.append("<$tag") +// html.out.append(args.asSequence().map { " $it" }.joinToString()) +// html.out.append(">") +// html.inner() +// html.out.append("") +//} +// +// +//fun HtmlContext.text(text: String) { +// this.out.append(text) +//} +// +//fun HtmlContext.head(inner: HtmlContext.() -> Unit = {}) = element(this, "head", inner) +//fun HtmlContext.title(inner: HtmlContext.() -> Unit = {}) = element(this, "title", inner) +//fun HtmlContext.body(inner: HtmlContext.() -> Unit = {}) = element(this, "body", inner) +//fun HtmlContext.style(inner: HtmlContext.() -> Unit = {}) = element(this, "style", inner) +//fun HtmlContext.link(href: String, inner: HtmlContext.() -> Unit = {}) = element(this, "link", inner, "rel=\"stylesheet\" href=\"$href\"") +//fun HtmlContext.div(inner: HtmlContext.() -> Unit = {}) = element(this, "div", inner) +//fun HtmlContext.div(cssClass: String, inner: HtmlContext.() -> Unit = {}) = element(this, "div", inner, "class=\"$cssClass\"") +// +//fun HtmlContext.table(inner: HtmlContext.() -> Unit = {}) = element(this, "table", inner) +//fun HtmlContext.tr(inner: HtmlContext.() -> Unit = {}) = element(this, "tr", inner) +//fun HtmlContext.tr(cssClass: String?, inner: HtmlContext.() -> Unit = {}) = element(this, "tr", inner, (if (cssClass == null) "" else "class=\"$cssClass\"")) +//fun HtmlContext.td(inner: HtmlContext.() -> Unit = {}) = element(this, "td", inner) +//fun HtmlContext.td(cssClass: String?, inner: HtmlContext.() -> Unit = {}) = element(this, "td", inner, (if (cssClass == null) "" else "class=\"$cssClass\"")) +//fun HtmlContext.a(href: String, inner: HtmlContext.() -> Unit = {}) = element(this, "a", inner, "href=\"$href\"") \ No newline at end of file diff --git a/src/main/java/com/smallhacker/disbrowser/ImmStack.kt b/src/main/java/com/smallhacker/disbrowser/ImmStack.kt new file mode 100644 index 0000000..4759acd --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/ImmStack.kt @@ -0,0 +1,37 @@ +package com.smallhacker.disbrowser + +interface ImmStack: Iterable { + fun isEmpty(): Boolean + + val top: E? + + fun pop(): ImmStack + + fun push(value: E): ImmStack = ImmStackImpl(this, value) +} + +fun immStack(): ImmStack { + @Suppress("UNCHECKED_CAST") + return EmptyImmStack as ImmStack +} + +private class ImmStackImpl(private val parent: ImmStack, override val top: E): ImmStack { + override fun isEmpty() = false + + override fun pop(): ImmStack = parent + + override fun iterator(): Iterator { + return sequenceOf(top).plus(parent).iterator() + } + +} + +private object EmptyImmStack: ImmStack { + override fun isEmpty() = true + + override val top: Any? = null + + override fun pop() = this + + override fun iterator() = emptySequence().iterator() +} \ No newline at end of file diff --git a/src/main/java/com/smallhacker/disbrowser/Main.java b/src/main/java/com/smallhacker/disbrowser/Main.java new file mode 100644 index 0000000..6e177c4 --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/Main.java @@ -0,0 +1,45 @@ +package com.smallhacker.disbrowser; + +import org.glassfish.grizzly.http.server.HttpServer; +import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory; +import org.glassfish.jersey.server.ResourceConfig; + +import java.io.IOException; +import java.net.URI; + +/** + * Main class. + * + */ +public class Main { + // Base URI the Grizzly HTTP server will listen on + public static final String BASE_URI = "http://localhost:8080/"; + + /** + * Starts Grizzly HTTP server exposing JAX-RS resources defined in this application. + * @return Grizzly HTTP server. + */ + 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"); + + // create and start a new instance of grizzly http server + // exposing the Jersey application at BASE_URI + return GrizzlyHttpServerFactory.createHttpServer(URI.create(BASE_URI), rc); + } + + /** + * Main method. + * @param args + * @throws IOException + */ + public static void main(String[] args) throws IOException { + final HttpServer server = startServer(); + System.out.println(String.format("Jersey app started with WADL available at " + + "%sapplication.wadl\nHit enter to stop it...", BASE_URI)); + System.in.read(); + server.stop(); + } +} + diff --git a/src/main/java/com/smallhacker/disbrowser/MyResource.kt b/src/main/java/com/smallhacker/disbrowser/MyResource.kt new file mode 100644 index 0000000..c6eff6b --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/MyResource.kt @@ -0,0 +1,159 @@ +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/asm/Address.kt b/src/main/java/com/smallhacker/disbrowser/asm/Address.kt new file mode 100644 index 0000000..02d2d43 --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/asm/Address.kt @@ -0,0 +1,19 @@ +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) + + 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) + + 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 withinBank(value: Int): Address = Address((this.value and 0xFF_0000) or value) + + override fun compareTo(other: Address) = value.compareTo(other.value) +} diff --git a/src/main/java/com/smallhacker/disbrowser/asm/Continuation.kt b/src/main/java/com/smallhacker/disbrowser/asm/Continuation.kt new file mode 100644 index 0000000..ff48f1f --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/asm/Continuation.kt @@ -0,0 +1,5 @@ +package com.smallhacker.disbrowser.asm + +enum class Continuation { + NO, YES, MAYBE +} \ No newline at end of file diff --git a/src/main/java/com/smallhacker/disbrowser/asm/Disassembly.kt b/src/main/java/com/smallhacker/disbrowser/asm/Disassembly.kt new file mode 100644 index 0000000..b9c6172 --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/asm/Disassembly.kt @@ -0,0 +1,13 @@ +package com.smallhacker.disbrowser.asm + +class Disassembly(lines: List) : Iterable { + override fun iterator() = lines.values.iterator() as Iterator + + private val lines = LinkedHashMap() + + init { + lines.forEach { this.lines[it.address] = it } + } + + operator fun contains(address: Address) = address in lines +} \ 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 new file mode 100644 index 0000000..823eb79 --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/asm/Instruction.kt @@ -0,0 +1,106 @@ +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 + + val postState = opcode.mutate(this) + .mutateAddress { it + bytes.size } + .withOrigin(this) + 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 + 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 -> "" + } + } + } + + private val operandLength + get() = opcode.mode.operandLength(preState) + + private fun link(): Address? { + if (!opcode.link) { + return null + } + + 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) + else -> null + } + } + + override fun toString(): String { + return "$address ${bytesToString()} ${opcode.mnemonic} ${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 new file mode 100644 index 0000000..971c71c --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/asm/Metadata.kt @@ -0,0 +1,66 @@ +package com.smallhacker.disbrowser.asm + +import com.smallhacker.disbrowser.util.left +import com.smallhacker.disbrowser.util.or +import com.smallhacker.disbrowser.util.toLong + +class Metadata { + private val lines = HashMap() + + fun add(line: MetadataLine): Metadata { + lines[line.address] = line + return this + } + + operator fun get(address: Address): MetadataLine? { + return lines[address] + } +} + +fun Metadata.at(address: Int, runner: MetadataLine.() -> Unit) { + val line = MetadataLine(Address(address)) + this.add(line) + runner(line) +} + +fun metadata(runner: Metadata.() -> Unit): Metadata { + val metadata = Metadata() + runner(metadata) + return metadata +} + +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) } + + } +} + +class JslTableRoutine(private val entries: Int) : InstructionFlag { + fun readTable(postJsr: State): Sequence
{ + val data = postJsr.data + return (0 until entries) + .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) } + } + +// 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 }) +// } +// } +} \ 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 new file mode 100644 index 0000000..4fbe15d --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/asm/MetadataLine.kt @@ -0,0 +1,8 @@ +package com.smallhacker.disbrowser.asm + +data class MetadataLine( + val address: Address, + var label: String? = null, + var comment: 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 new file mode 100644 index 0000000..3706785 --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/asm/Mnemonic.kt @@ -0,0 +1,15 @@ +package com.smallhacker.disbrowser.asm + +enum class Mnemonic { + 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, + JSR, LDA, LDX, LDY, LSR, MVN, MVP, NOP, ORA, PEA, PEI, + PER, PHA, PHB, PHD, PHK, PHP, PHX, PHY, PLA, PLB, PLD, + PLP, PLX, PLY, REP, ROL, ROR, RTI, RTL, RTS, SBC, SEC, + SED, SEI, SEP, STA, STP, STX, STY, STZ, TAX, TAY, TCD, + TCS, TDC, TRB, TSB, TSC, TSX, TXA, TXS, TXY, TYA, TYX, + WAI, WDM, XBA, XCE, + + DB, DW, DL +} diff --git a/src/main/java/com/smallhacker/disbrowser/asm/Mode.kt b/src/main/java/com/smallhacker/disbrowser/asm/Mode.kt new file mode 100644 index 0000000..fe051c0 --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/asm/Mode.kt @@ -0,0 +1,111 @@ +package com.smallhacker.disbrowser.asm + +import com.smallhacker.disbrowser.util.* + +fun format(format: String, value: UVal<*>): String { + return format.replace(Regex("[0]+"), value.toHex()) +} + +enum class Mode { + DATA_BYTE(1, "$00", { dataByte }, dataMode = true), + DATA_WORD(2, "$0000", { dataWord }, dataMode = true), + DATA_LONG(3, "$000000", { dataLong }, dataMode = true), + + 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) }) + ; + + private val length: Int + val print: Instruction.() -> String + val dataMode: Boolean + + constructor(length: Int, print: Instruction.() -> String) { + this.length = length + this.print = print + this.dataMode = false + } + + constructor(length: Int, format: String, valueGetter: Instruction.() -> UVal<*>, dataMode: Boolean = false) { + this.length = length + this.print = { format(format, valueGetter(this)) } + this.dataMode = dataMode + } + + /** + * Returns the total length, in bytes, of an instruction of this mode and its operands. + * + * This is usually one greater than [operandLength], except in the cases when the instruction is just pure data + * without an opcode (in which case the two are equal). + * + * 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 + } + } + + /** + * Returns the length, in bytes, of the operands of an instruction of this mode. + * + * This is usually one less than [operandLength], except in the cases when the instruction is just pure data + * without an opcode (in which case the two are equal). + * + * 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 + } + } +} diff --git a/src/main/java/com/smallhacker/disbrowser/asm/ModeFormat.kt b/src/main/java/com/smallhacker/disbrowser/asm/ModeFormat.kt new file mode 100644 index 0000000..6a3b7db --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/asm/ModeFormat.kt @@ -0,0 +1,19 @@ +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 new file mode 100644 index 0000000..91055f9 --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/asm/Opcode.kt @@ -0,0 +1,363 @@ +package com.smallhacker.disbrowser.asm + +import java.util.HashMap + +import com.smallhacker.disbrowser.asm.Mnemonic.* +import com.smallhacker.disbrowser.asm.Mode.* +import com.smallhacker.disbrowser.util.UByte + +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) 0 else 1 + + val continuation: Continuation + get() = _continuation + + val link: Boolean + get() = _link + + val branch: Boolean + get() = _branch + + private fun stop(): Opcode { + this._continuation = Continuation.NO + return this + } + + private fun mayStop(): Opcode { + this._continuation = Continuation.MAYBE + return this + } + + private fun linking(): Opcode { + this._link = true + return this + } + + private fun branching(): Opcode { + linking() + this._branch = true + return this + } + + companion object { + val DATA_BYTE = Opcode(Mnemonic.DB, Mode.DATA_BYTE, { null }, { it.preState }) + val DATA_WORD = Opcode(Mnemonic.DW, Mode.DATA_WORD, { null }, { it.preState }) + val DATA_LONG = Opcode(Mnemonic.DL, Mode.DATA_LONG, { null }, { it.preState }) + + val POINTER_WORD = Opcode(Mnemonic.DW, Mode.DATA_WORD, { null }, { it.preState }).linking() + val POINTER_LONG = Opcode(Mnemonic.DL, Mode.DATA_LONG, { null }, { it.preState }).linking() + + private val OPCODES: Array + + fun opcode(byteValue: UByte): Opcode { + return OPCODES[byteValue.value] + } + + init { + val ocs = HashMap() + + fun add(value: Int, mnemonic: Mnemonic, mode: Mode, ender: SegmentEnder, mutate: (Instruction) -> State = { it.preState }): Opcode { + val opcode = Opcode(mnemonic, mode, ender, mutate) + ocs[value] = opcode + return opcode + } + + val alwaysContinue: SegmentEnder = { null } + val alwaysStop: SegmentEnder = { stoppingSegmentEnd(address) } + val branching: SegmentEnder = { branchingSegmentEnd(address, postState, linkedState!!) } + val alwaysBranching: SegmentEnder = { alwaysBranchingSegmentEnd(address, linkedState!!) } + val jumping: SegmentEnder = { jumpSegmentEnd(address, linkedState!!) } + val dynamicJumping: SegmentEnder = { stoppingSegmentEnd(address) } + val subJumping: SegmentEnder = { subroutineSegmentEnd(address, linkedState!!, postState.address) } + val dynamicSubJumping: SegmentEnder = { stoppingSegmentEnd(address) } + val returning: SegmentEnder = { returnSegmentEnd(address) } + + + add(0x00, BRK, IMMEDIATE_8, alwaysStop).stop() + add(0x02, COP, IMMEDIATE_8, alwaysStop).stop() + add(0x42, WDM, IMMEDIATE_8, alwaysStop).stop() + + add(0xEA, NOP, IMPLIED, alwaysContinue) + + add(0xDB, STP, IMPLIED, alwaysStop).stop() + add(0xCB, WAI, IMPLIED, alwaysContinue) + + add(0x10, BPL, RELATIVE, branching).branching() + add(0x30, BMI, RELATIVE, branching).branching() + add(0x50, BVC, RELATIVE, branching).branching() + add(0x70, BVS, RELATIVE, branching).branching() + add(0x80, BRA, RELATIVE, alwaysBranching).stop().branching() + add(0x90, BCC, RELATIVE, branching).branching() + add(0xB0, BCS, RELATIVE, branching).branching() + add(0xD0, BNE, RELATIVE, branching).branching() + add(0xF0, BEQ, RELATIVE, branching).branching() + add(0x82, BRL, RELATIVE_LONG, alwaysBranching).stop().branching() + + add(0x4C, JMP, ABSOLUTE, jumping).linking().stop() + add(0x5C, JML, ABSOLUTE_LONG, jumping).linking().stop() + add(0x6C, JMP, ABSOLUTE_INDIRECT, dynamicJumping).stop() + add(0x7C, JMP, ABSOLUTE_X_INDIRECT, dynamicJumping).stop() + add(0xDC, JMP, ABSOLUTE_INDIRECT_LONG, dynamicJumping).stop() + + add(0x22, JSL, ABSOLUTE_LONG, subJumping).linking().mayStop() + add(0x20, JSR, ABSOLUTE, subJumping).linking().mayStop() + add(0xFC, JSR, ABSOLUTE_X_INDIRECT, dynamicSubJumping).mayStop() + + add(0x60, RTS, IMPLIED, returning).stop() + add(0x6B, RTL, IMPLIED, returning).stop() + add(0x40, RTI, IMPLIED, returning).stop() + + add(0x1B, TCS, IMPLIED, alwaysContinue) + add(0x3B, TSC, IMPLIED, alwaysContinue) + add(0x5B, TCD, IMPLIED, alwaysContinue) + add(0x7B, TDC, IMPLIED, alwaysContinue) + add(0xAA, TAX, IMPLIED, alwaysContinue) + add(0xA8, TAY, IMPLIED, alwaysContinue) + add(0xBA, TSX, IMPLIED, alwaysContinue) + add(0x8A, TXA, IMPLIED, alwaysContinue) + add(0x9A, TXS, IMPLIED, alwaysContinue) + add(0x9B, TXY, IMPLIED, alwaysContinue) + add(0x98, TYA, IMPLIED, alwaysContinue) + add(0xBB, TYX, IMPLIED, alwaysContinue) + add(0xEB, XBA, IMPLIED, alwaysContinue) + + add(0x18, CLC, IMPLIED, alwaysContinue) + add(0x38, SEC, IMPLIED, alwaysContinue) + add(0x58, CLI, IMPLIED, alwaysContinue) + add(0x78, SEI, IMPLIED, alwaysContinue) + 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(0xFB, XCE, IMPLIED, alwaysContinue) + + add(0xC1, CMP, DIRECT_X_INDIRECT, alwaysContinue) + add(0xC3, CMP, DIRECT_S, alwaysContinue) + add(0xC5, CMP, DIRECT, alwaysContinue) + add(0xC7, CMP, DIRECT_INDIRECT_LONG, alwaysContinue) + add(0xC9, CMP, IMMEDIATE_M, alwaysContinue) + add(0xCD, CMP, ABSOLUTE, alwaysContinue) + add(0xCF, CMP, ABSOLUTE_LONG, alwaysContinue) + add(0xD1, CMP, DIRECT_INDIRECT_Y, alwaysContinue) + add(0xD2, CMP, DIRECT_INDIRECT, alwaysContinue) + add(0xD3, CMP, DIRECT_S_INDIRECT_Y, alwaysContinue) + add(0xD5, CMP, DIRECT_X, alwaysContinue) + add(0xD7, CMP, DIRECT_INDIRECT_LONG_Y, alwaysContinue) + add(0xD9, CMP, ABSOLUTE_Y, alwaysContinue) + add(0xDD, CMP, ABSOLUTE_X, alwaysContinue) + add(0xDF, CMP, ABSOLUTE_LONG_X, alwaysContinue) + add(0xE0, CPX, IMMEDIATE_X, alwaysContinue) + add(0xE4, CPX, DIRECT, alwaysContinue) + add(0xEC, CPX, ABSOLUTE, alwaysContinue) + add(0xC0, CPY, IMMEDIATE_X, alwaysContinue) + add(0xC4, CPY, DIRECT, alwaysContinue) + add(0xCC, CPY, ABSOLUTE, alwaysContinue) + + add(0xA1, LDA, DIRECT_X_INDIRECT, alwaysContinue) + add(0xA3, LDA, DIRECT_S, alwaysContinue) + add(0xA5, LDA, DIRECT, alwaysContinue) + add(0xA7, LDA, DIRECT_INDIRECT_LONG, alwaysContinue) + add(0xA9, LDA, IMMEDIATE_M, alwaysContinue) + add(0xAD, LDA, ABSOLUTE, alwaysContinue) + add(0xAF, LDA, ABSOLUTE_LONG, alwaysContinue) + add(0xB1, LDA, DIRECT_INDIRECT_Y, alwaysContinue) + add(0xB2, LDA, DIRECT_INDIRECT, alwaysContinue) + add(0xB3, LDA, DIRECT_S_INDIRECT_Y, alwaysContinue) + add(0xB5, LDA, DIRECT_X, alwaysContinue) + add(0xB7, LDA, DIRECT_INDIRECT_LONG_Y, alwaysContinue) + add(0xB9, LDA, ABSOLUTE_Y, alwaysContinue) + add(0xBD, LDA, ABSOLUTE_X, alwaysContinue) + add(0xBF, LDA, ABSOLUTE_LONG_X, alwaysContinue) + add(0xA2, LDX, IMMEDIATE_X, alwaysContinue) + add(0xA6, LDX, DIRECT, alwaysContinue) + add(0xAE, LDX, ABSOLUTE, alwaysContinue) + add(0xB6, LDX, DIRECT_Y, alwaysContinue) + add(0xBE, LDX, ABSOLUTE_Y, alwaysContinue) + add(0xA0, LDY, IMMEDIATE_X, alwaysContinue) + add(0xA4, LDY, DIRECT, alwaysContinue) + add(0xAC, LDY, ABSOLUTE, alwaysContinue) + add(0xB4, LDY, DIRECT_X, alwaysContinue) + add(0xBC, LDY, ABSOLUTE_X, alwaysContinue) + add(0x81, STA, DIRECT_X_INDIRECT, alwaysContinue) + add(0x83, STA, DIRECT_S, alwaysContinue) + add(0x85, STA, DIRECT, alwaysContinue) + add(0x87, STA, DIRECT_INDIRECT_LONG, alwaysContinue) + add(0x8D, STA, ABSOLUTE, alwaysContinue) + add(0x8F, STA, ABSOLUTE_LONG, alwaysContinue) + add(0x91, STA, DIRECT_INDIRECT_Y, alwaysContinue) + add(0x92, STA, DIRECT_INDIRECT, alwaysContinue) + add(0x93, STA, DIRECT_S_INDIRECT_Y, alwaysContinue) + add(0x95, STA, DIRECT_X, alwaysContinue) + add(0x97, STA, DIRECT_INDIRECT_LONG_Y, alwaysContinue) + add(0x99, STA, ABSOLUTE_Y, alwaysContinue) + add(0x9D, STA, ABSOLUTE_X, alwaysContinue) + add(0x9F, STA, ABSOLUTE_LONG_X, alwaysContinue) + add(0x86, STX, DIRECT, alwaysContinue) + add(0x8E, STX, ABSOLUTE, alwaysContinue) + add(0x96, STX, DIRECT_Y, alwaysContinue) + add(0x84, STY, DIRECT, alwaysContinue) + add(0x8C, STY, ABSOLUTE, alwaysContinue) + add(0x94, STY, DIRECT_X, alwaysContinue) + add(0x64, STZ, DIRECT, alwaysContinue) + add(0x74, STZ, DIRECT_X, alwaysContinue) + add(0x9C, STZ, ABSOLUTE, alwaysContinue) + add(0x9E, STZ, ABSOLUTE_X, alwaysContinue) + + add(0x48, PHA, IMPLIED, alwaysContinue) { it.preState.pushUnknown(it.preState.mWidth) } + add(0xDA, PHX, IMPLIED, alwaysContinue) { it.preState.pushUnknown(it.preState.xWidth) } + add(0x5A, PHY, IMPLIED, alwaysContinue) { it.preState.pushUnknown(it.preState.xWidth) } + add(0x68, PLA, IMPLIED, alwaysContinue) { it.preState.pull(it.preState.mWidth) } + 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(0x08, PHP, IMPLIED, alwaysContinue) { it.preState.push(it.preState.flags) } + add(0x28, PLP, IMPLIED, alwaysContinue) { it.preState.pull { copy(flags = it) } } + + add(0x3A, DEC, IMPLIED, alwaysContinue) + add(0xC6, DEC, DIRECT, alwaysContinue) + add(0xCE, DEC, ABSOLUTE, alwaysContinue) + add(0xD6, DEC, DIRECT_X, alwaysContinue) + add(0xDE, DEC, ABSOLUTE_X, alwaysContinue) + add(0xCA, DEX, IMPLIED, alwaysContinue) + add(0x88, DEY, IMPLIED, alwaysContinue) + add(0x1A, INC, IMPLIED, alwaysContinue) + add(0xE6, INC, DIRECT, alwaysContinue) + add(0xEE, INC, ABSOLUTE, alwaysContinue) + add(0xF6, INC, DIRECT_X, alwaysContinue) + add(0xFE, INC, ABSOLUTE_X, alwaysContinue) + add(0xE8, INX, IMPLIED, alwaysContinue) + add(0xC8, INY, IMPLIED, alwaysContinue) + + add(0x06, ASL, DIRECT, alwaysContinue) + add(0x0A, ASL, IMPLIED, alwaysContinue) + add(0x0E, ASL, ABSOLUTE, alwaysContinue) + add(0x16, ASL, DIRECT_X, alwaysContinue) + add(0x1E, ASL, ABSOLUTE_X, alwaysContinue) + add(0x46, LSR, DIRECT, alwaysContinue) + add(0x4A, LSR, IMPLIED, alwaysContinue) + add(0x4E, LSR, ABSOLUTE, alwaysContinue) + add(0x56, LSR, DIRECT_X, alwaysContinue) + add(0x5E, LSR, ABSOLUTE_X, alwaysContinue) + add(0x26, ROL, DIRECT, alwaysContinue) + add(0x2A, ROL, IMPLIED, alwaysContinue) + add(0x2E, ROL, ABSOLUTE, alwaysContinue) + add(0x36, ROL, DIRECT_X, alwaysContinue) + add(0x3E, ROL, ABSOLUTE_X, alwaysContinue) + add(0x66, ROR, DIRECT, alwaysContinue) + add(0x6A, ROR, IMPLIED, alwaysContinue) + add(0x6E, ROR, ABSOLUTE, alwaysContinue) + add(0x76, ROR, DIRECT_X, alwaysContinue) + add(0x7E, ROR, ABSOLUTE_X, alwaysContinue) + + add(0x61, ADC, DIRECT_X_INDIRECT, alwaysContinue) + add(0x63, ADC, DIRECT_S, alwaysContinue) + add(0x65, ADC, DIRECT, alwaysContinue) + add(0x67, ADC, DIRECT_INDIRECT_LONG, alwaysContinue) + add(0x69, ADC, IMMEDIATE_M, alwaysContinue) + add(0x6D, ADC, ABSOLUTE, alwaysContinue) + add(0x6F, ADC, ABSOLUTE_LONG, alwaysContinue) + add(0x71, ADC, DIRECT_INDIRECT_Y, alwaysContinue) + add(0x72, ADC, DIRECT_INDIRECT, alwaysContinue) + add(0x73, ADC, DIRECT_S_INDIRECT_Y, alwaysContinue) + add(0x75, ADC, DIRECT_X, alwaysContinue) + add(0x77, ADC, DIRECT_INDIRECT_LONG_Y, alwaysContinue) + add(0x79, ADC, ABSOLUTE_Y, alwaysContinue) + add(0x7D, ADC, ABSOLUTE_X, alwaysContinue) + add(0x7F, ADC, ABSOLUTE_LONG_X, alwaysContinue) + add(0xE1, SBC, DIRECT_X_INDIRECT, alwaysContinue) + add(0xE3, SBC, DIRECT_S, alwaysContinue) + add(0xE5, SBC, DIRECT, alwaysContinue) + add(0xE7, SBC, DIRECT_INDIRECT_LONG, alwaysContinue) + add(0xE9, SBC, IMMEDIATE_M, alwaysContinue) + add(0xED, SBC, ABSOLUTE, alwaysContinue) + add(0xEF, SBC, ABSOLUTE_LONG, alwaysContinue) + add(0xF1, SBC, DIRECT_INDIRECT_Y, alwaysContinue) + add(0xF2, SBC, DIRECT_INDIRECT, alwaysContinue) + add(0xF3, SBC, DIRECT_S_INDIRECT_Y, alwaysContinue) + add(0xF5, SBC, DIRECT_X, alwaysContinue) + add(0xF7, SBC, DIRECT_INDIRECT_LONG_Y, alwaysContinue) + add(0xF9, SBC, ABSOLUTE_Y, alwaysContinue) + add(0xFD, SBC, ABSOLUTE_X, alwaysContinue) + add(0xFF, SBC, ABSOLUTE_LONG_X, alwaysContinue) + + add(0x21, AND, DIRECT_X_INDIRECT, alwaysContinue) + add(0x23, AND, DIRECT_S, alwaysContinue) + add(0x25, AND, DIRECT, alwaysContinue) + add(0x27, AND, DIRECT_INDIRECT_LONG, alwaysContinue) + add(0x29, AND, IMMEDIATE_M, alwaysContinue) + add(0x2D, AND, ABSOLUTE, alwaysContinue) + add(0x2F, AND, ABSOLUTE_LONG, alwaysContinue) + add(0x31, AND, DIRECT_INDIRECT_Y, alwaysContinue) + add(0x32, AND, DIRECT_INDIRECT, alwaysContinue) + add(0x33, AND, DIRECT_S_INDIRECT_Y, alwaysContinue) + add(0x35, AND, DIRECT_X, alwaysContinue) + add(0x37, AND, DIRECT_INDIRECT_LONG_Y, alwaysContinue) + add(0x39, AND, ABSOLUTE_Y, alwaysContinue) + add(0x3D, AND, ABSOLUTE_X, alwaysContinue) + add(0x3F, AND, ABSOLUTE_LONG_X, alwaysContinue) + add(0x41, EOR, DIRECT_X_INDIRECT, alwaysContinue) + add(0x43, EOR, DIRECT_S, alwaysContinue) + add(0x45, EOR, DIRECT, alwaysContinue) + add(0x47, EOR, DIRECT_INDIRECT_LONG, alwaysContinue) + add(0x49, EOR, IMMEDIATE_M, alwaysContinue) + add(0x4D, EOR, ABSOLUTE, alwaysContinue) + add(0x4F, EOR, ABSOLUTE_LONG, alwaysContinue) + add(0x51, EOR, DIRECT_INDIRECT_Y, alwaysContinue) + add(0x52, EOR, DIRECT_INDIRECT, alwaysContinue) + add(0x53, EOR, DIRECT_S_INDIRECT_Y, alwaysContinue) + add(0x55, EOR, DIRECT_X, alwaysContinue) + add(0x57, EOR, DIRECT_INDIRECT_LONG_Y, alwaysContinue) + add(0x59, EOR, ABSOLUTE_Y, alwaysContinue) + add(0x5D, EOR, ABSOLUTE_X, alwaysContinue) + add(0x5F, EOR, ABSOLUTE_LONG_X, alwaysContinue) + add(0x01, ORA, DIRECT_X_INDIRECT, alwaysContinue) + add(0x03, ORA, DIRECT_S, alwaysContinue) + add(0x05, ORA, DIRECT, alwaysContinue) + add(0x07, ORA, DIRECT_INDIRECT_LONG, alwaysContinue) + add(0x09, ORA, IMMEDIATE_M, alwaysContinue) + add(0x0D, ORA, ABSOLUTE, alwaysContinue) + add(0x0F, ORA, ABSOLUTE_LONG, alwaysContinue) + add(0x11, ORA, DIRECT_INDIRECT_Y, alwaysContinue) + add(0x12, ORA, DIRECT_INDIRECT, alwaysContinue) + add(0x13, ORA, DIRECT_S_INDIRECT_Y, alwaysContinue) + add(0x15, ORA, DIRECT_X, alwaysContinue) + add(0x17, ORA, DIRECT_INDIRECT_LONG_Y, alwaysContinue) + add(0x19, ORA, ABSOLUTE_Y, alwaysContinue) + add(0x1D, ORA, ABSOLUTE_X, alwaysContinue) + add(0x1F, ORA, ABSOLUTE_LONG_X, alwaysContinue) + + add(0x14, TRB, DIRECT, alwaysContinue) + add(0x1C, TRB, ABSOLUTE, alwaysContinue) + add(0x04, TSB, DIRECT, alwaysContinue) + add(0x0C, TSB, ABSOLUTE, alwaysContinue) + + add(0x24, BIT, DIRECT, alwaysContinue) + add(0x2C, BIT, ABSOLUTE, alwaysContinue) + add(0x34, BIT, DIRECT_X, alwaysContinue) + add(0x3C, BIT, ABSOLUTE_X, alwaysContinue) + add(0x89, BIT, IMMEDIATE_M, alwaysContinue) + + add(0x54, MVN, BLOCK_MOVE, alwaysContinue) + add(0x44, MVP, BLOCK_MOVE, alwaysContinue) + + add(0xF4, PEA, IMMEDIATE_16, alwaysContinue) + add(0xD4, PEI, DIRECT, alwaysContinue) + add(0x62, PER, RELATIVE_LONG, alwaysContinue) + + OPCODES = Array(256) { ocs[it]!! } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/smallhacker/disbrowser/asm/RomData.kt b/src/main/java/com/smallhacker/disbrowser/asm/RomData.kt new file mode 100644 index 0000000..9519667 --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/asm/RomData.kt @@ -0,0 +1,44 @@ +package com.smallhacker.disbrowser.asm + +import com.smallhacker.disbrowser.util.* +import java.nio.file.Files +import java.nio.file.Path + +interface RomData { + val size: Int + + operator fun get(address: Int): UByte + + fun range(start: Int, length: Int): RomData = RomRange(this, start, length) + + fun asSequence(): Sequence = (0 until size).asSequence().map { get(it) } + + companion object { + fun load(path: Path): RomData = Rom(Files.readAllBytes(path)) + } +} + +fun RomData.getWord(address: Int): UWord = get(address).toWord() or (get(address + 1).toWord() left 8) + +private class Rom(private val bytes: ByteArray): RomData { + override val size = bytes.size + + override fun get(address: Int): UByte { + checkRange(address) + return uByte(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 fun RomData.checkRange(address: Int) { + if (address < 0 || address >= size) { + throw IndexOutOfBoundsException("Index $address out of range: [0, $size)") + } +} diff --git a/src/main/java/com/smallhacker/disbrowser/asm/Segment.kt b/src/main/java/com/smallhacker/disbrowser/asm/Segment.kt new file mode 100644 index 0000000..e806a0c --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/asm/Segment.kt @@ -0,0 +1,29 @@ +package com.smallhacker.disbrowser.asm + +class Segment (val start: Address, val end: SegmentEnd, val instructions: List) + +class SegmentEnd(val address: Address, val local: List = emptyList(), val remote: List = emptyList(), val returnAddress: Address? = null, val returning: Boolean = false) + +fun stoppingSegmentEnd(address: Address) + = SegmentEnd(address) + +fun branchingSegmentEnd(address: Address, continueState: State, branchState: State) + = SegmentEnd(address, local = listOf(continueState, branchState)) + +fun alwaysBranchingSegmentEnd(address: Address, branchState: State) + = SegmentEnd(address, local = listOf(branchState)) + +fun jumpSegmentEnd(address: Address, targetState: State) + = SegmentEnd(address, remote = listOf(targetState)) + +fun subroutineSegmentEnd(address: Address, targetState: State, returnAddress: Address) + = SegmentEnd(address, remote = listOf(targetState), returnAddress = returnAddress) + +fun returnSegmentEnd(address: Address) + = SegmentEnd(address, returning = true) + +fun continuationSegmentEnd(state: State) + = SegmentEnd(state.address, local = listOf(state)) + +fun Segment.toDisassembly() = Disassembly(instructions) + diff --git a/src/main/java/com/smallhacker/disbrowser/asm/State.kt b/src/main/java/com/smallhacker/disbrowser/asm/State.kt new file mode 100644 index 0000000..be2d4ea --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/asm/State.kt @@ -0,0 +1,114 @@ +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 mWidth: Int? get() = toWidth(m) + val xWidth: Int? get() = toWidth(x) + + fun sep(i: UByte) = withFlags(flags.withBits(i.value)) + fun rep(i: UByte) = withFlags(flags.withoutBits(i.value)) + fun uncertain() = withFlags(VagueNumber()) + + private fun withFlags(flags: VagueNumber) = copy(flags = flags) + + 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: VagueNumber) = copy(stack = stack.push(value)) + fun pushUnknown(count: Int? = 1): State { + if (count == null) { + return copy(stack = immStack()) + } + + var stack = this.stack + for (i in 1..count) { + stack = stack.push(VagueNumber()) + } + return copy(stack = stack) + } + + fun pull() = (stack.top ?: VagueNumber()) to copy(stack = stack.pop()) + fun pull(count: Int?): State { + if (count == null) { + return copy(stack = immStack()) + } + + var stack = this.stack + for (i in 1..count) { + stack = stack.pop() + } + return copy(stack = stack) + } + + fun clearStack() = copy(stack = immStack()) + + override fun toString(): String { + return "A:${printSize(m)} XY:${printSize(x)} S:" + stackToString() + } + + private fun stackToString(): String { + return stack.reversed().asSequence() + .map { stackByteToString(it) } + .joinToString(" ") + } + + private fun stackByteToString(v: VagueNumber): String { + if (v.certain) { + return String.format("%02x", v.value) + } + + if (v.certainty == 0) { + return "??" + } + + val c = v.certainty + val high = (c and 0xF0) != 0 + val low = (c and 0x0F) != 0 + + 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 "?") + .toString() + } + + private fun printSize(flag: Boolean?): String = when (flag) { + null -> "??" + true -> " 8" + false -> "16" + } + + val urlString: String + get() { + val out = StringBuilder() + out.append(when (x) { + null -> "" + true -> "X" + false -> "x" + }) + out.append(when (m) { + null -> "" + true -> "M" + false -> "m" + }) + return out.toString() + } + + private fun toWidth(flag: Boolean?): Int? = when (flag) { + null -> null + true -> 1 + false -> 2 + } +} + +fun State.pushByte(value: Byte) = this.push(VagueNumber(value.toInt())) +fun State.pull(consumer: State.(VagueNumber) -> State): State { + val (value, state) = this.pull() + return consumer(state, value) +} \ No newline at end of file diff --git a/src/main/java/com/smallhacker/disbrowser/asm/VagueNumber.kt b/src/main/java/com/smallhacker/disbrowser/asm/VagueNumber.kt new file mode 100644 index 0000000..06f5cdd --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/asm/VagueNumber.kt @@ -0,0 +1,40 @@ +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) + + 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 certain: Boolean + get() = certainty == -1 + + fun get(mask: Int): Int? { + if ((certainty and mask) != mask) { + return null + } + return value and mask + } + + fun getBoolean(mask: Int): Boolean? { + val value = get(mask) ?: return null + return value == mask + } + + override fun toString(): String { + var i = 1 shl 31 + val out = StringBuilder() + while (i != 0) { + val b = getBoolean(i) + when (b) { + true -> out.append('1') + false -> out.append('0') + null -> out.append('?') + } + i = i ushr 1 + } + return out.toString() + } +} \ No newline at end of file diff --git a/src/main/java/com/smallhacker/disbrowser/disassembler/Disassembler.kt b/src/main/java/com/smallhacker/disbrowser/disassembler/Disassembler.kt new file mode 100644 index 0000000..1f35453 --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/disassembler/Disassembler.kt @@ -0,0 +1,167 @@ +package com.smallhacker.disbrowser.disassembler + +import com.smallhacker.disbrowser.asm.* +import java.util.* +import kotlin.collections.ArrayList + +object Disassembler { + fun disassemble(initialState: State, metadata: Metadata, global: Boolean): Disassembly { + val seen = HashSet
() + val queue = ArrayDeque() + + fun tryAdd(state: State) { + if (seen.add(state.address)) { + queue.add(state) + } + } + tryAdd(initialState) + + val instructions = ArrayList() + while (queue.isNotEmpty()) { + val state = queue.remove() + + val ins = disassembleInstruction(state) + instructions.add(ins) + + var stop = (ins.opcode.continuation == Continuation.NO) or + (ins.opcode.mode.instructionLength(state) == null) + + metadata[ins.address]?.flags?.forEach { + if (it is JmpIndirectLongInterleavedTable) { + if (global) { + it.readTable(state.data) + .map { ins.postState.copy(address = it) } + .forEach { tryAdd(it) } + } + stop = true + } else if (it is JslTableRoutine) { + if (global) { + it.readTable(ins.postState) + .map { ins.postState.copy(address = it) } + .forEach { tryAdd(it) } + } + stop = true + } + } + + val linkedState = ins.linkedState + + if (linkedState != null) { + metadata[linkedState.address]?.flags?.forEach { + if (it === NonReturningRoutine) { + stop = true + println(ins.address.toFormattedString()) + } + } + } + + if (!stop) { + tryAdd(ins.postState) + } + + + if (linkedState != null) { + if (ins.opcode.branch || global) { + tryAdd(linkedState) + } + } + } + + val instructionList = instructions + .sortedBy { it.address } + .toList() + + return Disassembly(instructionList) + } + + fun disassembleSegments(initialState: State): List { + val mapping = HashMap() + val queue = ArrayDeque() + + val segments = ArrayList() + + fun tryAdd(state: State) { + if (!mapping.containsKey(state.address)) { + queue.add(state) + } + } + tryAdd(initialState) + + + while (queue.isNotEmpty()) { + val state = queue.remove() + + if (mapping.containsKey(state.address)) { + continue + } + + val segment = disassembleSegment(state, mapping) + if (segment.instructions.isEmpty()) { + continue + } + + segments.add(segment) + + val end = segment.end + end.local.forEach { queue.add(it) } + + end.remote.forEach { + + } + + + } + + return segments + } + + fun disassembleSegment(initialState: State, mapping: MutableMap): Segment { + val instructions = ArrayList() + var lastState = initialState + + val queue = ArrayDeque() + val seen = HashSet
() + + fun finalize(segment: Segment): Segment { + instructions.forEach { + mapping[it.address] = segment + } + return segment + } + + fun tryAdd(state: State) { + if (seen.add(state.address)) { + queue.add(state) + } + } + tryAdd(initialState) + + while (queue.isNotEmpty()) { + val state = queue.remove() + + val ins = disassembleInstruction(state) + instructions.add(ins) + println(ins) + + val segmentEnd = ins.opcode.ender(ins) + + if (segmentEnd != null) { + + return finalize(Segment(initialState.address, segmentEnd, instructions)) + } + + tryAdd(ins.postState) + lastState = ins.postState + } + + return finalize(Segment(initialState.address, continuationSegmentEnd(lastState), instructions)) + } + + 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 bytes = state.data.range(pc, length) + return Instruction(bytes, opcode, state) + } +} diff --git a/src/main/java/com/smallhacker/disbrowser/util/UVal.kt b/src/main/java/com/smallhacker/disbrowser/util/UVal.kt new file mode 100644 index 0000000..1dc636a --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/util/UVal.kt @@ -0,0 +1,59 @@ +package com.smallhacker.disbrowser.util + +import java.util.* + +abstract class UType(val bytes: Int, val name: String) { + val mask = (1 shl (bytes * 8)) - 1 +} + +class UVal(value: Int, val type: U) : Comparable> { + val value = value and type.mask + + override fun equals(other: Any?) = equalsBy(other, { value }, { type }) + + override fun toString() = "${type.name}(${toHex(true)})" + + fun toHex(prefix: Boolean = false): String { + val digits = 2 * type.bytes + val start = if (prefix) "0x" else "" + val pattern = "%0${digits}x" + return start + String.format(pattern, value) + } + + override fun compareTo(other: UVal): Int = Integer.compare(value, other.value) + + override fun hashCode() = Objects.hash(value, type) + + //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") +} + +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 + +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) } + +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) + +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 } +inline infix fun , V: UType> U.and(other: U): U = mutate { it and other.value } +inline infix fun , V: UType> U.or(other: U): U = mutate { it or other.value } +inline operator fun > U.not(): U = mutate { it.inv() } + +inline infix operator fun , V: UType> U.plus(other: U): U = mutate { it + other.value } +inline infix operator fun , V: UType> U.minus(other: U): U = mutate { it - other.value } diff --git a/src/main/java/com/smallhacker/disbrowser/util/misc.kt b/src/main/java/com/smallhacker/disbrowser/util/misc.kt new file mode 100644 index 0000000..fb922f6 --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/util/misc.kt @@ -0,0 +1,16 @@ +package com.smallhacker.disbrowser.util + +inline fun T.equalsBy(other: Any?, vararg values: T.() -> Any?) = when (other) { + !is T -> false + else -> values.asSequence() + .map { it(this) to it(other) } + .all { it.first == it.second } +} + +fun tryParseInt(s: String, radix: Int = 10): Int? { + return try { + Integer.parseInt(s, radix) + } catch (e: NumberFormatException) { + null + } +} diff --git a/src/main/resources/main.js b/src/main/resources/main.js new file mode 100644 index 0000000..01b2fab --- /dev/null +++ b/src/main/resources/main.js @@ -0,0 +1,48 @@ +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/style.css new file mode 100644 index 0000000..1e714ee --- /dev/null +++ b/src/main/resources/style.css @@ -0,0 +1,95 @@ +table { + border-spacing: 0; + font-family: monospace; + white-space: pre +} + +td { + padding-right: 12px; +} + +.code-opcode { + width: 200px +} + +tr:nth-child(2n) { + background-color: #eee +} + +tr { + transition: background-color 1000ms 1500ms; +} + +tr.line-active { + background-color: yellow !important; + transition: none; +} + +.arrow { + width: 10px; + position: relative; + padding-right: 0; +} + +.arrow::before { + content: ""; + display: block; + position: absolute; + top: 0; + bottom: 0; + right: 2px; + width: 2px; + background: blue; +} + +.arrow-up-start::after, +.arrow-up-end::after, +.arrow-down-start::after, +.arrow-down-end::after { + content: ""; + background: blue; + height: 2px; + left: 25%; + right: 2px; + top: 50%; + margin: -1px 0; + position: absolute; +} + +.arrow-down-end::before, +.arrow-up-end::before { + bottom: 50%; + margin-bottom: -1px; + +} + +.arrow-down-start::before, +.arrow-up-start::before { + top: 50%; + margin-top: -1px; +} + +.arrow-head { + position: absolute; + left: 0; + top: 50%; + width: 0; + height: 0; + border: 5px solid transparent; + border-right-color: blue; + margin: -5px; +} + +.arrow-link { + width: 6px; + height: 6px; + background-color: blue; + display: inline-block; + border-radius: 100px; + z-index: 1; + position: relative; +} + +.routine-end > td { + border-bottom: 1px solid black; +} \ No newline at end of file