This commit is contained in:
Smallhacker 2019-01-10 22:19:08 -05:00
parent 7a4b94e907
commit 188d53a768
31 changed files with 941 additions and 517 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
/.idea
/target
/src/main/resources/public/disbrowser.*

View File

@ -0,0 +1,64 @@
[
{
"address": 32768,
"label": "ResetVector",
"comment": null,
"flags": []
},
{
"address": 32820,
"label": "MainGameLoop",
"comment": null,
"flags": []
},
{
"address": 32949,
"label": "JumpToGameMode",
"comment": null,
"flags": []
},
{
"address": 165840,
"label": null,
"comment": null,
"flags": [
{
"flagType": "JslTableRoutine",
"entries": 4
}
]
},
{
"address": 32966,
"label": null,
"comment": null,
"flags": [
{
"flagType": "JmpIndirectLongInterleavedTable",
"start": 32865,
"entries": 28
}
]
},
{
"address": 835861,
"label": null,
"comment": null,
"flags": [
{
"flagType": "JslTableRoutine",
"entries": 12
}
]
},
{
"address": 34716,
"label": null,
"comment": null,
"flags": [
{
"flagType": "NonReturningRoutine"
}
]
}
]

21
pom.xml
View File

@ -22,6 +22,26 @@
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-grizzly2-http</artifactId>
@ -134,5 +154,6 @@
<jersey.version>2.27</jersey.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<kotlin.version>1.3.11</kotlin.version>
<jackson.version>2.9.8</jackson.version>
</properties>
</project>

View File

