diff --git a/buildSrc/src/main/java/Versions.kt b/buildSrc/src/main/java/Versions.kt index 5ed8e948..05639674 100644 --- a/buildSrc/src/main/java/Versions.kt +++ b/buildSrc/src/main/java/Versions.kt @@ -1,6 +1,7 @@ object Versions { const val asm = "7.3.1" const val bouncyCastle = "1.64" + const val clikt = "2.5.0" const val dependencyLicenseReport = "1.13" const val fernflower = "1.0.4-SNAPSHOT" const val guava = "28.2-jre" @@ -8,6 +9,7 @@ object Versions { const val inlineLogger = "1.0.2" const val javaParser = "3.15.14" const val jdom = "2.0.6" + const val jgrapht = "1.4.0" const val jimfs = "1.1" const val junit = "5.6.0" const val kotlin = "1.3.70" diff --git a/deob-cli/build.gradle.kts b/deob-cli/build.gradle.kts new file mode 100644 index 00000000..158bed44 --- /dev/null +++ b/deob-cli/build.gradle.kts @@ -0,0 +1,36 @@ +plugins { + `maven-publish` + application + kotlin("jvm") +} + +application { + mainClassName = "dev.openrs2.deob.cli.DeobfuscatorCliKt" +} + +dependencies { + implementation(project(":asm")) + implementation(project(":deob")) + implementation(project(":deob-ir")) + implementation("com.github.ajalt:clikt:${Versions.clikt}") + implementation("com.google.guava:guava:${Versions.guava}") + implementation("org.jgrapht:jgrapht-io:${Versions.jgrapht}") + implementation("org.jgrapht:jgrapht-guava:${Versions.jgrapht}") +} + +publishing { + publications.create("maven") { + from(components["java"]) + + pom { + packaging = "jar" + name.set("OpenRS2 Deobfuscator CLI") + description.set( + """ + A command-line interface that provides access to the deobfuscators + analyis, deobfuscation, and decompilation tools. + """.trimIndent() + ) + } + } +} diff --git a/deob-cli/src/main/java/dev/openrs2/deob/cli/DeobfuscatorClassLoader.kt b/deob-cli/src/main/java/dev/openrs2/deob/cli/DeobfuscatorClassLoader.kt new file mode 100644 index 00000000..c13cac9e --- /dev/null +++ b/deob-cli/src/main/java/dev/openrs2/deob/cli/DeobfuscatorClassLoader.kt @@ -0,0 +1,49 @@ +package dev.openrs2.deob.cli + +import org.objectweb.asm.ClassReader +import org.objectweb.asm.tree.ClassNode +import java.io.File +import java.io.InputStream +import java.lang.IllegalArgumentException +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths + +private fun load(input: InputStream) = input.use { + val node = ClassNode() + val reader = ClassReader(input) + + reader.accept(node, ClassReader.SKIP_DEBUG) + + node +} + +interface DeobfuscatorClassLoader { + fun load(name: String): ClassNode +} + +object SystemClassLoader : DeobfuscatorClassLoader { + override fun load(name: String): ClassNode { + val classPath = "/${name.replace('.', File.separatorChar)}.class" + val classFile = this.javaClass.getResourceAsStream(classPath) + + return load(classFile) + } +} + +class ClasspathClassLoader(val classPath: List) : DeobfuscatorClassLoader { + override fun load(name: String): ClassNode { + val relativePath = Paths.get("${name.replace('.', File.separatorChar)}.class") + + for (entry in classPath) { + val classFilePath = entry.resolve(relativePath) + if (!Files.exists(classFilePath)) { + continue + } + + return load(Files.newInputStream(classFilePath)) + } + + throw IllegalArgumentException("Unable to find class named $name") + } +} diff --git a/deob-cli/src/main/java/dev/openrs2/deob/cli/DeobfuscatorCli.kt b/deob-cli/src/main/java/dev/openrs2/deob/cli/DeobfuscatorCli.kt new file mode 100644 index 00000000..428d1738 --- /dev/null +++ b/deob-cli/src/main/java/dev/openrs2/deob/cli/DeobfuscatorCli.kt @@ -0,0 +1,36 @@ +package dev.openrs2.deob.cli + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.subcommands +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import dev.openrs2.deob.cli.ir.PrintCfgCommand +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths + +class DeobfuscatorCli : CliktCommand(name = "deob") { + val classpath: String by option(help = "Change the classpath used to resolve class files") + .default("system") + + override fun run() { + val loader = when (classpath) { + "system" -> SystemClassLoader + else -> { + val paths : List = classpath.split(File.pathSeparatorChar).map { + Paths.get(it) + } + + val classLoader = ClasspathClassLoader(paths) + + classLoader + } + } + + currentContext.obj = DeobfuscatorOptions(loader) + } +} + +fun main(args: Array) = DeobfuscatorCli() + .subcommands(PrintCfgCommand) + .main(args) diff --git a/deob-cli/src/main/java/dev/openrs2/deob/cli/DeobfuscatorOptions.kt b/deob-cli/src/main/java/dev/openrs2/deob/cli/DeobfuscatorOptions.kt new file mode 100644 index 00000000..985321ea --- /dev/null +++ b/deob-cli/src/main/java/dev/openrs2/deob/cli/DeobfuscatorOptions.kt @@ -0,0 +1,3 @@ +package dev.openrs2.deob.cli + +data class DeobfuscatorOptions(val classLoader: DeobfuscatorClassLoader) diff --git a/deob-cli/src/main/java/dev/openrs2/deob/cli/ir/MethodScopedCommand.kt b/deob-cli/src/main/java/dev/openrs2/deob/cli/ir/MethodScopedCommand.kt new file mode 100644 index 00000000..9119fd58 --- /dev/null +++ b/deob-cli/src/main/java/dev/openrs2/deob/cli/ir/MethodScopedCommand.kt @@ -0,0 +1,26 @@ +package dev.openrs2.deob.cli.ir + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.requireObject +import com.github.ajalt.clikt.parameters.arguments.argument +import dev.openrs2.deob.cli.DeobfuscatorOptions +import dev.openrs2.deob.ir.Method +import dev.openrs2.deob.ir.translation.BytecodeToIrTranlator + +abstract class MethodScopedCommand(command: String) : CliktCommand(name = command) { + val className: String by argument(help = "Fully qualified name of the class name to disassemble") + val methodName: String by argument(help = "Name of the method to print the control flow graph for") + val options by requireObject() + + final override fun run() { + val clazz = PrintCfgCommand.options.classLoader.load(PrintCfgCommand.className) + val method = clazz.methods.find { it.name == PrintCfgCommand.methodName }!! + + val decompiler = BytecodeToIrTranlator() + val ir = decompiler.decompile(clazz, method) + + run(ir) + } + + abstract fun run(method: Method) +} diff --git a/deob-cli/src/main/java/dev/openrs2/deob/cli/ir/PrintCfgCommand.kt b/deob-cli/src/main/java/dev/openrs2/deob/cli/ir/PrintCfgCommand.kt new file mode 100644 index 00000000..0c277f7e --- /dev/null +++ b/deob-cli/src/main/java/dev/openrs2/deob/cli/ir/PrintCfgCommand.kt @@ -0,0 +1,32 @@ +package dev.openrs2.deob.cli.ir + +import com.google.common.graph.EndpointPair +import dev.openrs2.deob.ir.BasicBlock +import dev.openrs2.deob.ir.Method +import org.jgrapht.Graph +import org.jgrapht.nio.DefaultAttribute +import org.jgrapht.nio.GraphExporter +import org.jgrapht.nio.dot.DOTExporter + +typealias BlockGraph = Graph> +typealias BlockGraphExporter = GraphExporter> + +fun dotExporter(): BlockGraphExporter { + val exporter = DOTExporter>() + + exporter.setVertexAttributeProvider { + val label = it.toString().replace("\n", "\\l") + + mapOf( + "label" to DefaultAttribute.createAttribute(label) + ) + } + + return exporter +} + +object PrintCfgCommand : MethodScopedCommand("ir-print-cfg") { + override fun run(method: Method) { + TODO("Make this output expressions") + } +} diff --git a/deob-ir/build.gradle.kts b/deob-ir/build.gradle.kts new file mode 100644 index 00000000..af1e5ef8 --- /dev/null +++ b/deob-ir/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + `maven-publish` + application + kotlin("jvm") +} + +dependencies { + implementation(project(":asm")) + implementation("com.google.guava:guava:${Versions.guava}") +} + +publishing { + publications.create("maven") { + from(components["java"]) + + pom { + packaging = "jar" + name.set("OpenRS2 Deobfuscator IR") + description.set( + """ + An intermediate reprsentation of bytecode. + """.trimIndent() + ) + } + } +} diff --git a/deob-ir/src/main/java/dev/openrs2/deob/ir/BasicBlock.kt b/deob-ir/src/main/java/dev/openrs2/deob/ir/BasicBlock.kt new file mode 100644 index 00000000..0fd44f68 --- /dev/null +++ b/deob-ir/src/main/java/dev/openrs2/deob/ir/BasicBlock.kt @@ -0,0 +1,27 @@ +package dev.openrs2.deob.ir + +import com.google.common.graph.MutableValueGraph +import dev.openrs2.deob.ir.flow.ControlFlowTransfer + +typealias BasicBlockGraph = MutableValueGraph + +class BasicBlock( + internal val graph: BasicBlockGraph +) { + val statements = mutableListOf() + + fun successors(): Set = graph.successors(this) + fun predecessors(): Set = graph.predecessors(this) + + override fun equals(other: Any?): Boolean { + return this === other + } + + override fun hashCode(): Int { + return System.identityHashCode(this) + } + + override fun toString(): String { + return "BasicBlock(expressions=\n${statements.joinToString("\n")}\n)" + } +} diff --git a/deob-ir/src/main/java/dev/openrs2/deob/ir/Expr.kt b/deob-ir/src/main/java/dev/openrs2/deob/ir/Expr.kt new file mode 100644 index 00000000..9156611d --- /dev/null +++ b/deob-ir/src/main/java/dev/openrs2/deob/ir/Expr.kt @@ -0,0 +1,52 @@ +package dev.openrs2.deob.ir + +import dev.openrs2.asm.MemberRef +import org.objectweb.asm.Type + +sealed class Storage + +class LocalVarStorage(val slot: Int = -1) : Storage() +class FieldStorage(val instance: Expr, val member: MemberRef) : Storage() +class StaticStorage(val member: MemberRef) : Storage() + +sealed class UnaryOp { + object Negate : UnaryOp() + data class InstanceOf(val type: Type) : UnaryOp() + data class Cast(val type: Type) : UnaryOp() +} + +enum class BinOp { + Equals, + NotEquals, + LessThan, + GreaterThanOrEquals, + GreaterThan, + LessThanOrEquals, + Remainder, + Divide, + Multiply, + Add, + Subtract, + ShiftLeft, + ShiftRight, + UnsignedShiftRight, + ExclusiveOr +} + +data class BinaryExpr(val lhs: Expr, val rhs: Expr, val op: dev.openrs2.deob.ir.BinOp) : Expr() + +sealed class Expr + +data class IndexExpr(val array: Expr, val index: Expr) : Expr() + +data class UnaryExpr(val operand: Expr, val operator: UnaryOp) : Expr() + +data class ConstExpr(val value: Any?) : Expr() + +data class CallExpr(val instance: Expr?, val method: MemberRef, val arguments: List) : Expr() + +data class VarExpr(val storage: Storage) : Expr() { + companion object { + fun local(slot: Int) = VarExpr(LocalVarStorage(slot)) + } +} diff --git a/deob-ir/src/main/java/dev/openrs2/deob/ir/Method.kt b/deob-ir/src/main/java/dev/openrs2/deob/ir/Method.kt new file mode 100644 index 00000000..52124b94 --- /dev/null +++ b/deob-ir/src/main/java/dev/openrs2/deob/ir/Method.kt @@ -0,0 +1,11 @@ +package dev.openrs2.deob.ir + +import com.google.common.graph.MutableValueGraph +import dev.openrs2.deob.ir.flow.ControlFlowTransfer +import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.MethodNode + +class Method(val owner: ClassNode, val method: MethodNode, val entry: BasicBlock) { + val cfg: MutableValueGraph + get() = entry.graph +} diff --git a/deob-ir/src/main/java/dev/openrs2/deob/ir/Stmt.kt b/deob-ir/src/main/java/dev/openrs2/deob/ir/Stmt.kt new file mode 100644 index 00000000..d9ff1ba2 --- /dev/null +++ b/deob-ir/src/main/java/dev/openrs2/deob/ir/Stmt.kt @@ -0,0 +1,22 @@ +package dev.openrs2.deob.ir + +sealed class Stmt { + fun accept(visitor: StmtVisitor) = when (this) { + is AssignmentStmt -> visitor.visitAssignmen(slot, expr) + is CallStmt -> visitor.visitCall(expr) + is IfStmt -> visitor.visitIf(expr) + is ReturnStmt -> visitor.visitReturn(expr) + } +} + +data class AssignmentStmt(val slot: VarExpr, val expr: Expr) : Stmt() +data class CallStmt(val expr: CallExpr) : Stmt() +data class IfStmt(val expr: BinaryExpr) : Stmt() +data class ReturnStmt(val expr: Expr?) : Stmt() + +interface StmtVisitor { + fun visitAssignmen(variable: VarExpr, value: Expr) + fun visitCall(expr: CallExpr) + fun visitIf(conditional: BinaryExpr) + fun visitReturn(value: Expr?) +} diff --git a/deob-ir/src/main/java/dev/openrs2/deob/ir/flow/ControlFlowAnalyzer.kt b/deob-ir/src/main/java/dev/openrs2/deob/ir/flow/ControlFlowAnalyzer.kt new file mode 100644 index 00000000..af5a3616 --- /dev/null +++ b/deob-ir/src/main/java/dev/openrs2/deob/ir/flow/ControlFlowAnalyzer.kt @@ -0,0 +1,80 @@ +package dev.openrs2.deob.ir.flow + +import com.google.common.graph.Graph +import com.google.common.graph.GraphBuilder +import org.objectweb.asm.tree.MethodNode +import org.objectweb.asm.tree.analysis.Analyzer +import org.objectweb.asm.tree.analysis.BasicInterpreter +import org.objectweb.asm.tree.analysis.BasicValue +import java.util.Stack + +/** + * A bytecode analyzer that produces a graph representing the basic control structure of a method. + * + * @param reduce if the graph should be reduced by coalescing all nodes with an immediate dominator + * with only a single successor. + */ +class ControlFlowAnalyzer(private val reduce: Boolean = false) : Analyzer(BasicInterpreter()) { + val instructionsToBlocks = mutableMapOf() + + private val graph = GraphBuilder + .directed() + .allowsSelfLoops(true) + .build() + + override fun newControlFlowEdge(insnIndex: Int, successorIndex: Int) { + graph.putEdge(insnIndex, successorIndex) + } + + override fun newControlFlowExceptionEdge(insnIndex: Int, successorIndex: Int): Boolean { + graph.putEdge(insnIndex, successorIndex) + return true + } + + private fun reduceGraph() { + val nodeQueue = Stack() + val nodesVisited = mutableSetOf() + + nodeQueue.push(0) + + while (nodeQueue.isNotEmpty()) { + // Coalesce all nodes that are immediately dominated and are the sole successor + // of their dominator + val current = nodeQueue.pop() + val nextSuccessors = generateSequence { graph.successors(current).singleOrNull() }.iterator() + + for (successor in nextSuccessors) { + val isImmediateDominator = current == graph.predecessors(successor).singleOrNull() + + if (!isImmediateDominator || successor == current) { + break + } + + for (domSuccessor in graph.successors(successor)) { + graph.putEdge(current, domSuccessor) + } + + graph.removeEdge(current, successor) + graph.removeNode(successor) + } + + for (successor in graph.successors(current)) { + if (!nodesVisited.contains(successor)) { + nodeQueue.push(successor) + } + } + + nodesVisited += current + } + } + + fun createGraph(owner: String, method: MethodNode): Graph { + analyze(owner, method) + + if (reduce) { + reduceGraph() + } + + return graph + } +} diff --git a/deob-ir/src/main/java/dev/openrs2/deob/ir/flow/ControlFlowTransfer.kt b/deob-ir/src/main/java/dev/openrs2/deob/ir/flow/ControlFlowTransfer.kt new file mode 100644 index 00000000..3b206fae --- /dev/null +++ b/deob-ir/src/main/java/dev/openrs2/deob/ir/flow/ControlFlowTransfer.kt @@ -0,0 +1,8 @@ +package dev.openrs2.deob.ir.flow + +sealed class ControlFlowTransfer { + + data class ConditionalJump(val successful: Boolean) : ControlFlowTransfer() + + object Goto : ControlFlowTransfer() +} diff --git a/deob/src/main/java/dev/openrs2/deob/analysis/DataFlowAnalyzer.kt b/deob-ir/src/main/java/dev/openrs2/deob/ir/flow/data/DataFlowAnalyzer.kt similarity index 95% rename from deob/src/main/java/dev/openrs2/deob/analysis/DataFlowAnalyzer.kt rename to deob-ir/src/main/java/dev/openrs2/deob/ir/flow/data/DataFlowAnalyzer.kt index 4885b044..def3c339 100644 --- a/deob/src/main/java/dev/openrs2/deob/analysis/DataFlowAnalyzer.kt +++ b/deob-ir/src/main/java/dev/openrs2/deob/ir/flow/data/DataFlowAnalyzer.kt @@ -1,7 +1,8 @@ -package dev.openrs2.deob.analysis +package dev.openrs2.deob.ir.flow.data import com.google.common.graph.Graph import com.google.common.graph.Graphs +import dev.openrs2.deob.ir.flow.ControlFlowAnalyzer import org.objectweb.asm.tree.AbstractInsnNode import org.objectweb.asm.tree.MethodNode diff --git a/deob/src/main/java/dev/openrs2/deob/analysis/LiveVariableAnalyzer.kt b/deob-ir/src/main/java/dev/openrs2/deob/ir/flow/data/LiveVariableAnalyzer.kt similarity index 96% rename from deob/src/main/java/dev/openrs2/deob/analysis/LiveVariableAnalyzer.kt rename to deob-ir/src/main/java/dev/openrs2/deob/ir/flow/data/LiveVariableAnalyzer.kt index 924d154e..db2de217 100644 --- a/deob/src/main/java/dev/openrs2/deob/analysis/LiveVariableAnalyzer.kt +++ b/deob-ir/src/main/java/dev/openrs2/deob/ir/flow/data/LiveVariableAnalyzer.kt @@ -1,4 +1,4 @@ -package dev.openrs2.deob.analysis +package dev.openrs2.deob.ir.flow.data import org.objectweb.asm.Opcodes import org.objectweb.asm.tree.AbstractInsnNode diff --git a/deob-ir/src/main/java/dev/openrs2/deob/ir/translation/BytecodeToIrTranlator.kt b/deob-ir/src/main/java/dev/openrs2/deob/ir/translation/BytecodeToIrTranlator.kt new file mode 100644 index 00000000..58619110 --- /dev/null +++ b/deob-ir/src/main/java/dev/openrs2/deob/ir/translation/BytecodeToIrTranlator.kt @@ -0,0 +1,16 @@ +package dev.openrs2.deob.ir.translation + +import dev.openrs2.deob.ir.Method +import dev.openrs2.deob.ir.translation.decompiler.IrAnalyzer +import dev.openrs2.deob.ir.translation.decompiler.IrInterpreter +import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.MethodNode + +class BytecodeToIrTranlator { + fun decompile(owner: ClassNode, method: MethodNode): Method { + val irAnalyzer = IrAnalyzer(IrInterpreter()) + val entryBlock = irAnalyzer.decode(owner.name, method) + + return Method(owner, method, entryBlock) + } +} diff --git a/deob-ir/src/main/java/dev/openrs2/deob/ir/translation/decompiler/IrAnalyzer.kt b/deob-ir/src/main/java/dev/openrs2/deob/ir/translation/decompiler/IrAnalyzer.kt new file mode 100644 index 00000000..98265d81 --- /dev/null +++ b/deob-ir/src/main/java/dev/openrs2/deob/ir/translation/decompiler/IrAnalyzer.kt @@ -0,0 +1,106 @@ +package dev.openrs2.deob.ir.translation.decompiler + +import com.google.common.graph.ValueGraphBuilder +import dev.openrs2.deob.ir.AssignmentStmt +import dev.openrs2.deob.ir.BasicBlock +import dev.openrs2.deob.ir.BasicBlockGraph +import dev.openrs2.deob.ir.BinaryExpr +import dev.openrs2.deob.ir.CallExpr +import dev.openrs2.deob.ir.CallStmt +import dev.openrs2.deob.ir.Expr +import dev.openrs2.deob.ir.IfStmt +import dev.openrs2.deob.ir.ReturnStmt +import dev.openrs2.deob.ir.StmtVisitor +import dev.openrs2.deob.ir.VarExpr +import dev.openrs2.deob.ir.flow.ControlFlowTransfer +import org.objectweb.asm.Opcodes +import org.objectweb.asm.tree.InsnList +import org.objectweb.asm.tree.JumpInsnNode +import org.objectweb.asm.tree.MethodNode +import org.objectweb.asm.tree.analysis.Analyzer + +class IrAnalyzer(private val interpreter: IrInterpreter) : StmtVisitor, Analyzer(interpreter) { + init { + interpreter.visitor = this + } + + /** + * A mapping of program counters (instruction offsets) to the [BasicBlock]s + * that define them. + */ + private lateinit var blocks: MutableMap + + /** + * The control flow graph defining the method. + */ + private lateinit var graph: BasicBlockGraph + + /** + * The current [BasicBlock] of the method that is being analyzed. + */ + private lateinit var bb: BasicBlock + + /** + * The code belonging to the current method being analyzed. + */ + private lateinit var code: InsnList + + /** + * Get or create a new basic block that begins at the instruction with the given [index]. + */ + private fun bb(index: Int) = blocks.computeIfAbsent(index) { BasicBlock(graph) } + + /** + * Run the analyzer on the [method] owned by the class with name [owner] and return the [BasicBlock] + * that defines method entry. + */ + fun decode(owner: String, method: MethodNode): BasicBlock { + analyze(owner, method) + return blocks[0] ?: error("No entry BasicBlock found") + } + + override fun init(owner: String, method: MethodNode) { + graph = ValueGraphBuilder.directed() + .allowsSelfLoops(true) + .build() + + bb = BasicBlock(graph) + code = method.instructions + blocks = mutableMapOf(0 to bb) + } + + override fun newControlFlowEdge(insnIndex: Int, successorIndex: Int) { + when (val insn = code[insnIndex]) { + is JumpInsnNode -> { + val transfer = if (insn.opcode == Opcodes.GOTO) { + ControlFlowTransfer.ConditionalJump(code[successorIndex] == insn.label) + } else { + ControlFlowTransfer.Goto + } + + graph.putEdgeValue(bb, bb(successorIndex), transfer) + } + } + + val startsBlock = blocks[insnIndex] + if (startsBlock != null) { + bb = startsBlock + } + } + + override fun visitAssignmen(variable: VarExpr, value: Expr) { + bb.statements.add(AssignmentStmt(variable, value)) + } + + override fun visitCall(expr: CallExpr) { + bb.statements.add(CallStmt(expr)) + } + + override fun visitIf(conditional: BinaryExpr) { + bb.statements.add(IfStmt(conditional)) + } + + override fun visitReturn(value: Expr?) { + bb.statements.add(ReturnStmt(value)) + } +} diff --git a/deob-ir/src/main/java/dev/openrs2/deob/ir/translation/decompiler/IrInterpreter.kt b/deob-ir/src/main/java/dev/openrs2/deob/ir/translation/decompiler/IrInterpreter.kt new file mode 100644 index 00000000..4a1d2193 --- /dev/null +++ b/deob-ir/src/main/java/dev/openrs2/deob/ir/translation/decompiler/IrInterpreter.kt @@ -0,0 +1,334 @@ +package dev.openrs2.deob.ir.translation.decompiler + +import dev.openrs2.asm.MemberRef +import dev.openrs2.asm.intConstant +import dev.openrs2.asm.toPrettyString +import dev.openrs2.deob.ir.BinOp +import dev.openrs2.deob.ir.BinaryExpr +import dev.openrs2.deob.ir.CallExpr +import dev.openrs2.deob.ir.ConstExpr +import dev.openrs2.deob.ir.FieldStorage +import dev.openrs2.deob.ir.IndexExpr +import dev.openrs2.deob.ir.LocalVarStorage +import dev.openrs2.deob.ir.StaticStorage +import dev.openrs2.deob.ir.StmtVisitor +import dev.openrs2.deob.ir.Storage +import dev.openrs2.deob.ir.UnaryExpr +import dev.openrs2.deob.ir.UnaryOp +import dev.openrs2.deob.ir.VarExpr +import org.objectweb.asm.Opcodes +import org.objectweb.asm.Opcodes.ACONST_NULL +import org.objectweb.asm.Opcodes.ALOAD +import org.objectweb.asm.Opcodes.ARETURN +import org.objectweb.asm.Opcodes.ASM7 +import org.objectweb.asm.Opcodes.ASTORE +import org.objectweb.asm.Opcodes.CHECKCAST +import org.objectweb.asm.Opcodes.D2F +import org.objectweb.asm.Opcodes.D2L +import org.objectweb.asm.Opcodes.DADD +import org.objectweb.asm.Opcodes.DCMPG +import org.objectweb.asm.Opcodes.DDIV +import org.objectweb.asm.Opcodes.DMUL +import org.objectweb.asm.Opcodes.DNEG +import org.objectweb.asm.Opcodes.DREM +import org.objectweb.asm.Opcodes.DSUB +import org.objectweb.asm.Opcodes.DUP +import org.objectweb.asm.Opcodes.F2L +import org.objectweb.asm.Opcodes.GETFIELD +import org.objectweb.asm.Opcodes.GETSTATIC +import org.objectweb.asm.Opcodes.I2B +import org.objectweb.asm.Opcodes.I2C +import org.objectweb.asm.Opcodes.I2D +import org.objectweb.asm.Opcodes.I2F +import org.objectweb.asm.Opcodes.I2L +import org.objectweb.asm.Opcodes.I2S +import org.objectweb.asm.Opcodes.IADD +import org.objectweb.asm.Opcodes.IALOAD +import org.objectweb.asm.Opcodes.ICONST_M1 +import org.objectweb.asm.Opcodes.IDIV +import org.objectweb.asm.Opcodes.IFEQ +import org.objectweb.asm.Opcodes.IFGE +import org.objectweb.asm.Opcodes.IFGT +import org.objectweb.asm.Opcodes.IFLE +import org.objectweb.asm.Opcodes.IFLT +import org.objectweb.asm.Opcodes.IFNE +import org.objectweb.asm.Opcodes.IFNONNULL +import org.objectweb.asm.Opcodes.IFNULL +import org.objectweb.asm.Opcodes.IF_ACMPEQ +import org.objectweb.asm.Opcodes.IF_ACMPNE +import org.objectweb.asm.Opcodes.IF_ICMPEQ +import org.objectweb.asm.Opcodes.IF_ICMPGE +import org.objectweb.asm.Opcodes.IF_ICMPGT +import org.objectweb.asm.Opcodes.IF_ICMPLE +import org.objectweb.asm.Opcodes.IF_ICMPLT +import org.objectweb.asm.Opcodes.IF_ICMPNE +import org.objectweb.asm.Opcodes.ILOAD +import org.objectweb.asm.Opcodes.IMUL +import org.objectweb.asm.Opcodes.INEG +import org.objectweb.asm.Opcodes.INSTANCEOF +import org.objectweb.asm.Opcodes.IREM +import org.objectweb.asm.Opcodes.IRETURN +import org.objectweb.asm.Opcodes.ISHL +import org.objectweb.asm.Opcodes.ISHR +import org.objectweb.asm.Opcodes.ISTORE +import org.objectweb.asm.Opcodes.ISUB +import org.objectweb.asm.Opcodes.IUSHR +import org.objectweb.asm.Opcodes.IXOR +import org.objectweb.asm.Opcodes.L2D +import org.objectweb.asm.Opcodes.L2F +import org.objectweb.asm.Opcodes.LALOAD +import org.objectweb.asm.Opcodes.LDC +import org.objectweb.asm.Opcodes.LOOKUPSWITCH +import org.objectweb.asm.Opcodes.LSHL +import org.objectweb.asm.Opcodes.LSHR +import org.objectweb.asm.Opcodes.LUSHR +import org.objectweb.asm.Opcodes.LXOR +import org.objectweb.asm.Opcodes.NEW +import org.objectweb.asm.Opcodes.PUTFIELD +import org.objectweb.asm.Opcodes.SIPUSH +import org.objectweb.asm.Opcodes.SWAP +import org.objectweb.asm.Opcodes.TABLESWITCH +import org.objectweb.asm.Type +import org.objectweb.asm.tree.AbstractInsnNode +import org.objectweb.asm.tree.FieldInsnNode +import org.objectweb.asm.tree.JumpInsnNode +import org.objectweb.asm.tree.LdcInsnNode +import org.objectweb.asm.tree.LookupSwitchInsnNode +import org.objectweb.asm.tree.MethodInsnNode +import org.objectweb.asm.tree.MultiANewArrayInsnNode +import org.objectweb.asm.tree.TableSwitchInsnNode +import org.objectweb.asm.tree.TypeInsnNode +import org.objectweb.asm.tree.VarInsnNode +import org.objectweb.asm.tree.analysis.Interpreter + +class IrInterpreter : Interpreter(ASM7) { + lateinit var visitor: StmtVisitor + + override fun newValue(type: Type?) = IrValue(null) + + override fun newEmptyValue(local: Int) = IrValue(VarExpr.local(local)) + + override fun newParameterValue(isInstanceMethod: Boolean, local: Int, type: Type) = + IrValue(VarExpr.local(local), type) + + override fun naryOperation(insn: AbstractInsnNode?, values: MutableList): IrValue? = when (insn) { + is MethodInsnNode -> handleMethodOperation(insn, values) + is MultiANewArrayInsnNode -> handleNewMultiArrayOperation(insn, values) + else -> error("Unrecognized nary operation instruction: $insn") + } + + override fun ternaryOperation( + insn: AbstractInsnNode?, + value1: IrValue?, + value2: IrValue?, + value3: IrValue? + ): IrValue { + TODO("Not yet implemented") + } + + override fun merge(value1: IrValue?, value2: IrValue?): IrValue? { + return value1 + } + + override fun newReturnTypeValue(type: Type?): IrValue? { + if (type == Type.VOID_TYPE) { + return null + } + + return IrValue(null, type) + } + + /** + * We ignore this, since we catch all of the return instructions (outwith RETURN) in [unaryOperation]. + */ + override fun returnOperation(insn: AbstractInsnNode, value: IrValue, expected: IrValue) {} + + /** + * Interprets a bytecode instruction with a single argument. This method is called for the + * following opcodes: + * + *

INEG, LNEG, FNEG, DNEG, IINC, I2L, I2F, I2D, L2I, L2F, L2D, F2I, F2L, F2D, D2I, D2L, D2F, + * I2B, I2C, I2S, IFEQ, IFNE, IFLT, IFGE, IFGT, IFLE, TABLESWITCH, LOOKUPSWITCH, IRETURN, LRETURN, + * FRETURN, DRETURN, ARETURN, PUTSTATIC, GETFIELD, NEWARRAY, ANEWARRAY, ARRAYLENGTH, ATHROW, + * CHECKCAST, INSTANCEOF, MONITORENTER, MONITOREXIT, IFNULL, IFNONNULL + * + * @param insn the bytecode instruction to be interpreted. + * @param value the argument of the instruction to be interpreted. + * @return the result of the interpretation of the given instruction. + * @throws AnalyzerException if an error occurred during the interpretation. + */ + override fun unaryOperation(insn: AbstractInsnNode, value: IrValue) = when (insn.opcode) { + I2B -> handleCastOperation(insn, value, Type.BYTE_TYPE) + I2C -> handleCastOperation(insn, value, Type.CHAR_TYPE) + I2S -> handleCastOperation(insn, value, Type.SHORT_TYPE) + I2D, L2D -> handleCastOperation(insn, value, Type.DOUBLE_TYPE) + I2L, D2L, F2L -> handleCastOperation(insn, value, Type.LONG_TYPE) + I2F, D2F, L2F -> handleCastOperation(insn, value, Type.FLOAT_TYPE) + CHECKCAST -> handleCastOperation(insn, value, (insn as TypeInsnNode).desc.let(Type::getType)) + INSTANCEOF -> handleCastOperation( + insn, + value, + (insn as TypeInsnNode).desc.let(Type::getType), + instanceOf = true + ) + GETFIELD -> handleVarLoadOperation( + insn, + FieldStorage(value.expr!!, MemberRef(insn as FieldInsnNode)) + ) + IFNULL, IFNONNULL -> handleConditionalOperation( + insn as JumpInsnNode, + value, + IrValue(ConstExpr(null), value.type) + ) + TABLESWITCH -> handleTableSwitchOperation(insn as TableSwitchInsnNode, value) + LOOKUPSWITCH -> handleLookupSwitchOperation(insn as LookupSwitchInsnNode, value) + in INEG..DNEG -> handleUnaryOperation(insn,value, UnaryOp.Negate) + in IFEQ..IFLE -> handleConditionalOperation(insn as JumpInsnNode, value, IrValue(ConstExpr(0), Type.INT_TYPE)) + in IRETURN..ARETURN -> handleReturnOperation(insn, value) + else -> error("Invalid instruction for unary expression: ${insn.toPrettyString()}") + } + + private fun handleTableSwitchOperation(tableSwitchInsnNode: TableSwitchInsnNode, value: IrValue): IrValue? { + TODO("Not yet implemented") + } + + private fun handleLookupSwitchOperation(lookupSwitchInsnNode: LookupSwitchInsnNode, value: IrValue): IrValue? { + TODO("Not yet implemented") + } + + /** + * Interprets a bytecode instruction with two arguments. This method is called for the following + * opcodes: + * + *

IALOAD, LALOAD, FALOAD, DALOAD, AALOAD, BALOAD, CALOAD, SALOAD, IADD, LADD, FADD, DADD, + * ISUB, LSUB, FSUB, DSUB, IMUL, LMUL, FMUL, DMUL, IDIV, LDIV, FDIV, DDIV, IREM, LREM, FREM, DREM, + * ISHL, LSHL, ISHR, LSHR, IUSHR, LUSHR, IAND, LAND, IOR, LOR, IXOR, LXOR, LCMP, FCMPL, FCMPG, + * DCMPL, DCMPG, IF_ICMPEQ, IF_ICMPNE, IF_ICMPLT, IF_ICMPGE, IF_ICMPGT, IF_ICMPLE, IF_ACMPEQ, + * IF_ACMPNE, PUTFIELD + * + * @param insn the bytecode instruction to be interpreted. + * @param value1 the first argument of the instruction to be interpreted. + * @param value2 the second argument of the instruction to be interpreted. + * @return the result of the interpretation of the given instruction. + * @throws AnalyzerException if an error occurred during the interpretation. + */ + override fun binaryOperation(insn: AbstractInsnNode, lhs: IrValue, rhs: IrValue) = when (insn.opcode) { + PUTFIELD -> handleVarStoreOperation(insn, FieldStorage(lhs.expr!!, MemberRef(insn as FieldInsnNode)), rhs) + in IALOAD..LALOAD -> handleArrayLoadOperation(insn, lhs, rhs) + in IADD..DCMPG -> handleBinaryOperation(insn, lhs, rhs) + in IF_ICMPEQ..IF_ACMPNE -> handleConditionalOperation(insn as JumpInsnNode, lhs, rhs) + else -> error("Unhandled binary operation instruction: $insn") + } + + private fun handleArrayLoadOperation(insn: AbstractInsnNode, index: IrValue, array: IrValue): IrValue? = + IrValue(IndexExpr(array.expr!!, index.expr!!)) + + override fun copyOperation(insn: AbstractInsnNode, value: IrValue) = when (insn.opcode) { + in ISTORE..ASTORE -> { + handleVarStoreOperation(insn, LocalVarStorage((insn as VarInsnNode).`var`), value) + value + } + in ILOAD..ALOAD -> handleVarLoadOperation(insn, LocalVarStorage((insn as VarInsnNode).`var`)) + in DUP..SWAP -> { + val local = LocalVarStorage() + + handleVarStoreOperation(insn, local, value) + handleVarLoadOperation(insn, local) + } + else -> error("Invalid copy instruction: $insn") + } + + override fun newOperation(insn: AbstractInsnNode) = when (insn.opcode) { + NEW -> IrValue(null, (insn as TypeInsnNode).desc.let(Type::getType)) + ACONST_NULL -> handleConstantOperation(null) + LDC -> handleConstantOperation((insn as LdcInsnNode).cst) + GETSTATIC -> handleVarLoadOperation(insn, StaticStorage(MemberRef((insn as FieldInsnNode)))) + in ICONST_M1..SIPUSH -> handleConstantOperation(insn.intConstant!!) + else -> error("Unhandled new operation instruction: $insn") + } + + private fun handleConstantOperation(value: Any?) = IrValue(ConstExpr(value)) + + fun handleBinaryOperation(insn: AbstractInsnNode, lhs: IrValue, rhs: IrValue): IrValue? { + val op = when (insn.opcode) { + in ISUB..DSUB -> BinOp.Subtract + in IADD..DADD -> BinOp.Add + in IMUL..DMUL -> BinOp.Multiply + in IDIV..DDIV -> BinOp.Divide + in IREM..DREM -> BinOp.Remainder + in ISHL..LSHL -> BinOp.ShiftLeft + in ISHR..LSHR -> BinOp.ShiftRight + in IUSHR..LUSHR -> BinOp.UnsignedShiftRight + in IXOR..LXOR -> BinOp.ExclusiveOr + else -> error("Invalid instruction for binary expression: ${insn.toPrettyString()}") + } + + return IrValue(BinaryExpr(lhs.expr!!, rhs.expr!!, op)) + } + + fun handleCastOperation(insn: AbstractInsnNode, value: IrValue, type: Type, instanceOf: Boolean = false): IrValue { + val op = if (instanceOf) { + UnaryOp.InstanceOf(type) + } else { + UnaryOp.Cast(type) + } + + return IrValue(UnaryExpr(value.expr!!, op)) + } + + private fun handleConditionalOperation(insn: JumpInsnNode, lhs: IrValue, rhs: IrValue): IrValue? { + val op = when (insn.opcode) { + IFEQ, IF_ACMPEQ, IF_ICMPEQ, IFNULL -> BinOp.Equals + IFNE, IF_ACMPNE, IF_ICMPNE, IFNONNULL -> BinOp.NotEquals + IFLE, IF_ICMPLE -> BinOp.LessThanOrEquals + IFLT, IF_ICMPLT -> BinOp.LessThan + IFGE, IF_ICMPGE -> BinOp.GreaterThanOrEquals + IFGT, IF_ICMPGT -> BinOp.GreaterThan + else -> error("Not a valid conditional instruction: $insn") + } + + visitor.visitIf(BinaryExpr(lhs.expr!!, rhs.expr!!, op)) + return null + } + + fun handleMethodOperation(insn: MethodInsnNode, values: MutableList): IrValue? { + val method = MemberRef(insn) + val returnType = Type.getReturnType(insn.desc) + val instance = when (insn.opcode) { + Opcodes.INVOKESTATIC -> null + else -> values.removeAt(0).expr ?: error("No expression found for isntance slot") + } + + val call = CallExpr(instance, method, values.map { it.expr!! }) + + return when (returnType) { + Type.VOID_TYPE -> { + visitor.visitCall(call) + null + } + else -> IrValue(call, returnType) + } + } + + fun handleNewMultiArrayOperation(insn: MultiANewArrayInsnNode, values: MutableList): IrValue? { + return null + } + + fun handleReturnOperation(insn: AbstractInsnNode, value: IrValue?): IrValue? { + visitor.visitReturn(value?.expr) + return null + } + + fun handleUnaryOperation(insn: AbstractInsnNode, value: IrValue, operator: UnaryOp): IrValue { + return IrValue(UnaryExpr(value.expr!!, operator)) + } + + fun handleVarLoadOperation(insn: AbstractInsnNode, storage: Storage): IrValue? { + return IrValue(VarExpr(storage)) + } + + fun handleVarStoreOperation(insn: AbstractInsnNode, storage: Storage, value: IrValue): IrValue? { + visitor.visitAssignmen(VarExpr(storage), value.expr!!) + return null + } +} diff --git a/deob-ir/src/main/java/dev/openrs2/deob/ir/translation/decompiler/IrValue.kt b/deob-ir/src/main/java/dev/openrs2/deob/ir/translation/decompiler/IrValue.kt new file mode 100644 index 00000000..e24b6cbc --- /dev/null +++ b/deob-ir/src/main/java/dev/openrs2/deob/ir/translation/decompiler/IrValue.kt @@ -0,0 +1,9 @@ +package dev.openrs2.deob.ir.translation.decompiler + +import dev.openrs2.deob.ir.Expr +import org.objectweb.asm.Type +import org.objectweb.asm.tree.analysis.Value + +class IrValue(val expr: Expr?, val type: Type? = null) : Value { + override fun getSize() = type?.size ?: 1 +} diff --git a/deob-ir/src/test/java/dev/openrs2/deob/ir/translation/BytecodeToIrTranlatorTests.kt b/deob-ir/src/test/java/dev/openrs2/deob/ir/translation/BytecodeToIrTranlatorTests.kt new file mode 100644 index 00000000..ad261796 --- /dev/null +++ b/deob-ir/src/test/java/dev/openrs2/deob/ir/translation/BytecodeToIrTranlatorTests.kt @@ -0,0 +1,31 @@ +package dev.openrs2.deob.ir.translation + +import dev.openrs2.deob.ir.translation.fixture.Fixture +import dev.openrs2.deob.ir.translation.fixture.FixtureMethod +import org.junit.jupiter.api.Test + +class BytecodeToIrTranlatorTests { + + @Test + fun `Creates entry basic block`() { + class CfgSample() : Fixture { + val a = true + val b = false + + override fun test() { + if (a) { + println("a") + } else if (b) { + println("b") + } + } + } + + val fixture = FixtureMethod.from(CfgSample::class) + val decompiler = BytecodeToIrTranlator() + val irMethod = decompiler.decompile(fixture.owner, fixture.method) + + // @TODO - some way of asserting on the output cfg? + } +} + diff --git a/deob-ir/src/test/java/dev/openrs2/deob/ir/translation/fixture/Fixture.java b/deob-ir/src/test/java/dev/openrs2/deob/ir/translation/fixture/Fixture.java new file mode 100644 index 00000000..3dc411fc --- /dev/null +++ b/deob-ir/src/test/java/dev/openrs2/deob/ir/translation/fixture/Fixture.java @@ -0,0 +1,10 @@ +package dev.openrs2.deob.ir.translation.fixture; + +import dev.openrs2.deob.ir.translation.BytecodeToIrTranlatorTests; + +/** + * Marker interface for {@link BytecodeToIrTranlatorTests}. + */ +public interface Fixture { + void test(); +} diff --git a/deob-ir/src/test/java/dev/openrs2/deob/ir/translation/fixture/FixtureMethod.kt b/deob-ir/src/test/java/dev/openrs2/deob/ir/translation/fixture/FixtureMethod.kt new file mode 100644 index 00000000..1061178e --- /dev/null +++ b/deob-ir/src/test/java/dev/openrs2/deob/ir/translation/fixture/FixtureMethod.kt @@ -0,0 +1,28 @@ +package dev.openrs2.deob.ir.translation.fixture + +import org.objectweb.asm.ClassReader +import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.MethodNode +import java.io.File +import kotlin.reflect.KClass + +class FixtureMethod private constructor(val owner: ClassNode, val method: MethodNode) { + companion object { + fun from(ty: KClass): FixtureMethod { + val classPath = "/${ty.java.name.replace('.', File.separatorChar)}.class" + val classFile = ty.java.getResourceAsStream(classPath) + + return classFile.use { + val node = ClassNode() + val reader = ClassReader(classFile) + + reader.accept(node, ClassReader.SKIP_DEBUG) + + val method = node.methods.find { it.name == "test" } ?: + throw IllegalStateException("Fixture class has no test() method") + + return FixtureMethod(node, method) + } + } + } +} diff --git a/deob/build.gradle.kts b/deob/build.gradle.kts index 4e614403..a1067ae7 100644 --- a/deob/build.gradle.kts +++ b/deob/build.gradle.kts @@ -11,6 +11,7 @@ application { dependencies { implementation(project(":bundler")) implementation(project(":deob-annotations")) + implementation(project(":deob-ir")) implementation("com.google.guava:guava:${Versions.guava}") } diff --git a/deob/src/main/java/dev/openrs2/deob/analysis/ControlFlowAnalyzer.kt b/deob/src/main/java/dev/openrs2/deob/analysis/ControlFlowAnalyzer.kt deleted file mode 100644 index d75b8ef7..00000000 --- a/deob/src/main/java/dev/openrs2/deob/analysis/ControlFlowAnalyzer.kt +++ /dev/null @@ -1,29 +0,0 @@ -package dev.openrs2.deob.analysis - -import com.google.common.graph.Graph -import com.google.common.graph.GraphBuilder -import org.objectweb.asm.tree.MethodNode -import org.objectweb.asm.tree.analysis.Analyzer -import org.objectweb.asm.tree.analysis.BasicInterpreter -import org.objectweb.asm.tree.analysis.BasicValue - -class ControlFlowAnalyzer : Analyzer(BasicInterpreter()) { - private val graph = GraphBuilder - .directed() - .allowsSelfLoops(true) - .immutable() - - override fun newControlFlowEdge(insnIndex: Int, successorIndex: Int) { - graph.putEdge(insnIndex, successorIndex) - } - - override fun newControlFlowExceptionEdge(insnIndex: Int, successorIndex: Int): Boolean { - graph.putEdge(insnIndex, successorIndex) - return true - } - - fun createGraph(owner: String, method: MethodNode): Graph { - analyze(owner, method) - return graph.build() - } -} diff --git a/deob/src/main/java/dev/openrs2/deob/transform/DummyLocalTransformer.kt b/deob/src/main/java/dev/openrs2/deob/transform/DummyLocalTransformer.kt index 327e221a..f3bd848e 100644 --- a/deob/src/main/java/dev/openrs2/deob/transform/DummyLocalTransformer.kt +++ b/deob/src/main/java/dev/openrs2/deob/transform/DummyLocalTransformer.kt @@ -6,7 +6,7 @@ import dev.openrs2.asm.classpath.Library import dev.openrs2.asm.deleteExpression import dev.openrs2.asm.pure import dev.openrs2.asm.transform.Transformer -import dev.openrs2.deob.analysis.LiveVariableAnalyzer +import dev.openrs2.deob.ir.flow.data.LiveVariableAnalyzer import org.objectweb.asm.Opcodes import org.objectweb.asm.tree.AbstractInsnNode import org.objectweb.asm.tree.ClassNode diff --git a/deob/src/test/java/dev/openrs2/deob/test/BytecodeTest.kt b/deob/src/test/java/dev/openrs2/deob/test/BytecodeTest.kt new file mode 100644 index 00000000..d8b1316e --- /dev/null +++ b/deob/src/test/java/dev/openrs2/deob/test/BytecodeTest.kt @@ -0,0 +1,35 @@ +package dev.openrs2.deob.test + +import org.objectweb.asm.Opcodes +import org.objectweb.asm.commons.InstructionAdapter +import org.objectweb.asm.tree.MethodNode + +@DslMarker +annotation class BytecodeDslMarker + +enum class Expectation { + Removed, + Added, + Present +} + +data class BytecodeTest(val code: MethodNode, val expectations: List) + +@BytecodeDslMarker +class BytecodeTestBuilder : InstructionAdapter(Opcodes.ASM7, MethodNode()) { + val method = mv as MethodNode + val expectations = mutableListOf() + + override fun visitInsn(opcode: Int) { + super.visitInsn(opcode) + expectations.add(Expectation.Present) + } + + operator fun Unit.unaryMinus() { + expectations[expectations.size - 1] = Expectation.Removed + } + + operator fun Unit.unaryPlus() { + expectations[expectations.size - 1] = Expectation.Added + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 39291edb..d2edb7f1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,6 +11,8 @@ include( "deob", "deob-annotations", "deob-ast", + "deob-cli", + "deob-ir", "game" )