import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain; 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.FieldInsnNode; import org.objectweb.asm.tree.FieldNode; import org.objectweb.asm.tree.InsnList; import org.objectweb.asm.tree.InsnNode; import org.objectweb.asm.tree.IntInsnNode; import org.objectweb.asm.tree.LdcInsnNode; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; import org.objectweb.asm.tree.TypeInsnNode; import org.objectweb.asm.tree.VarInsnNode; /** * 在**内存里**给编辑器加一个正式视图("Spine View"),磁盘上的载荷一个字节都不改。 * * 为什么必须走内存:编辑器启动时会自检——把它自己 jar 里所有类的 SHA1 和签名里的清单比对, * 一旦有出入就报 "Spine's integrity check failed."。所以任何磁盘改动都不行。 * * 两处插桩(与 tools/patch_view/ViewPatcher 完全同源): * us. :造一个新枚举常量 spineView,并在 $VALUES 组装完之后追加进数组 * (必须在分组列表生成之前,否则 Views 菜单里看不到它) * tD. :最后一个 `putfield us.D:Ldl;`(即给视图塞面板 Actor)之后, * 插一条 com.spineview.ViewInstaller.install() */ public final class ViewAgent { private static final String VIEW_ENUM = "us"; private static final String UI_CLASS = "tD"; private static final String VIEW_NAME = "spineView"; private static final String VIEW_TITLE = "Spine View"; private static final String VIEW_FIELD = "SPINEVIEW"; private static boolean enumDone; private static boolean uiDone; public static void premain(String arguments, Instrumentation instrumentation) { System.err.println("[view-agent] premain 已加载"); instrumentation.addTransformer(new ClassFileTransformer() { @Override public byte[] transform(ClassLoader loader, String className, Class beingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { try { if (VIEW_ENUM.equals(className) && !enumDone && !"0".equals(System.getProperty("viewagent.enum"))) { enumDone = true; return patchEnum(classfileBuffer); } // 默认不再动 tD:这个混淆过的构造函数对插入指令极其敏感, // 插一条 NOP 没事,插任何带操作数的指令都会把它的初始化流程弄坏。 // 视图面板改由 ViewInstaller 在编辑器起来之后从外部填进 us.spineView.D。 if (UI_CLASS.equals(className) && !uiDone && "1".equals(System.getProperty("viewagent.ui"))) { uiDone = true; return patchUi(classfileBuffer); } } catch (Throwable error) { System.err.println("[view-agent] 插桩 " + className + " 失败: " + error); } return null; } }); } private static byte[] patchEnum(byte[] data) { ClassNode node = new ClassNode(); new ClassReader(data).accept(node, 0); boolean hasField = false; for (FieldNode field : node.fields) { if (VIEW_FIELD.equals(field.name)) hasField = true; } if (!hasField) { node.fields.add(new FieldNode(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, VIEW_FIELD, "L" + VIEW_ENUM + ";", null, null)); } for (MethodNode method : node.methods) { if (!"".equals(method.name)) continue; InsnList head = new InsnList(); head.add(new TypeInsnNode(Opcodes.NEW, VIEW_ENUM)); head.add(new InsnNode(Opcodes.DUP)); head.add(new LdcInsnNode(VIEW_NAME)); head.add(new IntInsnNode(Opcodes.BIPUSH, 16)); head.add(new LdcInsnNode(VIEW_TITLE)); head.add(new FieldInsnNode(Opcodes.GETSTATIC, "MV", "L", "I")); head.add(new FieldInsnNode(Opcodes.GETSTATIC, "ut", "B", "Lut;")); head.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, VIEW_ENUM, "", "(Ljava/lang/String;ILjava/lang/String;ILut;)V", false)); head.add(new FieldInsnNode(Opcodes.PUTSTATIC, VIEW_ENUM, VIEW_FIELD, "L" + VIEW_ENUM + ";")); method.instructions.insert(head); AbstractInsnNode anchor = null; for (AbstractInsnNode insn = method.instructions.getFirst(); insn != null; insn = insn.getNext()) { if (insn instanceof FieldInsnNode) { FieldInsnNode field = (FieldInsnNode) insn; if (field.getOpcode() == Opcodes.PUTSTATIC && VIEW_ENUM.equals(field.owner) && "B".equals(field.name) && ("[L" + VIEW_ENUM + ";").equals(field.desc)) { anchor = insn; } } } if (anchor == null) throw new IllegalStateException("找不到 $VALUES 组装点"); InsnList extend = new InsnList(); extend.add(new FieldInsnNode(Opcodes.GETSTATIC, VIEW_ENUM, "B", "[L" + VIEW_ENUM + ";")); extend.add(new VarInsnNode(Opcodes.ASTORE, 40)); extend.add(new VarInsnNode(Opcodes.ALOAD, 40)); extend.add(new InsnNode(Opcodes.ARRAYLENGTH)); extend.add(new InsnNode(Opcodes.ICONST_1)); extend.add(new InsnNode(Opcodes.IADD)); extend.add(new TypeInsnNode(Opcodes.ANEWARRAY, VIEW_ENUM)); extend.add(new VarInsnNode(Opcodes.ASTORE, 41)); extend.add(new VarInsnNode(Opcodes.ALOAD, 40)); extend.add(new InsnNode(Opcodes.ICONST_0)); extend.add(new VarInsnNode(Opcodes.ALOAD, 41)); extend.add(new InsnNode(Opcodes.ICONST_0)); extend.add(new VarInsnNode(Opcodes.ALOAD, 40)); extend.add(new InsnNode(Opcodes.ARRAYLENGTH)); extend.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/lang/System", "arraycopy", "(Ljava/lang/Object;ILjava/lang/Object;II)V", false)); extend.add(new VarInsnNode(Opcodes.ALOAD, 41)); extend.add(new VarInsnNode(Opcodes.ALOAD, 40)); extend.add(new InsnNode(Opcodes.ARRAYLENGTH)); extend.add(new FieldInsnNode(Opcodes.GETSTATIC, VIEW_ENUM, VIEW_FIELD, "L" + VIEW_ENUM + ";")); extend.add(new InsnNode(Opcodes.AASTORE)); extend.add(new VarInsnNode(Opcodes.ALOAD, 41)); extend.add(new FieldInsnNode(Opcodes.PUTSTATIC, VIEW_ENUM, "B", "[L" + VIEW_ENUM + ";")); method.instructions.insert(anchor, extend); System.err.println("[view-agent] us. 已插入视图常量 " + VIEW_NAME); } ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS); node.accept(writer); return writer.toByteArray(); } private static byte[] patchUi(byte[] data) { ClassNode node = new ClassNode(); ClassReader reader = new ClassReader(data); reader.accept(node, 0); for (MethodNode method : node.methods) { if (!"".equals(method.name)) continue; // 插在构造函数最末尾(最后一条 return 之前):那时皮肤、贴图都读完了, // 期间去加载别的类不会干扰编辑器自己的资源流。 AbstractInsnNode end = method.instructions.getLast(); while (end != null && end.getOpcode() != Opcodes.RETURN) end = end.getPrevious(); if (end == null) continue; InsnList hook = new InsnList(); if ("1".equals(System.getProperty("viewagent.mark"))) { // 只碰 JDK 类,不加载我们自己的任何类(判定实验) hook.add(new LdcInsnNode("spineview.tD")); hook.add(new LdcInsnNode("1")); hook.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/lang/System", "setProperty", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", false)); hook.add(new InsnNode(Opcodes.POP)); } else if ("1".equals(System.getProperty("viewagent.ping"))) { hook.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "com/spineview/ViewInstaller", "ping", "()V", false)); } else if (!"1".equals(System.getProperty("viewagent.nop"))) { hook.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "com/spineview/ViewInstaller", "install", "()V", false)); } else { hook.add(new InsnNode(Opcodes.NOP)); System.err.println("[view-agent] 只插 NOP(判定实验)"); } method.instructions.insertBefore(end, hook); // 用 ClassWriter(reader, 0) 时不重算 max_stack,必须自己抬高, // 否则插入的代码会把操作栈写爆(表现为编辑器初始化随机的 IOException)。 // 多给几格,反正只影响这一帧的栈分配。 method.maxStack += 8; System.err.println("[view-agent] " + method.name + method.desc + " 已插入 ViewInstaller.install()"); } // 关键:用 ClassWriter(reader, 0),未改动的部分原样复制原始字节码。 // 之前用 COMPUTE_MAXS 全量重写这个 14 万字节、混淆得很厉害的类, // 会把它的初始化流程弄坏(表现为构造器里资源流出 IOException)。 // 我们只插了一条净栈效果为 0 的 invokestatic,max_stack / 栈帧都不用动。 ClassWriter writer = new ClassWriter(reader, 0); node.accept(writer); return writer.toByteArray(); } }