mirror of
https://github.com/Smallhacker/disbrowser.git
synced 2024-11-24 03:34:50 +00:00
Overdue for a proper repo
This commit is contained in:
commit
7a4b94e907
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/.idea
|
||||
/target
|
138
pom.xml
Normal file
138
pom.xml
Normal file
@ -0,0 +1,138 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.smallhacker.dis-browser</groupId>
|
||||
<artifactId>dis-browser</artifactId>
|
||||
<packaging>war</packaging>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<name>dis-browser</name>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jersey</groupId>
|
||||
<artifactId>jersey-bom</artifactId>
|
||||
<version>${jersey.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jersey.containers</groupId>
|
||||
<artifactId>jersey-container-grizzly2-http</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jersey.inject</groupId>
|
||||
<artifactId>jersey-hk2</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- uncomment this to get JSON support:
|
||||
<dependency>
|
||||
<groupId>org.glassfish.jersey.media</groupId>
|
||||
<artifactId>jersey-media-json-binding</artifactId>
|
||||
</dependency>
|
||||
-->
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.9</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib-jdk8</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-test</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>1.2.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>java</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<mainClass>com.smallhacker.dis-browser.Main</mainClass>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-maven-plugin</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>compile</id>
|
||||
<phase>compile</phase>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>test-compile</id>
|
||||
<phase>test-compile</phase>
|
||||
<goals>
|
||||
<goal>test-compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<jvmTarget>1.8</jvmTarget>
|
||||
<args>
|
||||
<arg>-XXLanguage:+InlineClasses</arg>
|
||||
<arg>-Xexperimental=kotlin.ExperimentalUnsignedTypes</arg>
|
||||
</args>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>compile</id>
|
||||
<phase>compile</phase>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>testCompile</id>
|
||||
<phase>test-compile</phase>
|
||||
<goals>
|
||||
<goal>testCompile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<inherited>true</inherited>
|
||||
<configuration>
|
||||
<source>1.8</source>
|
||||
<target>1.8</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<properties>
|
||||
<jersey.version>2.27</jersey.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<kotlin.version>1.3.11</kotlin.version>
|
||||
</properties>
|
||||
</project>
|
158
src/main/java/com/smallhacker/disbrowser/Grid.kt
Normal file
158
src/main/java/com/smallhacker/disbrowser/Grid.kt
Normal file
@ -0,0 +1,158 @@
|
||||
package com.smallhacker.disbrowser
|
||||
|
||||
import com.smallhacker.disbrowser.asm.*
|
||||
|
||||
class Grid {
|
||||
private val arrowCells = HashMap<Pair<Int, Int>, HtmlNode?>()
|
||||
private val arrowClasses = HashMap<Pair<Int, Int>, String>()
|
||||
private var arrowWidth = 0
|
||||
|
||||
private val content = HashMap<Pair<Int, Int>, HtmlNode?>()
|
||||
private val cellClasses = HashMap<Pair<Int, Int>, String>()
|
||||
private val addresses = HashMap<Address, Int>()
|
||||
private val rowClasses = HashMap<Int, String>()
|
||||
private val rowId = HashMap<Int, String>()
|
||||
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])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
126
src/main/java/com/smallhacker/disbrowser/Html.kt
Normal file
126
src/main/java/com/smallhacker/disbrowser/Html.kt
Normal file
@ -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<String, String>()
|
||||
|
||||
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<HtmlNode>()
|
||||
|
||||
init {
|
||||
inner(HtmlArea(this))
|
||||
}
|
||||
|
||||
override fun printTo(out: StringBuilder): StringBuilder {
|
||||
super.printTo(out)
|
||||
children.forEach { it.printTo(out) }
|
||||
out.append("</", tag, ">")
|
||||
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<HtmlNode>()
|
||||
|
||||
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<String> = (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(" "))
|
||||
}
|
||||
}
|
39
src/main/java/com/smallhacker/disbrowser/HtmlBuilder.kt
Normal file
39
src/main/java/com/smallhacker/disbrowser/HtmlBuilder.kt
Normal file
@ -0,0 +1,39 @@
|
||||
//package com.smallhacker.disbrowser
|
||||
//
|
||||
//class HtmlContext(val out: StringBuilder)
|
||||
//
|
||||
//fun html(inner: HtmlContext.() -> Unit = {}): String {
|
||||
// val html = HtmlContext(StringBuilder().append("<!DOCTYPE html>"))
|
||||
// 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("</$tag>")
|
||||
//}
|
||||
//
|
||||
//
|
||||
//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\"")
|
37
src/main/java/com/smallhacker/disbrowser/ImmStack.kt
Normal file
37
src/main/java/com/smallhacker/disbrowser/ImmStack.kt
Normal file
@ -0,0 +1,37 @@
|
||||
package com.smallhacker.disbrowser
|
||||
|
||||
interface ImmStack<E>: Iterable<E> {
|
||||
fun isEmpty(): Boolean
|
||||
|
||||
val top: E?
|
||||
|
||||
fun pop(): ImmStack<E>
|
||||
|
||||
fun push(value: E): ImmStack<E> = ImmStackImpl(this, value)
|
||||
}
|
||||
|
||||
fun <T> immStack(): ImmStack<T> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return EmptyImmStack as ImmStack<T>
|
||||
}
|
||||
|
||||
private class ImmStackImpl<E>(private val parent: ImmStack<E>, override val top: E): ImmStack<E> {
|
||||
override fun isEmpty() = false
|
||||
|
||||
override fun pop(): ImmStack<E> = parent
|
||||
|
||||
override fun iterator(): Iterator<E> {
|
||||
return sequenceOf(top).plus(parent).iterator()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private object EmptyImmStack: ImmStack<Any?> {
|
||||
override fun isEmpty() = true
|
||||
|
||||
override val top: Any? = null
|
||||
|
||||
override fun pop() = this
|
||||
|
||||
override fun iterator() = emptySequence<Any?>().iterator()
|
||||
}
|
45
src/main/java/com/smallhacker/disbrowser/Main.java
Normal file
45
src/main/java/com/smallhacker/disbrowser/Main.java
Normal file
@ -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();
|
||||
}
|
||||
}
|
||||
|
159
src/main/java/com/smallhacker/disbrowser/MyResource.kt
Normal file
159
src/main/java/com/smallhacker/disbrowser/MyResource.kt
Normal file
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
19
src/main/java/com/smallhacker/disbrowser/asm/Address.kt
Normal file
19
src/main/java/com/smallhacker/disbrowser/asm/Address.kt
Normal file
@ -0,0 +1,19 @@
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package com.smallhacker.disbrowser.asm
|
||||
|
||||
enum class Continuation {
|
||||
NO, YES, MAYBE
|
||||
}
|
13
src/main/java/com/smallhacker/disbrowser/asm/Disassembly.kt
Normal file
13
src/main/java/com/smallhacker/disbrowser/asm/Disassembly.kt
Normal file
@ -0,0 +1,13 @@
|
||||
package com.smallhacker.disbrowser.asm
|
||||
|
||||
class Disassembly(lines: List<Instruction>) : Iterable<Instruction> {
|
||||
override fun iterator() = lines.values.iterator() as Iterator<Instruction>
|
||||
|
||||
private val lines = LinkedHashMap<Address, Instruction>()
|
||||
|
||||
init {
|
||||
lines.forEach { this.lines[it.address] = it }
|
||||
}
|
||||
|
||||
operator fun contains(address: Address) = address in lines
|
||||
}
|
106
src/main/java/com/smallhacker/disbrowser/asm/Instruction.kt
Normal file
106
src/main/java/com/smallhacker/disbrowser/asm/Instruction.kt
Normal file
@ -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)"
|
||||
}
|
||||
}
|
66
src/main/java/com/smallhacker/disbrowser/asm/Metadata.kt
Normal file
66
src/main/java/com/smallhacker/disbrowser/asm/Metadata.kt
Normal file
@ -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<Address, MetadataLine>()
|
||||
|
||||
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<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) }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class JslTableRoutine(private val entries: Int) : InstructionFlag {
|
||||
fun readTable(postJsr: State): Sequence<Address> {
|
||||
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 })
|
||||
// }
|
||||
// }
|
||||
}
|
@ -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<InstructionFlag> = ArrayList()
|
||||
)
|
15
src/main/java/com/smallhacker/disbrowser/asm/Mnemonic.kt
Normal file
15
src/main/java/com/smallhacker/disbrowser/asm/Mnemonic.kt
Normal file
@ -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
|
||||
}
|
111
src/main/java/com/smallhacker/disbrowser/asm/Mode.kt
Normal file
111
src/main/java/com/smallhacker/disbrowser/asm/Mode.kt
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
19
src/main/java/com/smallhacker/disbrowser/asm/ModeFormat.kt
Normal file
19
src/main/java/com/smallhacker/disbrowser/asm/ModeFormat.kt
Normal file
@ -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.
|
||||
// }
|
||||
//}
|
363
src/main/java/com/smallhacker/disbrowser/asm/Opcode.kt
Normal file
363
src/main/java/com/smallhacker/disbrowser/asm/Opcode.kt
Normal file
@ -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<Opcode>
|
||||
|
||||
fun opcode(byteValue: UByte): Opcode {
|
||||
return OPCODES[byteValue.value]
|
||||
}
|
||||
|
||||
init {
|
||||
val ocs = HashMap<Int, Opcode>()
|
||||
|
||||
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]!! }
|
||||
}
|
||||
}
|
||||
}
|
44
src/main/java/com/smallhacker/disbrowser/asm/RomData.kt
Normal file
44
src/main/java/com/smallhacker/disbrowser/asm/RomData.kt
Normal file
@ -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<UByte> = (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)")
|
||||
}
|
||||
}
|
29
src/main/java/com/smallhacker/disbrowser/asm/Segment.kt
Normal file
29
src/main/java/com/smallhacker/disbrowser/asm/Segment.kt
Normal file
@ -0,0 +1,29 @@
|
||||
package com.smallhacker.disbrowser.asm
|
||||
|
||||
class Segment (val start: Address, val end: SegmentEnd, val instructions: List<Instruction>)
|
||||
|
||||
class SegmentEnd(val address: Address, val local: List<State> = emptyList(), val remote: List<State> = 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)
|
||||
|
114
src/main/java/com/smallhacker/disbrowser/asm/State.kt
Normal file
114
src/main/java/com/smallhacker/disbrowser/asm/State.kt
Normal file
@ -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<VagueNumber> = 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)
|
||||
}
|
40
src/main/java/com/smallhacker/disbrowser/asm/VagueNumber.kt
Normal file
40
src/main/java/com/smallhacker/disbrowser/asm/VagueNumber.kt
Normal file
@ -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()
|
||||
}
|
||||
}
|
@ -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<Address>()
|
||||
val queue = ArrayDeque<State>()
|
||||
|
||||
fun tryAdd(state: State) {
|
||||
if (seen.add(state.address)) {
|
||||
queue.add(state)
|
||||
}
|
||||
}
|
||||
tryAdd(initialState)
|
||||
|
||||
val instructions = ArrayList<Instruction>()
|
||||
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<Segment> {
|
||||
val mapping = HashMap<Address, Segment>()
|
||||
val queue = ArrayDeque<State>()
|
||||
|
||||
val segments = ArrayList<Segment>()
|
||||
|
||||
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<Address, Segment>): Segment {
|
||||
val instructions = ArrayList<Instruction>()
|
||||
var lastState = initialState
|
||||
|
||||
val queue = ArrayDeque<State>()
|
||||
val seen = HashSet<Address>()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
59
src/main/java/com/smallhacker/disbrowser/util/UVal.kt
Normal file
59
src/main/java/com/smallhacker/disbrowser/util/UVal.kt
Normal file
@ -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<U : UType>(value: Int, val type: U) : Comparable<UVal<U>> {
|
||||
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<U>): 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 <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>
|
||||
|
||||
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 <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 }
|
||||
inline infix fun <reified U: UVal<V>, V: UType> U.and(other: U): U = mutate { it and other.value }
|
||||
inline infix fun <reified U: UVal<V>, V: UType> U.or(other: U): U = mutate { it or other.value }
|
||||
inline operator fun <reified U: UVal<*>> U.not(): U = mutate { it.inv() }
|
||||
|
||||
inline infix operator fun <reified U: UVal<V>, V: UType> U.plus(other: U): U = mutate { it + other.value }
|
||||
inline infix operator fun <reified U: UVal<V>, V: UType> U.minus(other: U): U = mutate { it - other.value }
|
16
src/main/java/com/smallhacker/disbrowser/util/misc.kt
Normal file
16
src/main/java/com/smallhacker/disbrowser/util/misc.kt
Normal file
@ -0,0 +1,16 @@
|
||||
package com.smallhacker.disbrowser.util
|
||||
|
||||
inline fun <reified T> 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
|
||||
}
|
||||
}
|
48
src/main/resources/main.js
Normal file
48
src/main/resources/main.js
Normal file
@ -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);
|
||||
|
||||
|
||||
|
95
src/main/resources/style.css
Normal file
95
src/main/resources/style.css
Normal file
@ -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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user