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 index 373f00a400..c13cac9e42 100644 --- a/deob-cli/src/main/java/dev/openrs2/deob/cli/DeobfuscatorClassLoader.kt +++ b/deob-cli/src/main/java/dev/openrs2/deob/cli/DeobfuscatorClassLoader.kt @@ -33,7 +33,7 @@ object SystemClassLoader : DeobfuscatorClassLoader { class ClasspathClassLoader(val classPath: List) : DeobfuscatorClassLoader { override fun load(name: String): ClassNode { - val relativePath = Paths.get("/${name.replace('.', File.separatorChar)}.class") + val relativePath = Paths.get("${name.replace('.', File.separatorChar)}.class") for (entry in classPath) { val classFilePath = entry.resolve(relativePath) 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 index b9be35a02b..9119fd58db 100644 --- 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 @@ -5,7 +5,7 @@ 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.IrDecompiler +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") @@ -16,8 +16,8 @@ abstract class MethodScopedCommand(command: String) : CliktCommand(name = comman val clazz = PrintCfgCommand.options.classLoader.load(PrintCfgCommand.className) val method = clazz.methods.find { it.name == PrintCfgCommand.methodName }!! - val decompiler = IrDecompiler(clazz, method) - val ir = decompiler.decompile() + val decompiler = BytecodeToIrTranlator() + val ir = decompiler.decompile(clazz, method) run(ir) } 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 index dc364d9e46..0c277f7e50 100644 --- 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 @@ -1,10 +1,9 @@ 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 dev.openrs2.deob.ir.flow.BasicBlock import org.jgrapht.Graph -import org.jgrapht.graph.guava.MutableGraphAdapter import org.jgrapht.nio.DefaultAttribute import org.jgrapht.nio.GraphExporter import org.jgrapht.nio.dot.DOTExporter @@ -28,9 +27,6 @@ fun dotExporter(): BlockGraphExporter { object PrintCfgCommand : MethodScopedCommand("ir-print-cfg") { override fun run(method: Method) { - val graph: BlockGraph = MutableGraphAdapter(method.cfg) - val exporter: BlockGraphExporter = dotExporter() - - exporter.exportGraph(graph, System.out) + TODO("Make this output expressions") } } diff --git a/deob-ir/build.gradle.kts b/deob-ir/build.gradle.kts index b474070ffa..af1e5ef858 100644 --- a/deob-ir/build.gradle.kts +++ b/deob-ir/build.gradle.kts @@ -6,8 +6,6 @@ plugins { dependencies { implementation(project(":asm")) - implementation(project(":deob")) - implementation(project(":deob-annotations")) implementation("com.google.guava:guava:${Versions.guava}") } 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 0000000000..0fd44f68e6 --- /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 0000000000..9156611d21 --- /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 index 7dcee0569e..52124b9467 100644 --- a/deob-ir/src/main/java/dev/openrs2/deob/ir/Method.kt +++ b/deob-ir/src/main/java/dev/openrs2/deob/ir/Method.kt @@ -1,11 +1,11 @@ package dev.openrs2.deob.ir -import com.google.common.graph.MutableGraph -import dev.openrs2.deob.ir.flow.BasicBlock +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: MutableGraph + 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 0000000000..d9ff1ba22b --- /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/BasicBlock.kt b/deob-ir/src/main/java/dev/openrs2/deob/ir/flow/BasicBlock.kt deleted file mode 100644 index f5626beb49..0000000000 --- a/deob-ir/src/main/java/dev/openrs2/deob/ir/flow/BasicBlock.kt +++ /dev/null @@ -1,48 +0,0 @@ -package dev.openrs2.deob.ir.flow - -import com.google.common.graph.MutableGraph -import org.objectweb.asm.tree.InsnList -import org.objectweb.asm.tree.LabelNode -import org.objectweb.asm.util.Textifier -import org.objectweb.asm.util.TraceMethodVisitor - -class BasicBlock( - internal val graph: MutableGraph, - private val code: InsnList, - private var start: Int, - private var end: Int -) { - fun successors() : Set = graph.successors(this) - fun predecessors() : Set = graph.predecessors(this) - - var label: LabelNode? = null - - fun merge(other: BasicBlock) { - for (succ in other.successors()) { - graph.putEdge(this, succ) - } - - graph.removeEdge(this, other) - graph.removeNode(other) - - end = other.end - } - - override fun equals(other: Any?): Boolean { - return this === other - } - - override fun hashCode(): Int { - return System.identityHashCode(this) - } - - override fun toString(): String { - val textifier = Textifier() - val methodTracer = TraceMethodVisitor(textifier) - - code.accept(methodTracer) - - val text = textifier.text.subList(start, end + 1) as List - return text.reduce { l, r -> l + r } - } -} 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 0000000000..af5a36162b --- /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 0000000000..3b206fae6a --- /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 4885b044cf..def3c3393b 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 924d154e20..db2de21743 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 0000000000..586191106b --- /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/IrDecompiler.kt b/deob-ir/src/main/java/dev/openrs2/deob/ir/translation/IrDecompiler.kt deleted file mode 100644 index ffcd011de1..0000000000 --- a/deob-ir/src/main/java/dev/openrs2/deob/ir/translation/IrDecompiler.kt +++ /dev/null @@ -1,89 +0,0 @@ -package dev.openrs2.deob.ir.translation - -import com.google.common.graph.GraphBuilder -import dev.openrs2.deob.analysis.ControlFlowAnalyzer -import dev.openrs2.deob.ir.Method -import dev.openrs2.deob.ir.flow.BasicBlock -import org.objectweb.asm.tree.ClassNode -import org.objectweb.asm.tree.LabelNode -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 - -class IrDecompiler(private val owner: ClassNode, private val method: MethodNode) : - Analyzer(BasicInterpreter()) { - - private val graph = GraphBuilder - .directed() - .allowsSelfLoops(true) - .build() - - private val blocks = mutableMapOf() - private fun bb(index: Int) = blocks.computeIfAbsent(index) { - val bb = BasicBlock(graph, method.instructions, it, it) - val leader = method.instructions[index] - - if (leader is LabelNode) { - bb.label = leader - } - - bb - } - - override fun newControlFlowEdge(insnIndex: Int, successorIndex: Int) { - graph.putEdge(bb(insnIndex), bb(successorIndex)) - } - - override fun newControlFlowExceptionEdge(insnIndex: Int, successorIndex: Int): Boolean { - // @TODO: Attach basic blocks with exception types. - return true - } - - fun decompile(): Method { - analyze(owner.name, method) - - val entryBlock = blocks[0] ?: throw IllegalStateException("No method entry block found") - - val remainingBlocks = Stack() - remainingBlocks.push(entryBlock) - - val visited = mutableSetOf() - - while (remainingBlocks.isNotEmpty()) { - val bb = remainingBlocks.pop() - var next = bb.findTrivialSuccessor() - - while (next != null && next != bb) { - bb.merge(next) - - next = bb.findTrivialSuccessor() - } - - for (succ in bb.successors()) { - if (!visited.contains(succ)) { - remainingBlocks.push(succ) - } - } - - visited.add(bb) - } - - return Method(owner, method, entryBlock) - } -} - -private fun BasicBlock.findTrivialSuccessor(): BasicBlock? { - val successors = successors() - if (successors.size != 1) return null - - val successor = successors.first()!! - val nextPredecessors = successor.predecessors() - - if (nextPredecessors.size == 1) { - return successor - } else { - return null - } -} 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 0000000000..98265d81fd --- /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 0000000000..4a1d21939a --- /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 0000000000..e24b6cbc54 --- /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/IrDecompilerTests.kt b/deob-ir/src/test/java/dev/openrs2/deob/ir/translation/BytecodeToIrTranlatorTests.kt similarity index 62% rename from deob-ir/src/test/java/dev/openrs2/deob/ir/translation/IrDecompilerTests.kt rename to deob-ir/src/test/java/dev/openrs2/deob/ir/translation/BytecodeToIrTranlatorTests.kt index 5192f42f47..ad261796f4 100644 --- a/deob-ir/src/test/java/dev/openrs2/deob/ir/translation/IrDecompilerTests.kt +++ b/deob-ir/src/test/java/dev/openrs2/deob/ir/translation/BytecodeToIrTranlatorTests.kt @@ -4,12 +4,14 @@ import dev.openrs2.deob.ir.translation.fixture.Fixture import dev.openrs2.deob.ir.translation.fixture.FixtureMethod import org.junit.jupiter.api.Test -class IrDecompilerTests { +class BytecodeToIrTranlatorTests { @Test fun `Creates entry basic block`() { - class CfgSample(val a: Boolean, val b: Boolean) : - Fixture { + class CfgSample() : Fixture { + val a = true + val b = false + override fun test() { if (a) { println("a") @@ -20,8 +22,10 @@ class IrDecompilerTests { } val fixture = FixtureMethod.from(CfgSample::class) - val decompiler = IrDecompiler(fixture.owner, fixture.method) - val irMethod = decompiler.decompile() + 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 index 9182a1b27a..3dc411fcdc 100644 --- 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 @@ -1,9 +1,9 @@ package dev.openrs2.deob.ir.translation.fixture; -import dev.openrs2.deob.ir.translation.IrDecompilerTests; +import dev.openrs2.deob.ir.translation.BytecodeToIrTranlatorTests; /** - * Marker interface for {@link IrDecompilerTests}. + * Marker interface for {@link BytecodeToIrTranlatorTests}. */ public interface Fixture { void test(); diff --git a/deob/build.gradle.kts b/deob/build.gradle.kts index 4e61440328..a1067ae7c5 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 d75b8ef708..0000000000 --- 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 327e221abd..f3bd848ed3 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 0000000000..d8b1316ebb --- /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 + } +}