Simplify static field scrambling

By moving all the static fields in a single class in one go, we can just
combine the <clinit> methods together. This allows us to remove all the
really complicated logic for extracting initializers, and also allows us
to move the last few array fields with complex initializers.
pull/66/head
Graham 4 years ago
parent c70d810057
commit b6bba95435
  1. 160
      deob/src/main/java/dev/openrs2/deob/transform/StaticScramblingTransformer.kt

@ -2,35 +2,33 @@ 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.sequential
import dev.openrs2.asm.transform.Transformer
import dev.openrs2.deob.remap.TypedRemapper
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 kotlin.math.max
class StaticScramblingTransformer : Transformer() {
private data class Field(val node: FieldNode, val initializer: InsnList, val version: Int, val maxStack: Int) {
val dependencies = initializer.asSequence()
.filterIsInstance<FieldInsnNode>()
.filter { it.opcode == Opcodes.GETSTATIC }
.map(::MemberRef)
.toSet()
private class FieldSet(val owner: ClassNode, val fields: List<FieldNode>, val clinit: MethodNode?) {
val dependencies = clinit?.instructions
?.filterIsInstance<FieldInsnNode>()
?.filter { it.opcode == Opcodes.GETSTATIC && it.owner != owner.name }
?.map(::MemberRef)
?.toSet() ?: emptySet()
}
private val fields = mutableMapOf<MemberRef, Field>()
private val fieldSets = mutableMapOf<MemberRef, FieldSet>()
private val fieldClasses = mutableMapOf<MemberRef, String>()
private val methodClasses = mutableMapOf<MemberRef, String>()
private var nextStaticClass: ClassNode? = null
@ -70,94 +68,61 @@ class StaticScramblingTransformer : Transformer() {
return Pair(clazz, clinit)
}
private fun MethodNode.extractEntryExitBlocks(): List<AbstractInsnNode> {
/*
* Most (or all?) of the <clinit> 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.sequential }
val last = instructions.lastOrNull()
if (last == null || last.opcode != Opcodes.RETURN) {
return entry
private fun spliceFields() {
val done = mutableSetOf<FieldSet>()
for (fieldSet in fieldSets.values) {
spliceFields(done, fieldSet)
}
val exit = instructions.toList()
.dropLast(1)
.takeLastWhile { it.sequential }
return entry.plus(exit)
}
private fun MethodNode.extractInitializers(owner: String): Pair<Map<MemberDesc, InsnList>, Set<MemberDesc>> {
val entryExitBlocks = extractEntryExitBlocks()
private fun spliceFields(done: MutableSet<FieldSet>, fieldSet: FieldSet) {
if (!done.add(fieldSet)) {
return
}
val simpleInitializers = mutableMapOf<MemberDesc, InsnList>()
val complexInitializers = instructions.asSequence()
.filter { !entryExitBlocks.contains(it) }
.filterIsInstance<FieldInsnNode>()
.filter { it.opcode == Opcodes.GETSTATIC && it.owner == owner && it.name !in TypedRemapper.EXCLUDED_FIELDS }
.map(::MemberDesc)
.toSet()
for (dependency in fieldSet.dependencies) {
val dependencyFieldSet = fieldSets[dependency] ?: continue
spliceFields(done, dependencyFieldSet)
}
val putstatics = entryExitBlocks
.filterIsInstance<FieldInsnNode>()
.filter { it.opcode == Opcodes.PUTSTATIC && it.owner == owner && it.name !in TypedRemapper.EXCLUDED_FIELDS }
val (staticClass, staticClinit) = nextClass()
staticClass.fields.addAll(fieldSet.fields)
staticClass.version = ClassVersionUtils.maxVersion(staticClass.version, fieldSet.owner.version)
for (putstatic in putstatics) {
val desc = MemberDesc(putstatic)
if (simpleInitializers.containsKey(desc) || complexInitializers.contains(desc)) {
continue
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)
}
// TODO(gpe): use a filter here (pure with no *LOADs?)
val expr = getExpression(putstatic) ?: continue
// replace any remaining RETURNs with a GOTO to the end of the method
val end = LabelNode()
insns.add(end)
val initializer = InsnList()
for (insn in expr) {
instructions.remove(insn)
initializer.add(insn)
for (insn in insns) {
if (insn.opcode == Opcodes.RETURN) {
insns.set(insn, JumpInsnNode(Opcodes.GOTO, end))
}
}
instructions.remove(putstatic)
initializer.add(putstatic)
simpleInitializers[desc] = initializer
}
return Pair(simpleInitializers, complexInitializers)
}
// append just before the end of the static <clinit> RETURN
staticClinit.instructions.insertBefore(staticClinit.instructions.last, insns)
private fun spliceInitializers() {
val done = mutableSetOf<MemberRef>()
for ((ref, field) in fields) {
spliceInitializers(done, ref, field)
}
}
private fun spliceInitializers(done: MutableSet<MemberRef>, ref: MemberRef, field: Field) {
if (!done.add(ref)) {
return
staticClinit.tryCatchBlocks.addAll(fieldSet.clinit.tryCatchBlocks)
staticClinit.maxStack = max(staticClinit.maxStack, fieldSet.clinit.maxStack)
staticClinit.maxLocals = max(staticClinit.maxLocals, fieldSet.clinit.maxLocals)
}
for (dependency in field.dependencies) {
val dependencyField = fields[dependency] ?: continue
spliceInitializers(done, dependency, dependencyField)
for (field in fieldSet.fields) {
val ref = MemberRef(fieldSet.owner, field)
fieldClasses[ref] = staticClass.name
}
val (clazz, clinit) = nextClass()
clazz.fields.add(field.node)
clazz.version = ClassVersionUtils.maxVersion(clazz.version, field.version)
clinit.instructions.insertBefore(clinit.instructions.last, field.initializer)
clinit.maxStack = max(clinit.maxStack, field.maxStack)
fieldClasses[ref] = clazz.name
}
override fun preTransform(classPath: ClassPath) {
fields.clear()
fieldSets.clear()
fieldClasses.clear()
methodClasses.clear()
nextStaticClass = null
@ -171,33 +136,22 @@ class StaticScramblingTransformer : Transformer() {
for (clazz in library) {
// TODO(gpe): exclude the JSObject class
if (clazz.name in TypedRemapper.EXCLUDED_CLASSES) {
if (clazz.name == "jagex3/jagmisc/jagmisc") {
continue
}
val clinit = clazz.methods.find { it.name == "<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 (field.name in TypedRemapper.EXCLUDED_METHODS) {
return@removeIf false
}
val desc = MemberDesc(field)
if (complexInitializers.contains(desc)) {
return@removeIf false
}
val fields = clazz.fields.filter { it.access and Opcodes.ACC_STATIC != 0 }
clazz.fields.removeAll(fields)
val initializer = simpleInitializers[desc] ?: InsnList()
val maxStack = clinit?.maxStack ?: 0
val clinit = clazz.methods.find { it.name == "<clinit>" }
if (clinit != null) {
clazz.methods.remove(clinit)
}
val fieldSet = FieldSet(clazz, fields, clinit)
for (field in fields) {
val ref = MemberRef(clazz, field)
fields[ref] = Field(field, initializer, clazz.version, maxStack)
return@removeIf true
fieldSets[ref] = fieldSet
}
clazz.methods.removeIf { method ->
@ -218,7 +172,7 @@ class StaticScramblingTransformer : Transformer() {
}
}
spliceInitializers()
spliceFields()
for (clazz in staticClasses) {
library.add(clazz)

Loading…
Cancel
Save