forked from openrs2/openrs2
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
parent
1d64a932bb
commit
f4d0d26a87
@ -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 |
||||
} |
@ -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() |
||||
} |
||||
} |
@ -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…
Reference in new issue