forked from openrs2/openrs2
Adds support for decompiling bytecode into basic blocks based on the control flow graph of the code. Also adds a new deob-ir create, intended as an easier to work with intermediate representation of program code. Signed-off-by: Gary Tierney <gary.tierney@fastmail.com>feat/deob-ir
parent
caed5cebce
commit
bbcdba3b34
@ -0,0 +1,28 @@ |
||||
plugins { |
||||
`maven-publish` |
||||
application |
||||
kotlin("jvm") |
||||
} |
||||
|
||||
dependencies { |
||||
implementation(project(":asm")) |
||||
implementation(project(":deob")) |
||||
implementation(project(":deob-annotations")) |
||||
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,11 @@ |
||||
package dev.openrs2.deob.ir |
||||
|
||||
import com.google.common.graph.MutableGraph |
||||
import dev.openrs2.deob.ir.flow.BasicBlock |
||||
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> |
||||
get() = entry.graph |
||||
} |
@ -0,0 +1,48 @@ |
||||
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,89 @@ |
||||
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,27 @@ |
||||
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 IrDecompilerTests { |
||||
|
||||
@Test |
||||
fun `Creates entry basic block`() { |
||||
class CfgSample(val a: Boolean, val b: Boolean) : |
||||
Fixture { |
||||
override fun test() { |
||||
if (a) { |
||||
println("a") |
||||
} else if (b) { |
||||
println("b") |
||||
} |
||||
} |
||||
} |
||||
|
||||
val fixture = FixtureMethod.from(CfgSample::class) |
||||
val decompiler = IrDecompiler(fixture.owner, fixture.method) |
||||
val irMethod = decompiler.decompile() |
||||
} |
||||
} |
||||
|
@ -0,0 +1,10 @@ |
||||
package dev.openrs2.deob.ir.translation.fixture; |
||||
|
||||
import dev.openrs2.deob.ir.translation.IrDecompilerTests; |
||||
|
||||
/** |
||||
* Marker interface for {@link IrDecompilerTests}. |
||||
*/ |
||||
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) |
||||
} |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue