diff --git a/deob/src/main/java/dev/openrs2/deob/transform/StaticScramblingTransformer.kt b/deob/src/main/java/dev/openrs2/deob/transform/StaticScramblingTransformer.kt index f819e5a8..73f5cae0 100644 --- a/deob/src/main/java/dev/openrs2/deob/transform/StaticScramblingTransformer.kt +++ b/deob/src/main/java/dev/openrs2/deob/transform/StaticScramblingTransformer.kt @@ -2,20 +2,22 @@ package dev.openrs2.deob.transform import com.github.michaelbull.logging.InlineLogger import dev.openrs2.asm.ClassVersionUtils +import dev.openrs2.asm.MemberDesc import dev.openrs2.asm.MemberRef import dev.openrs2.asm.classpath.ClassPath import dev.openrs2.asm.classpath.Library +import dev.openrs2.asm.getExpression +import dev.openrs2.asm.isSequential import dev.openrs2.asm.transform.Transformer import dev.openrs2.deob.Profile import dev.openrs2.util.collect.DisjointSet import org.objectweb.asm.Opcodes +import org.objectweb.asm.tree.AbstractInsnNode import org.objectweb.asm.tree.ClassNode import org.objectweb.asm.tree.FieldInsnNode import org.objectweb.asm.tree.FieldNode import org.objectweb.asm.tree.InsnList import org.objectweb.asm.tree.InsnNode -import org.objectweb.asm.tree.JumpInsnNode -import org.objectweb.asm.tree.LabelNode import org.objectweb.asm.tree.MethodInsnNode import org.objectweb.asm.tree.MethodNode import javax.inject.Inject @@ -24,16 +26,17 @@ import kotlin.math.max @Singleton class StaticScramblingTransformer @Inject constructor(private val profile: Profile) : Transformer() { - private class FieldSet(val owner: ClassNode, val fields: List, val clinit: MethodNode?) { - val dependencies = clinit?.instructions - ?.filterIsInstance() - ?.filter { it.opcode == Opcodes.GETSTATIC && it.owner != owner.name } - ?.mapTo(mutableSetOf(), ::MemberRef) ?: emptySet() + private data class Field(val node: FieldNode, val initializer: InsnList, val version: Int, val maxStack: Int) { + val dependencies = initializer.asSequence() + .filterIsInstance() + .filter { it.opcode == Opcodes.GETSTATIC } + .map(::MemberRef) + .toSet() } private lateinit var inheritedFieldSets: DisjointSet private lateinit var inheritedMethodSets: DisjointSet - private val fieldSets = mutableMapOf() + private val fields = mutableMapOf, Field>() private val fieldClasses = mutableMapOf, String>() private val methodClasses = mutableMapOf, String>() private var nextStaticClass: ClassNode? = null @@ -73,63 +76,103 @@ class StaticScramblingTransformer @Inject constructor(private val profile: Profi return Pair(clazz, clinit) } - private fun spliceFields() { - val done = mutableSetOf
() - for (fieldSet in fieldSets.values) { - spliceFields(done, fieldSet) - } - } - - private fun spliceFields(done: MutableSet
, fieldSet: FieldSet) { - if (!done.add(fieldSet)) { - return + private fun MethodNode.extractEntryExitBlocks(): List { + /* + * Most (or all?) of the methods have "simple" initializers + * that we're capable of moving in the first and last basic blocks of + * the method. The last basic block is always at the end of the code + * and ends in a RETURN. This allows us to avoid worrying about making + * a full basic block control flow graph here. + */ + val entry = instructions.takeWhile { it.isSequential } + + val last = instructions.lastOrNull() + if (last == null || last.opcode != Opcodes.RETURN) { + return entry } - for (dependency in fieldSet.dependencies) { - val dependencyFieldSet = fieldSets[dependency] ?: continue - spliceFields(done, dependencyFieldSet) - } + val exit = instructions.toList() + .dropLast(1) + .takeLastWhile { it.isSequential } - val (staticClass, staticClinit) = nextClass() - staticClass.fields.addAll(fieldSet.fields) - staticClass.version = ClassVersionUtils.max(staticClass.version, fieldSet.owner.version) + return entry.plus(exit) + } - if (fieldSet.clinit != null) { - // remove tail RETURN - val insns = fieldSet.clinit.instructions - val last = insns.lastOrNull() - if (last != null && last.opcode == Opcodes.RETURN) { - insns.remove(last) + private fun MethodNode.extractInitializers(owner: String): Pair, Set> { + val entryExitBlocks = extractEntryExitBlocks() + + val simpleInitializers = mutableMapOf() + val complexInitializers = instructions.asSequence() + .filter { !entryExitBlocks.contains(it) } + .filterIsInstance() + .filter { it.opcode == Opcodes.GETSTATIC && it.owner == owner } + .filter { !profile.excludedFields.matches(it.owner, it.name, it.desc) } + .map(::MemberDesc) + .toSet() + + val putstatics = entryExitBlocks + .filterIsInstance() + .filter { it.opcode == Opcodes.PUTSTATIC && it.owner == owner } + .filter { !profile.excludedFields.matches(it.owner, it.name, it.desc) } + + for (putstatic in putstatics) { + val desc = MemberDesc(putstatic) + if (simpleInitializers.containsKey(desc) || complexInitializers.contains(desc)) { + continue } - // replace any remaining RETURNs with a GOTO to the end of the method - val end = LabelNode() - insns.add(end) + // TODO(gpe): use a filter here (pure with no *LOADs?) + val expr = getExpression(putstatic) ?: continue - for (insn in insns) { - if (insn.opcode == Opcodes.RETURN) { - insns.set(insn, JumpInsnNode(Opcodes.GOTO, end)) - } + val initializer = InsnList() + for (insn in expr) { + instructions.remove(insn) + initializer.add(insn) } + instructions.remove(putstatic) + initializer.add(putstatic) - // append just before the end of the static RETURN - staticClinit.instructions.insertBefore(staticClinit.instructions.last, insns) + simpleInitializers[desc] = initializer + } + + return Pair(simpleInitializers, complexInitializers) + } - staticClinit.tryCatchBlocks.addAll(fieldSet.clinit.tryCatchBlocks) - staticClinit.maxStack = max(staticClinit.maxStack, fieldSet.clinit.maxStack) - staticClinit.maxLocals = max(staticClinit.maxLocals, fieldSet.clinit.maxLocals) + private fun spliceInitializers() { + val done = mutableSetOf>() + for ((partition, field) in fields) { + spliceInitializers(done, partition, field) + } + } + + private fun spliceInitializers( + done: MutableSet>, + partition: DisjointSet.Partition, + field: Field + ) { + if (!done.add(partition)) { + return } - for (field in fieldSet.fields) { - val partition = inheritedFieldSets[MemberRef(fieldSet.owner, field)]!! - fieldClasses[partition] = staticClass.name + for (dependency in field.dependencies) { + val dependencyPartition = inheritedFieldSets[dependency]!! + val dependencyField = fields[dependencyPartition] ?: continue + spliceInitializers(done, partition, dependencyField) } + + val (staticClass, clinit) = nextClass() + staticClass.fields.add(field.node) + staticClass.version = ClassVersionUtils.max(staticClass.version, field.version) + clinit.instructions.insertBefore(clinit.instructions.last, field.initializer) + clinit.maxStack = max(clinit.maxStack, field.maxStack) + + fieldClasses[partition] = staticClass.name } override fun preTransform(classPath: ClassPath) { inheritedFieldSets = classPath.createInheritedFieldSets() inheritedMethodSets = classPath.createInheritedMethodSets() - fieldSets.clear() + fields.clear() fieldClasses.clear() methodClasses.clear() nextStaticClass = null @@ -143,22 +186,29 @@ class StaticScramblingTransformer @Inject constructor(private val profile: Profi for (clazz in library) { // TODO(gpe): exclude the JSObject class - if (clazz.name == "jagex3/jagmisc/jagmisc") { - continue - } - - val fields = clazz.fields.filter { it.access and Opcodes.ACC_STATIC != 0 } - clazz.fields.removeAll(fields) val clinit = clazz.methods.find { it.name == "" } - if (clinit != null) { - clazz.methods.remove(clinit) - } + val (simpleInitializers, complexInitializers) = clinit?.extractInitializers(clazz.name) + ?: Pair(emptyMap(), emptySet()) + + clazz.fields.removeIf { field -> + if (field.access and Opcodes.ACC_STATIC == 0) { + return@removeIf false + } else if (profile.excludedFields.matches(clazz.name, field.name, field.desc)) { + return@removeIf false + } - val fieldSet = FieldSet(clazz, fields, clinit) - for (field in fields) { - val ref = MemberRef(clazz, field) - fieldSets[ref] = fieldSet + val desc = MemberDesc(field) + if (complexInitializers.contains(desc)) { + return@removeIf false + } + + val initializer = simpleInitializers[desc] ?: InsnList() + val maxStack = clinit?.maxStack ?: 0 + + val partition = inheritedFieldSets[MemberRef(clazz, field)]!! + fields[partition] = Field(field, initializer, clazz.version, maxStack) + return@removeIf true } clazz.methods.removeIf { method -> @@ -180,7 +230,7 @@ class StaticScramblingTransformer @Inject constructor(private val profile: Profi } } - spliceFields() + spliceInitializers() for (clazz in staticClasses) { library.add(clazz)