WIP: Convert JVM bytecode to an intermediate represntation #93

Closed
gary.tierney wants to merge 3 commits from gary.tierney/openrs2:feat/deob-ir into master
  1. 2
      buildSrc/src/main/java/Versions.kt
  2. 36
      deob-cli/build.gradle.kts
  3. 49
      deob-cli/src/main/java/dev/openrs2/deob/cli/DeobfuscatorClassLoader.kt
  4. 36
      deob-cli/src/main/java/dev/openrs2/deob/cli/DeobfuscatorCli.kt
  5. 3
      deob-cli/src/main/java/dev/openrs2/deob/cli/DeobfuscatorOptions.kt
  6. 26
      deob-cli/src/main/java/dev/openrs2/deob/cli/ir/MethodScopedCommand.kt
  7. 32
      deob-cli/src/main/java/dev/openrs2/deob/cli/ir/PrintCfgCommand.kt
  8. 26
      deob-ir/build.gradle.kts
  9. 27
      deob-ir/src/main/java/dev/openrs2/deob/ir/BasicBlock.kt
  10. 52
      deob-ir/src/main/java/dev/openrs2/deob/ir/Expr.kt
  11. 11
      deob-ir/src/main/java/dev/openrs2/deob/ir/Method.kt
  12. 22
      deob-ir/src/main/java/dev/openrs2/deob/ir/Stmt.kt
  13. 80
      deob-ir/src/main/java/dev/openrs2/deob/ir/flow/ControlFlowAnalyzer.kt
  14. 8
      deob-ir/src/main/java/dev/openrs2/deob/ir/flow/ControlFlowTransfer.kt
  15. 3
      deob-ir/src/main/java/dev/openrs2/deob/ir/flow/data/DataFlowAnalyzer.kt
  16. 2
      deob-ir/src/main/java/dev/openrs2/deob/ir/flow/data/LiveVariableAnalyzer.kt
  17. 16
      deob-ir/src/main/java/dev/openrs2/deob/ir/translation/BytecodeToIrTranlator.kt
  18. 106
      deob-ir/src/main/java/dev/openrs2/deob/ir/translation/decompiler/IrAnalyzer.kt
  19. 334
      deob-ir/src/main/java/dev/openrs2/deob/ir/translation/decompiler/IrInterpreter.kt
  20. 9
      deob-ir/src/main/java/dev/openrs2/deob/ir/translation/decompiler/IrValue.kt
  21. 31
      deob-ir/src/test/java/dev/openrs2/deob/ir/translation/BytecodeToIrTranlatorTests.kt
  22. 10
      deob-ir/src/test/java/dev/openrs2/deob/ir/translation/fixture/Fixture.java
  23. 28
      deob-ir/src/test/java/dev/openrs2/deob/ir/translation/fixture/FixtureMethod.kt
  24. 1
      deob/build.gradle.kts
  25. 29
      deob/src/main/java/dev/openrs2/deob/analysis/ControlFlowAnalyzer.kt
  26. 2
      deob/src/main/java/dev/openrs2/deob/transform/DummyLocalTransformer.kt
  27. 35
      deob/src/test/java/dev/openrs2/deob/test/BytecodeTest.kt
  28. 2
      settings.gradle.kts