@ -59,28 +59,34 @@ class Grid {
.first()
}
fun add(ins: Instruction, metadata: MetadataLine?, disassembly: Disassembly) {
val address = ins.address
fun add(ins: CodeUnit, metadata: Metadata, disassembly: Disassembly) {
val insMetadata = ins.address ?.let { metadata[it] }
val actualAddress = ins.address
val presentedAddress = ins.presentedAddress
if (nextAddress != null) {
if (address != nextAddress) {
if (presentedAddress != nextAddress) {
addDummy()
}
}
nextAddress = ins.postState.address
nextAddress = ins.nextPresentedAddress
val y = (height++)
addresses[address] = y
addresses[presentedAddress] = y
add(y, ins.address,
text(ins.address.toFormattedString()),
text(actualAddress?.toFormattedString() ?: ""),
text(ins.bytesToString()),
text(metadata?.label?.plus(":") ?: ""),
input(value = insMetadata?.label ?: "")
.addClass("field-label")
.addClass("field-editable")
.attr("data-field", "label")
.attr("data-address", presentedAddress.value.toString()),
fragment {
text("${ins.opcode.mnemonic}")
text(ins.lengthSuffix)
text(ins.printOpcodeAndSuffix())
text(" ")
val operands = ins.opcode.mode.print(ins)
var operands = ins.printOperands()
val link = ins.linkedState
if (link == null) {
@ -93,13 +99,19 @@ class Grid {
else -> "/${link.address.toSimpleString()}/${link.urlString}"
}
operands = metadata[link.address]?.label ?: operands
a {
text(operands)
}.attr("href", url)
}
},
text(ins.postState.toString())
text(ins.postState?.toString() ?: ""),
input(value = insMetadata?.comment ?: "")
.addClass("field-comment")
.addClass("field-editable")
.attr("data-field", "comment")
.attr("data-address", presentedAddress.value.toString())
)
if (ins.opcode.continuation == Continuation.NO) {
@ -109,7 +121,7 @@ class Grid {
private fun addDummy() {
val y = (height++)
add(y, null, null, null, null, text("..."), null)
add(y, null, null, null, null, text("..."), null, null)
}
private fun add(y: Int, address: Address?,
@ -117,7 +129,8 @@ class Grid {
cBytes: HtmlNode?,
cLabel: HtmlNode?,
cCode: HtmlNode?,
cState: HtmlNode?
cState: HtmlNode?,
cComment: HtmlNode?
) {
if (address != null) {
rowId[y] = address.toSimpleString()
@ -127,9 +140,15 @@ class Grid {
content[2 to y] = cLabel
content[3 to y] = cCode
content[4 to y] = cState
content[5 to y] = cComment
}
fun output(): HtmlNode {
val contentMaxX = content.keys.asSequence()
.map { it.first }
.max()
?: -1
return table {
for (y in 0 until height) {
tr {
@ -145,7 +164,7 @@ class Grid {
arrowCells[x to y]?.appendTo(parent)
}.addClass(cssClass)
}
for (x in 4..4) {
for (x in 4..contentMaxX) {
val cssClass = cellClasses[x to y]
td {
content[x to y]?.appendTo(parent)

View File

@ -107,6 +107,8 @@ fun a(inner: InnerHtml = {}) = parent("a", inner)
fun HtmlArea.a(inner: InnerHtml = {}) = com.smallhacker.disbrowser.a(inner).appendTo(parent)
fun script(inner: InnerHtml = {}) = parent("script", inner)
fun HtmlArea.script(inner: InnerHtml = {}) = com.smallhacker.disbrowser.script(inner).appendTo(parent)
fun input(type: String = "text", value: String = "") = leaf("input").attr("type", type).attr("value", value)
fun HtmlArea.input(type: String = "text", value: String = "") = com.smallhacker.disbrowser.input(type, value).appendTo(parent)
fun HtmlNode.appendTo(node: HtmlNode) = apply { node.append(this) }
fun HtmlNode.addClass(c: String?) = attrAdd("class", c)

View File

@ -21,8 +21,8 @@ public class Main {
*/
public static HttpServer startServer() {
// create a resource config that scans for JAX-RS resources and providers
// in com.smallhacker.disbrowser package
final ResourceConfig rc = new ResourceConfig().packages("com.smallhacker.disbrowser");
// in com.smallhacker.disbrowser.resource package
final ResourceConfig rc = new ResourceConfig().packages("com.smallhacker.disbrowser.resource");
// create and start a new instance of grizzly http server
// exposing the Jersey application at BASE_URI

View File

@ -1,159 +0,0 @@
package com.smallhacker.disbrowser
import com.smallhacker.disbrowser.asm.*
import com.smallhacker.disbrowser.disassembler.Disassembler
import com.smallhacker.disbrowser.util.tryParseInt
import java.nio.file.Paths
import javax.ws.rs.GET
import javax.ws.rs.Path
import javax.ws.rs.PathParam
import javax.ws.rs.Produces
import javax.ws.rs.core.MediaType
import javax.ws.rs.core.Response
private val RESET_VECTOR_LOCATION = Address(0x00_FFFC)
@Path("/")
class MyResource {
private val romData = lazy {
//RomData.load(Paths.get("P:\\Emulation\\ROMs\\SNES\\Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"))
RomData.load(Paths.get("""P:\Emulation\ROMs\SNES\Super Mario World.sfc"""))
}
@GET
fun getIt(): Response {
return handle {
val resetVectorLocation = RESET_VECTOR_LOCATION
val initialAddress = Address(romData.value.getWord(resetVectorLocation.pc).value)
val flags = parseState("MX")
showDisassembly(initialAddress, flags)
}
}
@GET
@Path("{address}")
fun getIt(@PathParam("address") address: String): Response {
return handle {
parseAddress(address)?.let {
val flags = parseState("")
showDisassembly(it, flags)
}
}
}
@GET
@Path("{address}/{state}")
@Produces(MediaType.TEXT_HTML)
fun getIt(@PathParam("address") address: String, @PathParam("state") state: String): Response {
return handle {
parseAddress(address)?.let {
val flags = parseState(state)
showDisassembly(it, flags)
}
}
}
private fun parseAddress(address: String): Address? {
return tryParseInt(address, 16)
?.let { Address(it) }
}
private fun handle(runner: () -> HtmlNode?): Response {
try {
val disassembly = runner()
return if (disassembly == null)
Response.status(404).build()
else
Response.ok(disassembly.toString(), MediaType.TEXT_HTML).build()
} catch (e: Exception) {
e.printStackTrace()
throw e
}
}
private fun showDisassembly(initialAddress: Address, flags: VagueNumber): HtmlNode? {
val rom = romData.value
val metadata = getMetadata()
val initialState = State(data = rom, address = initialAddress, flags = flags)
val disassembly = Disassembler.disassemble(initialState, metadata, false)
return print(disassembly, metadata)
}
private fun parseState(state: String): VagueNumber {
var flags = VagueNumber()
flags = parseFlag(state, flags, 'M', 'm', 0x20)
flags = parseFlag(state, flags, 'X', 'x', 0x10)
return flags
}
@GET
@Path("resources/{file}.{ext}")
fun getCss(@PathParam("file") file: String, @PathParam("ext") ext: String): Response {
val mime = when (ext) {
"js" -> "application/javascript"
"css" -> "text/css"
else -> null
}
if (mime != null) {
javaClass.getResourceAsStream("/$file.$ext")
?.bufferedReader()
?.use {
return Response.ok(it.readText())
.type(mime)
.build()
}
}
return Response.status(404).build()
}
private fun parseFlag(state: String, flags: VagueNumber, set: Char, clear: Char, mask: Int): VagueNumber = when {
state.contains(set) -> flags.withBits(mask)
state.contains(clear) -> flags.withoutBits(mask)
else -> flags
}
private fun print(disassembly: Disassembly, metadata: Metadata): HtmlNode {
val grid = Grid()
disassembly.forEach { grid.add(it, metadata[it.address], disassembly) }
disassembly.asSequence()
.mapNotNull {
it.linkedState
?.let { link ->
it.address to link.address
}
}
.sortedBy { Math.abs(it.first.value - it.second.value) }
.forEach { grid.arrow(it.first, it.second) }
return html {
head {
title { text("Disassembly Browser") }
link {}.attr("rel", "stylesheet").attr("href", "/resources/style.css")
}
body {
grid.output().appendTo(parent)
script().attr("src", "/resources/main.js")
}
}
}
private fun getMetadata(): Metadata {
return metadata {
at(0x00_8000) { label = "RESET_VECTOR" }
at(0x00_80c6) { flags.add(JmpIndirectLongInterleavedTable(Address(0x00_8061), 28)) }
at(0x00_879c) { flags.add(NonReturningRoutine) }
at(0x02_87d0) { flags.add(JslTableRoutine(4)) }
at(0x0c_c115) { flags.add(JslTableRoutine(12)) }
//at(0x0c_c115) { stop = true }
}
}
}

View File

@ -0,0 +1,78 @@
package com.smallhacker.disbrowser
import com.smallhacker.disbrowser.asm.*
import com.smallhacker.disbrowser.disassembler.Disassembler
import com.smallhacker.disbrowser.util.jsonFile
import com.smallhacker.disbrowser.util.toUInt24
import java.nio.file.Paths
import kotlin.reflect.KMutableProperty1
private val RESET_VECTOR_LOCATION = address(0x00_FFFC)
object Service {
private val romName = "Zelda no Densetsu - Kamigami no Triforce (Japan)"
private val romDir = Paths.get("""P:\Emulation\ROMs\SNES""")
private val metaDir = Paths.get("""P:\Programming\dis-browser""")
private val metaFile = jsonFile<Metadata>(metaDir.resolve("$romName.json"))
private val metadata by lazy { metaFile.load() }
private val romData = lazy {
val path = romDir.resolve("$romName.sfc")
RomData.load(path)
}
fun showDisassemblyFromReset(): HtmlNode? {
val resetVectorLocation = RESET_VECTOR_LOCATION
val initialAddress = Address(romData.value.getWord(resetVectorLocation.pc).toUInt24())
val flags = VagueNumber(0x30u)
return showDisassembly(initialAddress, flags)
}
fun showDisassembly(initialAddress: Address, flags: VagueNumber): HtmlNode? {
val rom = romData.value
val initialState = State(data = rom, address = initialAddress, flags = flags)
val disassembly = Disassembler.disassemble(initialState, metadata, false)
return print(disassembly, metadata)
}
private fun print(disassembly: Disassembly, metadata: Metadata): HtmlNode {
val grid = Grid()
disassembly.forEach {
grid.add(it, metadata, disassembly)
}
disassembly.asSequence()
.mapNotNull {
it.linkedState
?.let { link ->
it.presentedAddress to link.address
}
}
.sortedBy { it.first distanceTo it.second }
.forEach { grid.arrow(it.first, it.second) }
return html {
head {
title { text("Disassembly Browser") }
link {}.attr("rel", "stylesheet").attr("href", "/resources/style.css")
}
body {
grid.output().appendTo(parent)
script().attr("src", "/resources/disbrowser.js")
}
}
}
fun updateMetadata(address: Address, field: KMutableProperty1<MetadataLine, String?>, value: String) {
if (value.isEmpty()) {
metadata[address]?.run {
field.set(this, null)
}
} else {
field.set(metadata.getOrAdd(address), value)
}
}
}

View File

@ -1,19 +1,36 @@
package com.smallhacker.disbrowser.asm
data class Address(val value: Int): Comparable<Address> {
val rom = (value and 0x8000) == 0
val pc = (value and 0x7FFF) or ((value and 0x7F_0000) shr 1)
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonValue
import com.smallhacker.disbrowser.util.UInt24
import com.smallhacker.disbrowser.util.toUInt24
data class Address(@JsonValue val value: UInt24): Comparable<Address> {
@JsonIgnore
val rom = (value and 0x8000u).toUInt() == 0u
@JsonIgnore
val pc = snesToPc(value)
operator fun plus(offset: Int) = Address(value + offset)
operator fun minus(offset: Int) = Address(value - offset)
operator fun inc() = plus(1)
operator fun dec() = plus(1)
operator fun dec() = minus(1)
override fun toString(): String = toFormattedString()
fun toFormattedString(): String = String.format("$%02x:%04x", value shr 16, value and 0xFFFF)
fun toSimpleString(): String = String.format("%06x", value)
fun toFormattedString(): String = String.format("$%02x:%04x", (value shr 16).toInt(), (value and 0xFFFFu).toInt())
fun toSimpleString(): String = String.format("%06x", value.toInt())
fun withinBank(value: Int): Address = Address((this.value and 0xFF_0000) or value)
fun withinBank(value: UShort): Address = Address((this.value and 0xFF_0000u) or value.toUInt24())
override fun compareTo(other: Address) = value.compareTo(other.value)
override fun compareTo(other: Address) = value.toUInt().compareTo(other.value.toUInt())
infix fun distanceTo(other: Address)= Math.abs(value.toInt() - other.value.toInt()).toUInt()
}
fun address(snesAddress: Int) = Address(snesAddress.toUInt24())
private fun snesToPc(value: UInt24): UInt {
// TODO: This is incredibly oversimplified. Anything that isn't a small LoROM will crash and burn
val intVal = value.toUInt()
return (intVal and 0x7FFFu) or ((intVal and 0x7F_0000u) shr 1)
}

View File

@ -1,13 +1,20 @@
package com.smallhacker.disbrowser.asm
class Disassembly(lines: List<Instruction>) : Iterable<Instruction> {
override fun iterator() = lines.values.iterator() as Iterator<Instruction>
class Disassembly(lines: List<CodeUnit>) : Iterable<CodeUnit> {
override fun iterator() = lineList.iterator() as Iterator<CodeUnit>
private val lines = LinkedHashMap<Address, Instruction>()
private val knownAddresses = HashSet<Address>()
private val lineList = ArrayList<CodeUnit>()
init {
lines.forEach { this.lines[it.address] = it }
lines.forEach {
val address = it.address
if (address != null) {
knownAddresses += address
}
lineList.add(it)
}
}
operator fun contains(address: Address) = address in lines
operator fun contains(address: Address) = address in knownAddresses
}

View File

@ -2,86 +2,107 @@ package com.smallhacker.disbrowser.asm
import com.smallhacker.disbrowser.util.*
class Instruction(val bytes: RomData, val opcode: Opcode, val preState: State) {
val address: Address
get() = preState.address
interface CodeUnit {
val address: Address?
val relativeAddress: Address
val presentedAddress: Address
val nextPresentedAddress: Address
val postState = opcode.mutate(this)
.mutateAddress { it + bytes.size }
val linkedState: State?
val preState: State?
val postState: State?
val bytes: RomData
val opcode: Opcode
val lengthSuffix: String?
fun operandByte(index: UInt): UByte = bytes[opcode.operandIndex + index]
fun printOpcodeAndSuffix(): String {
val mnemonic = opcode.mnemonic.displayName
val suffix = lengthSuffix ?: ""
return "$mnemonic$suffix"
}
fun printOperands() = opcode.mode.print(this)
fun bytesToString(): String {
return bytes.asSequence()
.map { toHex(it.toUInt(), 1u) }
.joinToString(" ")
.padEnd(11, ' ')
}
val operandLength: UInt?
val signedByte get() = byte.toByte()
val signedWord get() = word.toShort()
val byte get() = operandByte(0u)
val byte2 get() = operandByte(1u)
val word get() = joinBytes(operandByte(0u), operandByte(1u)).toUShort()
val long get() = joinBytes(operandByte(0u), operandByte(1u), operandByte(2u)).toUInt24()
val value
get() = when (operandLength?.toInt()) {
0 -> 0u
1 -> byte.toUInt()
2 -> word.toUInt()
3 -> long.toUInt()
else -> null
}
}
class DataBlock(
override val opcode: Opcode,
override val bytes: RomData,
override val presentedAddress: Address,
override val relativeAddress: Address,
override val linkedState: State?
) : CodeUnit {
override val nextPresentedAddress: Address
get() = presentedAddress + operandLength.toInt()
override val operandLength get() = bytes.size
override val address: Address? = null
override val preState: State? = null
override val postState: State? = null
override val lengthSuffix: String? = null
}
class Instruction(override val bytes: RomData, override val opcode: Opcode, override val preState: State) : CodeUnit {
override val address: Address get() = preState.address
override val relativeAddress get() = address
override val presentedAddress get() = address
override val nextPresentedAddress get() = postState.address
override val postState = opcode.mutate(this)
.mutateAddress { it + bytes.size.toInt() }
.withOrigin(this)
val linkedState = link()?.let { link ->
override val linkedState = link()?.let { link ->
opcode.mutate(this)
.mutateAddress { link }
.withOrigin(this)
}
operator fun get(index: Int): UByte {
return bytes[index]
}
val signedByte: Int
get() = byte.value.toByte().toInt()
val signedWord: Int
get() = word.value.toShort().toInt()
val byte: UByte
get() = get(1)
val byte2: UByte
get() = get(2)
val dataByte: UByte
get() = get(0)
val word: UWord
get() = (get(2).toWord() left 8) or get(1).toWord()
val dataWord: UWord
get() = (get(1).toWord() left 8) or get(0).toWord()
val long: ULong
get() = (get(3).toLong() left 16) or (get(2).toLong() left 8) or get(1).toLong()
val dataLong: ULong
get() = (get(2).toLong() left 16) or (get(1).toLong() left 8) or get(0).toLong()
fun bytesToString(): String {
return bytes.asSequence().map { it.toHex() }.joinToString(" ").padEnd(11, ' ')
}
//val value: ULong
// get() {
// val len = operandLength
// val start = opcode.operandIndex
//}
//fun getOperand(index: Int, length: Int) {
// val v = uLong(0)
// for (i in (index + length) downTo index)
//}
val lengthSuffix: String
override val lengthSuffix: String?
get() {
return when (opcode.mode) {
Mode.IMPLIED -> ""
Mode.IMMEDIATE_8 -> ""
Mode.IMMEDIATE_16 -> ""
Mode.RELATIVE -> ""
Mode.RELATIVE_LONG -> ""
Mode.BLOCK_MOVE -> ""
else -> when (operandLength) {
null -> ".?"
1 -> ".b"
2 -> ".w"
3 -> ".l"
else -> ""
}
if (!opcode.mode.showLengthSuffix) {
return null
}
return when (operandLength?.toInt()) {
null -> ".?"
1 -> ".b"
2 -> ".w"
3 -> ".l"
else -> null
}
}
private val operandLength
override val operandLength
get() = opcode.mode.operandLength(preState)
private fun link(): Address? {
@ -90,17 +111,17 @@ class Instruction(val bytes: RomData, val opcode: Opcode, val preState: State) {
}
return when (opcode.mode) {
Mode.ABSOLUTE -> address.withinBank(word.value)
Mode.ABSOLUTE_LONG -> Address(long.value)
Mode.RELATIVE -> address + 2 + signedByte
Mode.RELATIVE_LONG -> address + 3 + signedWord
Mode.DATA_WORD -> address.withinBank(dataWord.value)
Mode.DATA_LONG -> Address(dataLong.value)
Mode.ABSOLUTE -> relativeAddress.withinBank(word)
Mode.ABSOLUTE_LONG -> Address(long)
Mode.RELATIVE -> relativeAddress + 2 + signedByte.toInt()
Mode.RELATIVE_LONG -> relativeAddress + 3 + signedWord.toInt()
Mode.DATA_WORD -> relativeAddress.withinBank(word)
Mode.DATA_LONG -> Address(long)
else -> null
}
}
override fun toString(): String {
return "$address ${bytesToString()} ${opcode.mnemonic} ${opcode.mode.print(this).padEnd(100, ' ')} ($preState -> $postState)"
return "$address ${bytesToString()} ${opcode.mnemonic.displayName} ${opcode.mode.print(this).padEnd(100, ' ')} ($preState -> $postState)"
}
}

View File

@ -1,12 +1,21 @@
package com.smallhacker.disbrowser.asm
import com.smallhacker.disbrowser.util.left
import com.smallhacker.disbrowser.util.or
import com.smallhacker.disbrowser.util.toLong
import com.fasterxml.jackson.annotation.*
import com.fasterxml.jackson.annotation.JsonSubTypes.Type
import com.smallhacker.disbrowser.util.joinBytes
import com.smallhacker.disbrowser.util.toUInt24
class Metadata {
class Metadata() {
private val lines = HashMap<Address, MetadataLine>()
@JsonValue
private fun linesAsList() = lines.values.toList()
@JsonCreator
private constructor(@JsonProperty lines: List<MetadataLine>) : this() {
lines.forEach { add(it) }
}
fun add(line: MetadataLine): Metadata {
lines[line.address] = line
return this
@ -15,10 +24,20 @@ class Metadata {
operator fun get(address: Address): MetadataLine? {
return lines[address]
}
fun getOrAdd(address: Address): MetadataLine {
val line = this[address]
if (line != null) {
return line
}
val newLine = MetadataLine(address)
add(newLine)
return newLine
}
}
fun Metadata.at(address: Int, runner: MetadataLine.() -> Unit) {
val line = MetadataLine(Address(address))
val line = MetadataLine(address(address))
this.add(line)
runner(line)
}
@ -29,38 +48,77 @@ fun metadata(runner: Metadata.() -> Unit): Metadata {
return metadata
}
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "flagType")
@JsonSubTypes(
Type(value = NonReturningRoutine::class, name = "NonReturningRoutine"),
Type(value = JmpIndirectLongInterleavedTable::class, name = "JmpIndirectLongInterleavedTable"),
Type(value = JslTableRoutine::class, name = "JslTableRoutine")
)
interface InstructionFlag
object NonReturningRoutine : InstructionFlag
class JmpIndirectLongInterleavedTable(private val start: Address, private val entries: Int) : InstructionFlag {
fun readTable(data: RomData): Sequence<Address> {
return (0 until entries)
.asSequence()
.map { it + start.pc }
.map { pc -> data[pc].toLong() or (data[pc + entries].toLong() left 8) or (data[pc + entries + entries].toLong() left 16) }
.map { Address(it.value) }
}
object NonReturningRoutine : InstructionFlag {
override fun toString() = "NonReturningRoutine"
}
class JslTableRoutine(private val entries: Int) : InstructionFlag {
fun readTable(postJsr: State): Sequence<Address> {
val data = postJsr.data
return (0 until entries)
class JmpIndirectLongInterleavedTable @JsonCreator constructor(
@field:JsonProperty @JsonProperty private val start: Address,
@field:JsonProperty @JsonProperty private val entries: Int
) : InstructionFlag {
private val uEntries get() = entries.toUInt()
fun readTable(data: RomData): Sequence<Address> {
return (0u until uEntries)
.asSequence()
.map { postJsr.address.pc + (it * 3) }
.map { pc -> data[pc].toLong() or (data[pc + 1].toLong() left 8) or (data[pc + 2].toLong() left 16) }
.map { Address(it.value) }
.map { it + start.pc }
.map { pc -> joinBytes(data[pc], data[pc + uEntries], data[pc + uEntries + uEntries]).toUInt24() }
.map { Address(it) }
}
// fun dataInstructions(postJsr: State) {
// val data = postJsr.data
// return (0 until entries)
// .asSequence()
// .map {
// val offset = it * 3
// Instruction(data.range(postJsr.address.pc + offset, 3), Opcode.POINTER_LONG, postJsr.mutateAddress { it + offset })
// }
// }
fun generateCode(jumpInstruction: Instruction): Sequence<DataBlock> {
val table = jumpInstruction.preState.data.deinterleave(uEntries,
start.pc,
(start + entries).pc,
(start + (2u * uEntries).toInt()).pc
)
return (0u until uEntries)
.asSequence()
.map { index -> index * 3u }
.map { offset ->
val target = table.getLong(offset)
DataBlock(
Opcode.POINTER_LONG,
table.range(offset, 3u),
jumpInstruction.postState.address + offset.toInt(),
jumpInstruction.relativeAddress,
jumpInstruction.opcode.mutate(jumpInstruction)
.mutateAddress { Address(target) }
.withOrigin(jumpInstruction)
)
// Instruction(
// data.range((start.value + offset).toUInt(), 3u),
// Opcode.POINTER_LONG,
// preState.mutateAddress { start -> start + offset.toInt() }
// )
}
}
override fun toString() = "JmpIndirectLongInterleavedTable($start, $entries)"
}
class JslTableRoutine @JsonCreator constructor(
@field:JsonProperty @JsonProperty private val entries: Int
) : InstructionFlag {
fun readTable(postJsr: State): Sequence<Address> {
val data = postJsr.data
return (0u until entries.toUInt())
.asSequence()
.map { postJsr.address.pc + (it * 3u) }
.map { pc -> joinBytes(data[pc], data[pc + 1u], data[pc + 2u]).toUInt24() }
.map { Address(it) }
}
override fun toString() = "JslTableRoutine($entries)"
}

View File

@ -4,5 +4,6 @@ data class MetadataLine(
val address: Address,
var label: String? = null,
var comment: String? = null,
var preComment: String? = null,
val flags: MutableList<InstructionFlag> = ArrayList()
)
)

View File

@ -1,6 +1,6 @@
package com.smallhacker.disbrowser.asm
enum class Mnemonic {
enum class Mnemonic(private val nameOverride: String? = null) {
ADC, AND, ASL, BCC, BCS, BEQ, BIT, BMI, BNE, BPL, BRA,
BRK, BRL, BVC, BVS, CLC, CLD, CLI, CLV, CMP, COP, CPX,
CPY, DEC, DEX, DEY, EOR, INC, INX, INY, JMP, JML, JSL,
@ -11,5 +11,7 @@ enum class Mnemonic {
TCS, TDC, TRB, TSB, TSC, TSX, TXA, TXS, TXY, TYA, TYX,
WAI, WDM, XBA, XCE,
DB, DW, DL
DB(".db"), DW(".dw"), DL(".dl");
val displayName get() = nameOverride ?: name
}

View File

@ -2,76 +2,112 @@ package com.smallhacker.disbrowser.asm
import com.smallhacker.disbrowser.util.*
fun format(format: String, value: UVal<*>): String {
return format.replace(Regex("[0]+"), value.toHex())
}
private val ZEROES = Regex("[0]+")
private fun countBytes(format: String) = (ZEROES.find(format)?.groupValues?.firstOrNull()?.length?.toUInt() ?: 0u) / 2u
private const val ACCUMULATOR_SIZE = -1
private const val INDEX_SIZE = -2
fun format(format: String, value: UInt, operandBytes: UInt = countBytes(format)) =
format.replace(ZEROES, toHex(value, operandBytes))
enum class Mode {
DATA_BYTE(1, "$00", { dataByte }, dataMode = true),
DATA_WORD(2, "$0000", { dataWord }, dataMode = true),
DATA_LONG(3, "$000000", { dataLong }, dataMode = true),
DATA_BYTE("$00", dataMode = true, showLengthSuffix = false),
DATA_WORD("$0000", dataMode = true, showLengthSuffix = false),
DATA_LONG("$000000", dataMode = true, showLengthSuffix = false),
IMPLIED(1, { "" }),
IMMEDIATE_8(2, "#$00", { byte }),
IMMEDIATE_16(3, "#$0000", { word }),
IMMEDIATE_M(-1, {
when (preState.m) {
null -> "????"
true -> format("#$00", byte)
false -> format("#$0000", word)
}
}),
IMMEDIATE_X(-2, {
when (preState.x) {
null -> "???"
true -> format("#$00", byte)
false -> format("#$0000", word)
}
}),
ABSOLUTE(3, "$0000", { word }),
ABSOLUTE_X(3, "$0000,x", { word }),
ABSOLUTE_Y(3, "$0000,y", { word }),
ABSOLUTE_LONG(4, "$000000", { long }),
ABSOLUTE_LONG_X(4, "$000000,x", { long }),
ABSOLUTE_INDIRECT(3, "($0000)", { word }),
ABSOLUTE_INDIRECT_LONG(3, "[$0000]", { word }),
ABSOLUTE_X_INDIRECT(3, "($0000,x)", { word }),
DIRECT(2, "$00", { byte }),
DIRECT_X(2, "$00,x", { byte }),
DIRECT_Y(2, "$00,y", { byte }),
DIRECT_S(2, "$00,s", { byte }),
DIRECT_INDIRECT(2, "($00)", { byte }),
DIRECT_INDIRECT_Y(2, "($00),y", { byte }),
DIRECT_X_INDIRECT(2, "($00,x)", { byte }),
DIRECT_S_INDIRECT_Y(2, "($00,s),y", { byte }),
DIRECT_INDIRECT_LONG(2, "[$00]", { byte }),
DIRECT_INDIRECT_LONG_Y(2, "[$00],y", { byte }),
RELATIVE(2, {
val rel = signedByte.toInt() + 2
format("$000000", uLong((address + rel).value))
}),
//RELATIVE_LONG(3, "$0000", { word }),
RELATIVE_LONG(3, {
val rel = signedWord.toInt() + 3
format("$000000", uLong((address + rel).value))
}),
BLOCK_MOVE(3, { String.format("#$%02x,#$%02x", byte.value, byte2.value) })
IMPLIED("", showLengthSuffix = false),
IMMEDIATE_8("#$00", showLengthSuffix = false),
IMMEDIATE_16("#$0000", showLengthSuffix = false),
ABSOLUTE("$0000"),
ABSOLUTE_X("$0000,x"),
ABSOLUTE_Y("$0000,y"),
ABSOLUTE_LONG("$000000"),
ABSOLUTE_LONG_X("$000000,x"),
ABSOLUTE_INDIRECT("($0000)"),
ABSOLUTE_INDIRECT_LONG("[$0000]"),
ABSOLUTE_X_INDIRECT("($0000,x)"),
DIRECT("$00"),
DIRECT_X("$00,x"),
DIRECT_Y("$00,y"),
DIRECT_S("$00,s"),
DIRECT_INDIRECT("($00)"),
DIRECT_INDIRECT_Y("($00),y"),
DIRECT_X_INDIRECT("($00,x)"),
DIRECT_S_INDIRECT_Y("($00,s),y"),
DIRECT_INDIRECT_LONG("[$00]"),
DIRECT_INDIRECT_LONG_Y("[$00],y"),
IMMEDIATE_M(
operandLength = ACCUMULATOR_SIZE,
print = {
when (preState?.m) {
null -> "????"
true -> format("#$00", byte.toUInt())
false -> format("#$0000", word.toUInt())
}
}
),
IMMEDIATE_X(
operandLength = INDEX_SIZE,
print = {
when (preState?.x) {
null -> "???"
true -> format("#$00", byte.toUInt())
false -> format("#$0000", word.toUInt())
}
}
),
RELATIVE(
format = "$000000",
operandLength = 1u,
valueGetter = {
val rel = signedByte.toInt() + 2
(relativeAddress + rel).value.toUInt()
},
showLengthSuffix = false
),
RELATIVE_LONG(
format = "$000000",
operandLength = 2u,
valueGetter = {
val rel = signedWord.toInt() + 3
(relativeAddress + rel).value.toUInt()
},
showLengthSuffix = false
),
BLOCK_MOVE(
operandLength = 2,
print = { String.format("#$%02x,#$%02x", byte.toInt(), byte2.toInt()) },
showLengthSuffix = false
)
;
private val length: Int
val print: Instruction.() -> String
private val operandLength: Int
val print: CodeUnit.() -> String
val dataMode: Boolean
val showLengthSuffix: Boolean
constructor(length: Int, print: Instruction.() -> String) {
this.length = length
constructor(operandLength: Int, print: CodeUnit.() -> String, showLengthSuffix: Boolean = true) {
this.operandLength = operandLength
this.print = print
this.dataMode = false
this.showLengthSuffix = showLengthSuffix
}
constructor(length: Int, format: String, valueGetter: Instruction.() -> UVal<*>, dataMode: Boolean = false) {
this.length = length
this.print = { format(format, valueGetter(this)) }
constructor(
format: String,
printedLength: UInt = countBytes(format),
operandLength: UInt = printedLength,
valueGetter: CodeUnit.() -> UInt = { value!! },
dataMode: Boolean = false,
showLengthSuffix: Boolean = true
) {
this.operandLength = operandLength.toInt()
this.print = { format(format, valueGetter(this), printedLength) }
this.dataMode = dataMode
this.showLengthSuffix = showLengthSuffix
}
/**
@ -82,12 +118,10 @@ enum class Mode {
*
* If the length cannot be determined based on the current [State], `null` is returned.
*/
fun instructionLength(state: State): Int? {
return when (length) {
-1 -> state.mWidth?.plus(1)
-2 -> state.xWidth?.plus(1)
else -> length
}
fun instructionLength(state: State): UInt? {
val operatorLength = if (this.dataMode) 0u else 1u
return operandLength(state)
?.plus(operatorLength)
}
/**
@ -98,14 +132,11 @@ enum class Mode {
*
* If the length cannot be determined based on the current [State], `null` is returned.
*/
fun operandLength(state: State): Int? {
val len = instructionLength(state) ?: return null
return when(this) {
DATA_BYTE,
DATA_WORD,
DATA_LONG -> len
else -> len - 1
fun operandLength(state: State): UInt? {
return when (operandLength) {
ACCUMULATOR_SIZE -> state.mWidth
INDEX_SIZE -> state.xWidth
else -> operandLength.toUInt()
}
}
}

View File

@ -1,19 +0,0 @@
package com.smallhacker.disbrowser.asm
abstract class ModeFormat {
abstract fun print(instruction: Instruction, metadata: Metadata): String
//fun wrap(prefix: String = "", suffix: String = ""): ModeFormat {
//
//}
}
//private class AddressModeFormat: ModeFormat() {
// override fun print(instruction: Instruction, metadata: Metadata): String {
// val mode = instruction.opcode.mode
// val operandLength = mode.operandLength(instruction.preState)
// instruction.
//
// TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
// }
//}

View File

@ -4,7 +4,7 @@ import java.util.HashMap
import com.smallhacker.disbrowser.asm.Mnemonic.*
import com.smallhacker.disbrowser.asm.Mode.*
import com.smallhacker.disbrowser.util.UByte
import com.smallhacker.disbrowser.util.OldUByte
typealias SegmentEnder = Instruction.() -> SegmentEnd?
@ -14,7 +14,7 @@ class Opcode private constructor(val mnemonic: Mnemonic, val mode: Mode, val end
private var _branch = false
val operandIndex
get() = if (mode.dataMode) 0 else 1
get() = if (mode.dataMode) 0u else 1u
val continuation: Continuation
get() = _continuation
@ -57,7 +57,7 @@ class Opcode private constructor(val mnemonic: Mnemonic, val mode: Mode, val end
private val OPCODES: Array<Opcode>
fun opcode(byteValue: UByte): Opcode {
return OPCODES[byteValue.value]
return OPCODES[byteValue.toInt()]
}
init {
@ -135,8 +135,8 @@ class Opcode private constructor(val mnemonic: Mnemonic, val mode: Mode, val end
add(0xF8, SED, IMPLIED, alwaysContinue)
add(0xD8, CLD, IMPLIED, alwaysContinue)
add(0xB8, CLV, IMPLIED, alwaysContinue)
add(0xE2, SEP, IMMEDIATE_8, alwaysContinue) { it.preState.sep(it.bytes[1]) }
add(0xC2, REP, IMMEDIATE_8, alwaysContinue) { it.preState.rep(it.bytes[1]) }
add(0xE2, SEP, IMMEDIATE_8, alwaysContinue) { it.preState.sep(it.bytes[1u]) }
add(0xC2, REP, IMMEDIATE_8, alwaysContinue) { it.preState.rep(it.bytes[1u]) }
add(0xFB, XCE, IMPLIED, alwaysContinue)
add(0xC1, CMP, DIRECT_X_INDIRECT, alwaysContinue)
@ -218,11 +218,11 @@ class Opcode private constructor(val mnemonic: Mnemonic, val mode: Mode, val end
add(0xFA, PLX, IMPLIED, alwaysContinue) { it.preState.pull(it.preState.xWidth) }
add(0x7A, PLY, IMPLIED, alwaysContinue) { it.preState.pull(it.preState.xWidth) }
add(0x8B, PHB, IMPLIED, alwaysContinue) { it.preState.pushUnknown(1) }
add(0xAB, PLB, IMPLIED, alwaysContinue) { it.preState.pull(1) }
add(0x0B, PHD, IMPLIED, alwaysContinue) { it.preState.pushUnknown(1) }
add(0x2B, PLD, IMPLIED, alwaysContinue) { it.preState.pull(1) }
add(0x4B, PHK, IMPLIED, alwaysContinue) { it.preState.push(it.address.value shr 16) }
add(0x8B, PHB, IMPLIED, alwaysContinue) { it.preState.pushUnknown(1u) }
add(0xAB, PLB, IMPLIED, alwaysContinue) { it.preState.pull(1u) }
add(0x0B, PHD, IMPLIED, alwaysContinue) { it.preState.pushUnknown(1u) }
add(0x2B, PLD, IMPLIED, alwaysContinue) { it.preState.pull(1u) }
add(0x4B, PHK, IMPLIED, alwaysContinue) { it.preState.push((it.address.value shr 16).toUInt()) }
add(0x08, PHP, IMPLIED, alwaysContinue) { it.preState.push(it.preState.flags) }
add(0x28, PLP, IMPLIED, alwaysContinue) { it.preState.pull { copy(flags = it) } }

View File

@ -5,40 +5,62 @@ import java.nio.file.Files
import java.nio.file.Path
interface RomData {
val size: Int
val size: UInt
operator fun get(address: Int): UByte
operator fun get(address: UInt): UByte
fun range(start: Int, length: Int): RomData = RomRange(this, start, length)
fun range(start: UInt, length: UInt): RomData = RomRange(this, start, length)
fun asSequence(): Sequence<UByte> = (0 until size).asSequence().map { get(it) }
fun asSequence(): Sequence<UByte> = (0u until size).asSequence().map { this[it] }
companion object {
fun load(path: Path): RomData = Rom(Files.readAllBytes(path))
fun load(path: Path): RomData = ArrayRomData(Files.readAllBytes(path).toUByteArray())
}
}
fun RomData.getWord(address: Int): UWord = get(address).toWord() or (get(address + 1).toWord() left 8)
fun romData(vararg bytes: UByte): RomData = ArrayRomData(bytes)
private class Rom(private val bytes: ByteArray): RomData {
override val size = bytes.size
fun RomData.getWord(address: UInt) = joinBytes(this[address], this[address + 1u]).toUShort()
fun RomData.getLong(address: UInt) = joinBytes(this[address], this[address + 1u], this[address + 2u]).toUInt24()
override fun get(address: Int): UByte {
checkRange(address)
return uByte(bytes[address].toInt())
private class ArrayRomData(private val bytes: UByteArray) : RomData {
override val size = bytes.size.toUInt()
override fun get(address: UInt) = rangeChecked(address) {
bytes[address.toInt()]
}
}
private class RomRange(private val parent: RomData, private val start: Int, override val size: Int): RomData {
override fun get(address: Int): UByte {
checkRange(address)
return parent[start + address]
private class RomRange(private val parent: RomData, private val start: UInt, override val size: UInt) : RomData {
override fun get(address: UInt) = rangeChecked(address) {
parent[start + address]
}
}
private fun RomData.checkRange(address: Int) {
if (address < 0 || address >= size) {
private fun <T> RomData.rangeChecked(address: UInt, action: () -> T): T {
if (address >= size) {
throw IndexOutOfBoundsException("Index $address out of range: [0, $size)")
}
return action()
}
private class ReindexedRomData(
private val parent: RomData,
override val size: UInt,
private val mapper: (UInt) -> UInt
) : RomData {
override fun get(address: UInt) = rangeChecked(address) {
val mapped = mapper(address)
parent[mapped]
}
}
fun RomData.deinterleave(entries: UInt, vararg startOffsets: UInt): RomData {
val fieldCount = startOffsets.size.toUInt()
return ReindexedRomData(this, entries * fieldCount) {
val entry = it / fieldCount
val field = it.rem(fieldCount)
startOffsets[field.toInt()] + entry
}
}

View File

@ -2,17 +2,16 @@ package com.smallhacker.disbrowser.asm
import com.smallhacker.disbrowser.ImmStack
import com.smallhacker.disbrowser.immStack
import com.smallhacker.disbrowser.util.UByte
data class State(val origin: Instruction? = null, val data: RomData, val address: Address, val flags: VagueNumber = VagueNumber(), val stack: ImmStack<VagueNumber> = immStack()) {
val m: Boolean? get() = flags.getBoolean(0x20)
val x: Boolean? get() = flags.getBoolean(0x10)
val m: Boolean? get() = flags.getBoolean(0x20u)
val x: Boolean? get() = flags.getBoolean(0x10u)
val mWidth: Int? get() = toWidth(m)
val xWidth: Int? get() = toWidth(x)
val mWidth: UInt? get() = toWidth(m)
val xWidth: UInt? get() = toWidth(x)
fun sep(i: UByte) = withFlags(flags.withBits(i.value))
fun rep(i: UByte) = withFlags(flags.withoutBits(i.value))
fun sep(i: UByte) = withFlags(flags.withBits(i.toUInt()))
fun rep(i: UByte) = withFlags(flags.withoutBits(i.toUInt()))
fun uncertain() = withFlags(VagueNumber())
private fun withFlags(flags: VagueNumber) = copy(flags = flags)
@ -20,28 +19,28 @@ data class State(val origin: Instruction? = null, val data: RomData, val address
fun mutateAddress(mutator: (Address) -> Address) = copy(address = mutator(address))
fun withOrigin(instruction: Instruction?) = copy(origin = instruction)
fun push(value: Int) = push(VagueNumber(value))
fun push(value: UInt) = push(VagueNumber(value))
fun push(value: VagueNumber) = copy(stack = stack.push(value))
fun pushUnknown(count: Int? = 1): State {
fun pushUnknown(count: UInt? = 1u): State {
if (count == null) {
return copy(stack = immStack())
}
var stack = this.stack
for (i in 1..count) {
for (i in 1u..count) {
stack = stack.push(VagueNumber())
}
return copy(stack = stack)
}
fun pull() = (stack.top ?: VagueNumber()) to copy(stack = stack.pop())
fun pull(count: Int?): State {
fun pull(count: UInt?): State {
if (count == null) {
return copy(stack = immStack())
}
var stack = this.stack
for (i in 1..count) {
for (i in 1u..count) {
stack = stack.pop()
}
return copy(stack = stack)
@ -61,20 +60,20 @@ data class State(val origin: Instruction? = null, val data: RomData, val address
private fun stackByteToString(v: VagueNumber): String {
if (v.certain) {
return String.format("%02x", v.value)
return String.format("%02x", v.value.toInt())
}
if (v.certainty == 0) {
if (v.certainty == 0u) {
return "??"
}
val c = v.certainty
val high = (c and 0xF0) != 0
val low = (c and 0x0F) != 0
val high = (c and 0xF0u) != 0u
val low = (c and 0x0Fu) != 0u
return StringBuilder()
.append(if (high) String.format("%x", (v.value ushr 4) and 0xF) else "?")
.append(if (low) String.format("%x", v.value and 0xF) else "?")
.append(if (high) String.format("%x", ((v.value shr 4) and 0xFu).toInt()) else "?")
.append(if (low) String.format("%x", (v.value and 0xFu).toInt()) else "?")
.toString()
}
@ -100,14 +99,14 @@ data class State(val origin: Instruction? = null, val data: RomData, val address
return out.toString()
}
private fun toWidth(flag: Boolean?): Int? = when (flag) {
private fun toWidth(flag: Boolean?): UInt? = when (flag) {
null -> null
true -> 1
false -> 2
true -> 1u
false -> 2u
}
}
fun State.pushByte(value: Byte) = this.push(VagueNumber(value.toInt()))
fun State.pushByte(value: Byte) = this.push(VagueNumber(value.toUInt()))
fun State.pull(consumer: State.(VagueNumber) -> State): State {
val (value, state) = this.pull()
return consumer(state, value)

View File

@ -1,39 +1,42 @@
package com.smallhacker.disbrowser.asm
@Suppress("DataClassPrivateConstructor")
data class VagueNumber private constructor(val value: Int, val certainty: Int){
constructor() : this(0, 0)
constructor(value: Int) : this(value, -1)
inline class VagueNumber(private val valueAndCertainty: ULong) {
private constructor(value: UInt, certainty: UInt) : this(value.toULong() or (certainty.toULong() shl 32))
constructor(value: UInt) : this(value, 0xFFFF_FFFFu)
constructor() : this(0u, 0u)
fun withBits(value: Int) = VagueNumber(this.value or value, this.certainty or value)
fun withoutBits(value: Int) = VagueNumber(this.value and value.inv(), this.certainty or value)
val value get() = valueAndCertainty.toUInt()
val certainty get() = (valueAndCertainty shr 32).toUInt()
fun withBits(value: UInt) = VagueNumber(this.value or value, this.certainty or value)
fun withoutBits(value: UInt) = VagueNumber(this.value and value.inv(), this.certainty or value)
val certain: Boolean
get() = certainty == -1
get() = certainty == 0xFFFF_FFFFu
fun get(mask: Int): Int? {
fun get(mask: UInt): UInt? {
if ((certainty and mask) != mask) {
return null
}
return value and mask
}
fun getBoolean(mask: Int): Boolean? {
fun getBoolean(mask: UInt): Boolean? {
val value = get(mask) ?: return null
return value == mask
}
override fun toString(): String {
var i = 1 shl 31
var i = 1u shl 31
val out = StringBuilder()
while (i != 0) {
while (i != 0u) {
val b = getBoolean(i)
when (b) {
true -> out.append('1')
false -> out.append('0')
null -> out.append('?')
}
i = i ushr 1
i = i shr 1
}
return out.toString()
}

View File

@ -16,7 +16,7 @@ object Disassembler {
}
tryAdd(initialState)
val instructions = ArrayList<Instruction>()
val instructions = ArrayList<CodeUnit>()
while (queue.isNotEmpty()) {
val state = queue.remove()
@ -26,17 +26,21 @@ object Disassembler {
var stop = (ins.opcode.continuation == Continuation.NO) or
(ins.opcode.mode.instructionLength(state) == null)
metadata[ins.address]?.flags?.forEach {
if (it is JmpIndirectLongInterleavedTable) {
metadata[ins.address]?.flags?.forEach { flag ->
if (flag is JmpIndirectLongInterleavedTable) {
if (global) {
it.readTable(state.data)
flag.readTable(state.data)
.map { ins.postState.copy(address = it) }
.forEach { tryAdd(it) }
}
flag.generateCode(ins)
.forEach { instructions.add(it) }
stop = true
} else if (it is JslTableRoutine) {
} else if (flag is JslTableRoutine) {
if (global) {
it.readTable(ins.postState)
flag.readTable(ins.postState)
.map { ins.postState.copy(address = it) }
.forEach { tryAdd(it) }
}
@ -68,7 +72,7 @@ object Disassembler {
}
val instructionList = instructions
.sortedBy { it.address }
.sortedBy { it.presentedAddress }
.toList()
return Disassembly(instructionList)
@ -160,7 +164,7 @@ object Disassembler {
private fun disassembleInstruction(state: State): Instruction {
val pc = state.address.pc
val opcode = Opcode.opcode(state.data[pc])
val length = opcode.mode.instructionLength(state) ?: 1
val length = opcode.mode.instructionLength(state) ?: 1u
val bytes = state.data.range(pc, length)
return Instruction(bytes, opcode, state)
}

View File

@ -0,0 +1,72 @@
package com.smallhacker.disbrowser.resource
import com.smallhacker.disbrowser.HtmlNode
import com.smallhacker.disbrowser.Service
import com.smallhacker.disbrowser.asm.Address
import com.smallhacker.disbrowser.asm.VagueNumber
import com.smallhacker.disbrowser.util.toUInt24
import com.smallhacker.disbrowser.util.tryParseInt
import javax.ws.rs.GET
import javax.ws.rs.Path
import javax.ws.rs.PathParam
import javax.ws.rs.Produces
import javax.ws.rs.core.MediaType
import javax.ws.rs.core.Response
@Path("/")
class DisassemblyResource {
@GET
@Produces(MediaType.TEXT_HTML)
fun getIt() = handle {
Service.showDisassemblyFromReset()
}
@GET
@Path("{address}")
@Produces(MediaType.TEXT_HTML)
fun getIt(@PathParam("address") address: String) = getIt(address, "")
@GET
@Path("{address}/{state}")
@Produces(MediaType.TEXT_HTML)
fun getIt(@PathParam("address") address: String, @PathParam("state") state: String): Response {
return handle {
parseAddress(address)?.let {
val flags = parseState(state)
Service.showDisassembly(it, flags)
}
}
}
private fun parseAddress(address: String): Address? {
return tryParseInt(address, 16)
?.let { Address(it.toUInt24()) }
}
private fun handle(runner: () -> HtmlNode?): Response {
try {
val disassembly = runner()
return if (disassembly == null)
Response.status(404).build()
else
Response.ok(disassembly.toString(), MediaType.TEXT_HTML).build()
} catch (e: Exception) {
e.printStackTrace()
throw e
}
}
private fun parseState(state: String): VagueNumber {
var flags = VagueNumber()
flags = parseFlag(state, flags, 'M', 'm', 0x20u)
flags = parseFlag(state, flags, 'X', 'x', 0x10u)
return flags
}
private fun parseFlag(state: String, flags: VagueNumber, set: Char, clear: Char, mask: UInt): VagueNumber = when {
state.contains(set) -> flags.withBits(mask)
state.contains(clear) -> flags.withoutBits(mask)
else -> flags
}
}

View File

@ -0,0 +1,40 @@
package com.smallhacker.disbrowser.resource
import com.smallhacker.disbrowser.Service
import com.smallhacker.disbrowser.asm.Address
import com.smallhacker.disbrowser.asm.MetadataLine
import com.smallhacker.disbrowser.util.toUInt24
import com.smallhacker.disbrowser.util.tryParseInt
import javax.ws.rs.Consumes
import javax.ws.rs.POST
import javax.ws.rs.Path
import javax.ws.rs.PathParam
import javax.ws.rs.core.MediaType
import javax.ws.rs.core.Response
@Path("/rest")
class RestResource {
@POST
@Path("{address}/{field}")
@Consumes(MediaType.TEXT_PLAIN)
fun getIt(@PathParam("address") address: String, @PathParam("field") fieldName: String, body: String): Response {
val parsedAddress = parseAddress(address) ?: return Response.status(400).build()
val field = when (fieldName) {
"preComment" -> MetadataLine::preComment
"comment" -> MetadataLine::comment
"label" -> MetadataLine::label
else -> return Response.status(404).build()
}
Service.updateMetadata(parsedAddress, field, body)
return Response.noContent().build()
}
private fun parseAddress(address: String): Address? {
return tryParseInt(address, 16)
?.let { Address(it.toUInt24()) }
}
}

View File

@ -0,0 +1,31 @@
package com.smallhacker.disbrowser.resource
import javax.ws.rs.GET
import javax.ws.rs.Path
import javax.ws.rs.PathParam
import javax.ws.rs.core.Response
@Path("/")
class StaticResource {
@GET
@Path("resources/{file}.{ext}")
fun getStatic(@PathParam("file") file: String, @PathParam("ext") ext: String): Response {
val mime = when (ext) {
"js" -> "application/javascript"
"css" -> "text/css"
else -> null
}
if (mime != null) {
javaClass.getResourceAsStream("/public/$file.$ext")
?.bufferedReader()
?.use {
return Response.ok(it.readText())
.type(mime)
.build()
}
}
return Response.status(404).build()
}
}

View File

@ -27,27 +27,27 @@ class UVal<U : UType>(value: Int, val type: U) : Comparable<UVal<U>> {
//fun value(value: Int) = UVal(value, type)
//fun mutate(mutator: (Int) -> Int) = UVal(mutator(value), type)
object U1 : UType(1, "UByte")
object U2 : UType(2, "UWord")
object U3 : UType(3, "ULong")
object U1 : UType(1, "OldUByte")
object U2 : UType(2, "OldUWord")
object U3 : UType(3, "OldULong")
}
inline fun <reified U: UVal<*>> U.value(value: Int): U = UVal(value, type) as U
inline infix fun <reified U: UVal<*>> U.mutate(mutator: (Int) -> Int): U = value(mutator(value))
typealias UByte = UVal<UVal.U1>
typealias UWord = UVal<UVal.U2>
typealias ULong = UVal<UVal.U3>
typealias OldUByte = UVal<UVal.U1>
typealias OldUWord = UVal<UVal.U2>
typealias OldULong = UVal<UVal.U3>
fun UVal<*>.toByte() = uByte(value)
fun UVal<*>.toWord() = uWord(value)
fun UVal<*>.toLong() = uLong(value)
private val UBYTE_CACHE = Array(256) { UByte(it, UVal.U1) }
private val UBYTE_CACHE = Array(256) { OldUByte(it, UVal.U1) }
fun uByte(value: Int): UByte = UBYTE_CACHE[value and 0xFF]
fun uWord(value: Int): UWord = UVal(value, UVal.U2)
fun uLong(value: Int): ULong = UVal(value, UVal.U3)
fun uByte(value: Int): OldUByte = UBYTE_CACHE[value and 0xFF]
fun uWord(value: Int): OldUWord = UVal(value, UVal.U2)
fun uLong(value: Int): OldULong = UVal(value, UVal.U3)
inline infix fun <reified U: UVal<V>, V: UType> U.left(count: Int): U = mutate { it shl count }
inline infix fun <reified U: UVal<V>, V: UType> U.right(count: Int): U = mutate { it ushr count }

View File

@ -0,0 +1,22 @@
package com.smallhacker.disbrowser.util
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import java.nio.file.Path
val jsonMapper: ObjectMapper by lazy {
jacksonObjectMapper()
}
interface JsonFile<T> {
fun load(): T
fun save(value: T)
}
inline fun <reified T> jsonFile(path: Path): JsonFile<T> {
return object : JsonFile<T> {
override fun load() = jsonMapper.readValue<T>(path.toFile())
override fun save(value: T) = jsonMapper.writeValue(path.toFile(), value)
}
}

View File

@ -14,3 +14,50 @@ fun tryParseInt(s: String, radix: Int = 10): Int? {
null
}
}
fun toHex(value: UInt, bytes: UInt): String {
if (bytes == 0u) {
return ""
}
val digits = 2u * bytes
val pattern = "%0${digits}x"
return String.format(pattern, value.toInt())
}
fun joinBytes(vararg bytes: UByte) = bytes
.asSequence()
.mapIndexed { index, v -> v.toUInt() shl (index * 8) }
.reduce { a, b -> a or b }
inline class UInt24(private val data: UInt) {
fun toUInt() = data and 0x00FF_FFFFu
fun toUShort() = toUInt().toUShort()
fun toUByte() = toUInt().toUByte()
fun toInt() = toUInt().toInt()
fun toShort() = toUShort().toShort()
fun toByte() = toUByte().toByte()
infix fun and(v: UInt24) = (data and v.data).toUInt24()
infix fun and(v: UInt) = (data and v).toUInt24()
infix fun or(v: UInt24) = (data or v.data).toUInt24()
infix fun or(v: UInt) = (data or v).toUInt24()
infix fun shl(v: Int) = (data shl v).toUInt24()
infix fun shr(v: Int)= (toUInt() shr v).toUInt24()
operator fun plus(v: UInt24) = (toUInt() + v.toUInt()).toUInt24()
operator fun plus(v: UInt) = (toUInt() + v).toUInt24()
operator fun plus(v: Int) = (toInt() + v).toUInt24()
operator fun minus(v: UInt24) = (toUInt() - v.toUInt()).toUInt24()
operator fun minus(v: UInt) = (toUInt() - v).toUInt24()
operator fun minus(v: Int) = (toInt() - v).toUInt24()
override fun toString() = data.toString()
}
fun UInt.toUInt24() = UInt24(this and 0x00FF_FFFFu)
fun UShort.toUInt24() = this.toUInt().toUInt24()
fun UByte.toUInt24() = this.toUInt().toUInt24()
fun Int.toUInt24() = this.toUInt().toUInt24()
fun Short.toUInt24() = this.toUInt().toUInt24()
fun Byte.toUInt24() = this.toUInt().toUInt24()

View File

@ -1,48 +0,0 @@
function center(el) {
function documentOffsetTop (el) {
return el.offsetTop + ( el.offsetParent ? documentOffsetTop(el.offsetParent) : 0 );
}
var top = documentOffsetTop(el) - ( window.innerHeight / 2 );
window.scrollTo( 0, top );
}
function highlight(el) {
if (el) {
//el.scrollIntoView();
center(el);
var cl = el.classList;
var activeClass = "line-active";
cl.add(activeClass);
setTimeout(cl.remove.bind(cl, activeClass), 100);
}
}
function fromUrlPart(regex, input) {
var match = regex.exec(input);
if (!match) {
return null;
}
return document.getElementById(match[1].toLowerCase());
}
function fromHash() {
return fromUrlPart(/^#([0-9A-Fa-f]{6})$/, location.hash);
}
function fromPath() {
return fromUrlPart(/^\/([0-9A-Fa-f]{6})(?:\/.*)?/, location.pathname);
}
function fromUrl() {
return fromHash() || fromPath();
}
highlight(fromUrl());
window.addEventListener("hashchange", function () { highlight(fromHash()) }, false);

View File

@ -92,4 +92,16 @@ tr.line-active {
.routine-end > td {
border-bottom: 1px solid black;
}
.field-editable {
font-family: monospace;
border: none;
background: none;
padding: 0;
width: 100%;
}
.field-comment {
width: 500px;
}

62
src/main/ts/main.ts Normal file
View File

@ -0,0 +1,62 @@
function center(el: HTMLElement) {
function documentOffsetTop (el: HTMLElement) {
let top = el.offsetTop;
let parent = el.offsetParent;
if (parent && parent instanceof HTMLElement) {
top += documentOffsetTop(parent);
}
return top;
}
let top = documentOffsetTop(el) - (window.innerHeight / 2);
window.scrollTo( 0, top );
}
function highlight(el: HTMLElement | null) {
if (el) {
//el.scrollIntoView();
center(el);
let cl = el.classList;
let activeClass = "line-active";
cl.add(activeClass);
setTimeout(cl.remove.bind(cl, activeClass), 100);
}
}
function fromUrlPart(regex: RegExp, input: string) {
let match = regex.exec(input);
if (!match) {
return null;
}
return document.getElementById(match[1].toLowerCase());
}
function fromHash() {
return fromUrlPart(/^#([0-9A-Fa-f]{6})$/, location.hash);
}
function fromPath() {
return fromUrlPart(/^\/([0-9A-Fa-f]{6})(?:\/.*)?/, location.pathname);
}
function fromUrl() {
return fromHash() || fromPath();
}
highlight(fromUrl());
window.addEventListener("hashchange", function () { highlight(fromHash()) }, false);
let comments = document.getElementsByClassName("field-editable");
for (let i = 0; i < comments.length; i++) {
let comment = comments[i];
comment.addEventListener("change", e => {
let target = <HTMLInputElement>(e.target);
let field = target.dataset.field || "";
let address = parseInt(target.dataset.address || "-1");
let value = (target).value;
alert(field + "/" + address + "=" + value);
return false;
});
}

14
src/main/ts/tsconfig.json Normal file
View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"module": "amd",
"strictNullChecks": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"removeComments": true,
"sourceMap": true,
"outFile": "../resources/public/disbrowser.js"
},
"include": [
"*.ts"
]
}