Decode bytecode into IR expressions/statements

Adds an implementation of an interpreter that converts JVM bytecode to
expression trees and statements as it goes, handling stack manipiulation
instructions on the way.

Signed-off-by: Gary Tierney <gary.tierney@fastmail.com>
feat/deob-ir
Gary Tierney 4 years ago
parent 1d64a932bb
commit f4d0d26a87
  1. 2
      deob-cli/src/main/java/dev/openrs2/deob/cli/DeobfuscatorClassLoader.kt
  2. 6
      deob-cli/src/main/java/dev/openrs2/deob/cli/ir/MethodScopedCommand.kt
  3. 8
      deob-cli/src/main/java/dev/openrs2/deob/cli/ir/PrintCfgCommand.kt
  4. 2
      deob-ir/build.gradle.kts
  5. 27
      deob-ir/src/main/java/dev/openrs2/deob/ir/BasicBlock.kt
  6. 52
      deob-ir/src/main/java/dev/openrs2/deob/ir/Expr.kt
  7. 6
      deob-ir/src/main/java/dev/openrs2/deob/ir/Method.kt
  8. 22
      deob-ir/src/main/java/dev/openrs2/deob/ir/Stmt.kt
  9. 48
      deob-ir/src/main/java/dev/openrs2/deob/ir/flow/BasicBlock.kt
  10. 80
      deob-ir/src/main/java/dev/openrs2/deob/ir/flow/ControlFlowAnalyzer.kt
  11. 8
      deob-ir/src/main/java/dev/openrs2/deob/ir/flow/ControlFlowTransfer.kt
  12. 3
      deob-ir/src/main/java/dev/openrs2/deob/ir/flow/data/DataFlowAnalyzer.kt
  13. 2
      deob-ir/src/main/java/dev/openrs2/deob/ir/flow/data/LiveVariableAnalyzer.kt
  14. 16
      deob-ir/src/main/java/dev/openrs2/deob/ir/translation/BytecodeToIrTranlator.kt
  15. 89
      deob-ir/src/main/java/dev/openrs2/deob/ir/translation/IrDecompiler.kt
  16. 106
      deob-ir/src/main/java/dev/openrs2/deob/ir/translation/decompiler/IrAnalyzer.kt
  17. 334
      deob-ir/src/main/java/dev/openrs2/deob/ir/translation/decompiler/IrInterpreter.kt
  18. 9
      deob-ir/src/main/java/dev/openrs2/deob/ir/translation/decompiler/IrValue.kt
  19. 14
      deob-ir/src/test/java/dev/openrs2/deob/ir/translation/BytecodeToIrTranlatorTests.kt
  20. 4
      deob-ir/src/test/java/dev/openrs2/deob/ir/translation/fixture/Fixture.java
  21. 1
      deob/build.gradle.kts
  22. 29
      deob/src/main/java/dev/openrs2/deob/analysis/ControlFlowAnalyzer.kt
  23. 2
      deob/src/main/java/dev/openrs2/deob/transform/DummyLocalTransformer.kt
  24. 35
      deob/src/test/java/dev/openrs2/deob/test/BytecodeTest.kt

@ -33,7 +33,7 @@ object SystemClassLoader : DeobfuscatorClassLoader {
class ClasspathClassLoader(val classPath: List<Path>) : 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)

@ -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)
}

@ -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")
}
}

@ -6,8 +6,6 @@ plugins {
dependencies {
implementation(project(":asm"))
implementation(project(":deob"))
implementation(project(":deob-annotations"))
implementation("com.google.guava:guava:${Versions.guava}")
}

@ -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<BasicBlock, ControlFlowTransfer>
class BasicBlock(
internal val graph: BasicBlockGraph
) {
val statements = mutableListOf<Stmt>()
fun successors(): Set<BasicBlock> = graph.successors(this)
fun predecessors(): Set<BasicBlock> = 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)"
}
}

@ -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>) : Expr()
data class VarExpr(val storage: Storage) : Expr() {
companion object {
fun local(slot: Int) = VarExpr(LocalVarStorage(slot))
}
}

@ -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<BasicBlock>
val cfg: MutableValueGraph<BasicBlock, ControlFlowTransfer>
get() = entry.graph
}

@ -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?)
}

@ -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<BasicBlock>,
private val code: InsnList,
private var start: Int,
private var end: Int
) {
fun successors() : Set<BasicBlock> = graph.successors(this)
fun predecessors() : Set<BasicBlock> = 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<String>
return text.reduce { l, r -> l + r }
}
}

@ -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<BasicValue>(BasicInterpreter()) {
val instructionsToBlocks = mutableMapOf<Int, Int>()
private val graph = GraphBuilder
.directed()
.allowsSelfLoops(true)
.build<Int>()
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<Int>()
val nodesVisited = mutableSetOf<Int>()
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<Int> {
analyze(owner, method)
if (reduce) {
reduceGraph()
}
return graph
}
}

@ -0,0 +1,8 @@
package dev.openrs2.deob.ir.flow
sealed class ControlFlowTransfer {
data class ConditionalJump(val successful: Boolean) : ControlFlowTransfer()
object Goto : ControlFlowTransfer()
}

@ -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

@ -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

@ -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)
}
}

@ -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<BasicValue>(BasicInterpreter()) {
private val graph = GraphBuilder
.directed()
.allowsSelfLoops(true)
.build<BasicBlock>()
private val blocks = mutableMapOf<Int, BasicBlock>()
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<BasicBlock>()
remainingBlocks.push(entryBlock)
val visited = mutableSetOf<BasicBlock>()
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
}
}

@ -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<IrValue>(interpreter) {
init {
interpreter.visitor = this
}
/**
* A mapping of program counters (instruction offsets) to the [BasicBlock]s
* that define them.
*/
private lateinit var blocks: MutableMap<Int, BasicBlock>
/**
* 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))
}
}

@ -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<IrValue>(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<out IrValue>): 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:
*
* <p>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:
*
* <p>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<out IrValue>): 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<out IrValue>): 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
}
}

@ -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
}

@ -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?
}
}

@ -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();

@ -11,6 +11,7 @@ application {
dependencies {
implementation(project(":bundler"))
implementation(project(":deob-annotations"))
implementation(project(":deob-ir"))
implementation("com.google.guava:guava:${Versions.guava}")
}

@ -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<BasicValue>(BasicInterpreter()) {
private val graph = GraphBuilder
.directed()
.allowsSelfLoops(true)
.immutable<Int>()
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<Int> {
analyze(owner, method)
return graph.build()
}
}

@ -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

@ -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<Expectation>)
@BytecodeDslMarker
class BytecodeTestBuilder : InstructionAdapter(Opcodes.ASM7, MethodNode()) {
val method = mv as MethodNode
val expectations = mutableListOf<Expectation>()
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
}
}
Loading…
Cancel
Save