@ -1,6 +1,7 @@
object Versions {
const val asm = "7.3.1"
const val bouncyCastle = "1.64"
const val clikt = "2.5.0"
const val dependencyLicenseReport = "1.13"
const val fernflower = "1.0.4-SNAPSHOT"
const val guava = "28.2-jre"
@ -8,6 +9,7 @@ object Versions {
const val inlineLogger = "1.0.2"
const val javaParser = "3.15.14"
const val jdom = "2.0.6"
const val jgrapht = "1.4.0"
const val jimfs = "1.1"
const val junit = "5.6.0"
const val kotlin = "1.3.70"

@ -0,0 +1,36 @@
plugins {
`maven-publish`
application
kotlin("jvm")
}
application {
mainClassName = "dev.openrs2.deob.cli.DeobfuscatorCliKt"
}
dependencies {
implementation(project(":asm"))
implementation(project(":deob"))
implementation(project(":deob-ir"))
implementation("com.github.ajalt:clikt:${Versions.clikt}")
implementation("com.google.guava:guava:${Versions.guava}")
implementation("org.jgrapht:jgrapht-io:${Versions.jgrapht}")
implementation("org.jgrapht:jgrapht-guava:${Versions.jgrapht}")
}
publishing {
publications.create<MavenPublication>("maven") {
from(components["java"])
pom {
packaging = "jar"
name.set("OpenRS2 Deobfuscator CLI")
description.set(
"""
A command-line interface that provides access to the deobfuscators
analyis, deobfuscation, and decompilation tools.
""".trimIndent()
)
}
}
}

@ -0,0 +1,49 @@
package dev.openrs2.deob.cli
import org.objectweb.asm.ClassReader
import org.objectweb.asm.tree.ClassNode
import java.io.File
import java.io.InputStream
import java.lang.IllegalArgumentException
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
private fun load(input: InputStream) = input.use {
val node = ClassNode()
val reader = ClassReader(input)
reader.accept(node, ClassReader.SKIP_DEBUG)
node
}
interface DeobfuscatorClassLoader {
fun load(name: String): ClassNode
}
object SystemClassLoader : DeobfuscatorClassLoader {
override fun load(name: String): ClassNode {
val classPath = "/${name.replace('.', File.separatorChar)}.class"
val classFile = this.javaClass.getResourceAsStream(classPath)
return load(classFile)
}
}
class ClasspathClassLoader(val classPath: List<Path>) : DeobfuscatorClassLoader {
override fun load(name: String): ClassNode {
val relativePath = Paths.get("${name.replace('.', File.separatorChar)}.class")
for (entry in classPath) {
val classFilePath = entry.resolve(relativePath)
if (!Files.exists(classFilePath)) {
continue
}
return load(Files.newInputStream(classFilePath))
}
throw IllegalArgumentException("Unable to find class named $name")
}
}

@ -0,0 +1,36 @@
package dev.openrs2.deob.cli
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.subcommands
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.option
import dev.openrs2.deob.cli.ir.PrintCfgCommand
import java.io.File
import java.nio.file.Path
import java.nio.file.Paths
class DeobfuscatorCli : CliktCommand(name = "deob") {
val classpath: String by option(help = "Change the classpath used to resolve class files")
.default("system")
override fun run() {
val loader = when (classpath) {
"system" -> SystemClassLoader
else -> {
val paths : List<Path> = classpath.split(File.pathSeparatorChar).map {
Paths.get(it)
}
val classLoader = ClasspathClassLoader(paths)
classLoader
}
}
currentContext.obj = DeobfuscatorOptions(loader)
}
}
fun main(args: Array<String>) = DeobfuscatorCli()
.subcommands(PrintCfgCommand)
.main(args)

@ -0,0 +1,3 @@
package dev.openrs2.deob.cli
data class DeobfuscatorOptions(val classLoader: DeobfuscatorClassLoader)

@ -0,0 +1,26 @@
package dev.openrs2.deob.cli.ir
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.requireObject
import com.github.ajalt.clikt.parameters.arguments.argument
import dev.openrs2.deob.cli.DeobfuscatorOptions
import dev.openrs2.deob.ir.Method
import dev.openrs2.deob.ir.translation.BytecodeToIrTranlator
abstract class MethodScopedCommand(command: String) : CliktCommand(name = command) {
val className: String by argument(help = "Fully qualified name of the class name to disassemble")
val methodName: String by argument(help = "Name of the method to print the control flow graph for")
val options by requireObject<DeobfuscatorOptions>()
final override fun run() {
val clazz = PrintCfgCommand.options.classLoader.load(PrintCfgCommand.className)
val method = clazz.methods.find { it.name == PrintCfgCommand.methodName }!!
val decompiler = BytecodeToIrTranlator()
val ir = decompiler.decompile(clazz, method)
run(ir)
}
abstract fun run(method: Method)
}

@ -0,0 +1,32 @@
package dev.openrs2.deob.cli.ir
import com.google.common.graph.EndpointPair
import dev.openrs2.deob.ir.BasicBlock
import dev.openrs2.deob.ir.Method
import org.jgrapht.Graph
import org.jgrapht.nio.DefaultAttribute
import org.jgrapht.nio.GraphExporter
import org.jgrapht.nio.dot.DOTExporter
typealias BlockGraph = Graph<BasicBlock, EndpointPair<BasicBlock>>
typealias BlockGraphExporter = GraphExporter<BasicBlock, EndpointPair<BasicBlock>>
fun dotExporter(): BlockGraphExporter {
val exporter = DOTExporter<BasicBlock, EndpointPair<BasicBlock>>()
exporter.setVertexAttributeProvider {
val label = it.toString().replace("\n", "\\l")
mapOf(
"label" to DefaultAttribute.createAttribute(label)
)
}
return exporter
}
object PrintCfgCommand : MethodScopedCommand("ir-print-cfg") {
override fun run(method: Method) {
TODO("Make this output expressions")
}
}

@ -0,0 +1,26 @@
plugins {
`maven-publish`
application
kotlin("jvm")
}
dependencies {
implementation(project(":asm"))
implementation("com.google.guava:guava:${Versions.guava}")
}
publishing {
publications.create<MavenPublication>("maven") {
from(components["java"])
pom {
packaging = "jar"
name.set("OpenRS2 Deobfuscator IR")
description.set(
"""
An intermediate reprsentation of bytecode.
""".trimIndent()
)
}
}
}

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

@ -0,0 +1,11 @@
package dev.openrs2.deob.ir
import com.google.common.graph.MutableValueGraph
import dev.openrs2.deob.ir.flow.ControlFlowTransfer
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodNode
class Method(val owner: ClassNode, val method: MethodNode, val entry: BasicBlock) {
val cfg: MutableValueGraph<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?)
}

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

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

@ -0,0 +1,31 @@
package dev.openrs2.deob.ir.translation
import dev.openrs2.deob.ir.translation.fixture.Fixture
import dev.openrs2.deob.ir.translation.fixture.FixtureMethod
import org.junit.jupiter.api.Test
class BytecodeToIrTranlatorTests {
@Test
fun `Creates entry basic block`() {
class CfgSample() : Fixture {
val a = true
val b = false
override fun test() {
if (a) {
println("a")
} else if (b) {
println("b")
}
}
}
val fixture = FixtureMethod.from(CfgSample::class)
val decompiler = BytecodeToIrTranlator()
val irMethod = decompiler.decompile(fixture.owner, fixture.method)
// @TODO - some way of asserting on the output cfg?
}
}

@ -0,0 +1,10 @@
package dev.openrs2.deob.ir.translation.fixture;
import dev.openrs2.deob.ir.translation.BytecodeToIrTranlatorTests;
/**
* Marker interface for {@link BytecodeToIrTranlatorTests}.
*/
public interface Fixture {
void test();
}

@ -0,0 +1,28 @@
package dev.openrs2.deob.ir.translation.fixture
import org.objectweb.asm.ClassReader
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodNode
import java.io.File
import kotlin.reflect.KClass
class FixtureMethod private constructor(val owner: ClassNode, val method: MethodNode) {
companion object {
fun from(ty: KClass<out Fixture>): FixtureMethod {
val classPath = "/${ty.java.name.replace('.', File.separatorChar)}.class"
val classFile = ty.java.getResourceAsStream(classPath)
return classFile.use {
val node = ClassNode()
val reader = ClassReader(classFile)
reader.accept(node, ClassReader.SKIP_DEBUG)
val method = node.methods.find { it.name == "test" } ?:
throw IllegalStateException("Fixture class has no test() method")
return FixtureMethod(node, method)
}
}
}
}

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

@ -11,6 +11,8 @@ include(
"deob",
"deob-annotations",
"deob-ast",
"deob-cli",
"deob-ir",
"game"
)

Loading…
Cancel
Save