import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain; import java.util.ListIterator; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.InsnList; import org.objectweb.asm.tree.InsnNode; import org.objectweb.asm.tree.InvokeDynamicInsnNode; import org.objectweb.asm.tree.JumpInsnNode; import org.objectweb.asm.tree.MethodNode; import org.objectweb.asm.tree.VarInsnNode; /** * 3.8.99 载荷跑在自备 JDK8 上时,Editor.main 里有一道 * "请使用 Spine 自带的 Java 运行时" 的校验,不通过就 Runtime.halt(1)。 * * 字节码里的形状是: * iload 2 * ifne <成功分支> * goto <报错 + halt> * * 这个 agent 把那段 iload 前面塞上 iconst_1 / istore 2, * 让分支永远走成功那一条。只作用于内存中的类,不动磁盘文件。 */ public final class ForceAgent { private static final String TARGET = "com/esotericsoftware/spine/editor/Editor"; private static boolean patched; private static boolean skipLicencePatch; public static void premain(String arguments, Instrumentation instrumentation) { skipLicencePatch = arguments != null && arguments.contains("wyridonly"); System.err.println("[force-agent] premain 已加载" + (skipLicencePatch ? "(只改 WYRiDdid)" : "")); instrumentation.addTransformer(new ClassFileTransformer() { @Override public byte[] transform(ClassLoader loader, String className, Class beingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { if (!TARGET.equals(className)) return null; try { return patch(classfileBuffer); } catch (Throwable error) { System.err.println("[force-agent] 打补丁失败: " + error); return null; } } }); } private static byte[] patch(byte[] data) { ClassNode node = new ClassNode(); new ClassReader(data).accept(node, 0); for (MethodNode method : node.methods) { if (!"main".equals(method.name) || !"([Ljava/lang/String;)V".equals(method.desc)) continue; InsnList list = method.instructions; for (ListIterator it = list.iterator(); it.hasNext();) { AbstractInsnNode insn = it.next(); // 第二道门:invokedynamic WYRiDdid()Z 为 false 就直接 return。 // 直接把这次调用的结果换成常量 true —— 一对一替换,栈效果完全一样, // 所以栈帧表和 max_stack 都不用动(改成跳转就会破坏栈帧)。 if (insn instanceof InvokeDynamicInsnNode && "WYRiDdid".equals(((InvokeDynamicInsnNode) insn).name)) { list.set(insn, new InsnNode(Opcodes.ICONST_1)); System.err.println("[force-agent] 已把 WYRiDdid() 守卫改成永远通过"); } if (insn.getOpcode() != Opcodes.ILOAD || ((VarInsnNode) insn).var != 2) continue; if (skipLicencePatch) continue; AbstractInsnNode next = insn.getNext(); if (next == null || next.getOpcode() != Opcodes.IFNE) continue; AbstractInsnNode after = next.getNext(); if (after == null || after.getOpcode() != Opcodes.GOTO) continue; InsnList force = new InsnList(); force.add(new InsnNode(Opcodes.ICONST_1)); force.add(new VarInsnNode(Opcodes.ISTORE, 2)); list.insertBefore(insn, force); patched = true; System.err.println("[force-agent] 已把 Editor.main 的运行时校验改成永远通过"); } } if (!patched) return null; // 只插两条不入栈的指令,帧信息不变,用 COMPUTE_MAXS 就够了。 // 用 COMPUTE_FRAMES 的话 ASM 会在定义类的过程中反射加载同一个类, // 触发 "attempted duplicate class definition"。 ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS); node.accept(writer); return writer.toByteArray(); } }