如何利用ASM复制JVM栈中方法参数并插入指定静态方法调用
在ASM中虚方法调用后插入静态方法并复用原参数的实现方案
这个场景的核心难点在于:虚方法INVOKEVIRTUAL执行时会弹出栈上的所有调用参数,所以直接在调用后插入静态方法调用的话,栈上已经没有原参数可用了。我们需要先把这些参数临时保存到局部变量表,调用完成后再重新加载出来,才能传递给MyClass.myMethod。
下面分步骤拆解实现逻辑,同时给出ASM代码示例:
核心思路梳理
原方法调用a.doSomeThing(p1,p2,p3,p4,p5,p6)执行前,栈帧的状态是(从栈底到栈顶):a → p1 → p2 → p3 → p4 → p5 → p6。执行INVOKEVIRTUAL后,这些参数会被弹出栈,如果原方法有返回值,栈顶会替换为返回值。
所以我们需要做的是:
- 在执行原虚方法调用前,把栈上的所有参数(包括实例
a)保存到局部变量表 - 重新加载这些参数,执行原方法调用
- 处理原方法的返回值(如果有),避免破坏栈帧状态
- 再次加载保存的参数,调用目标静态方法
- 恢复原方法的返回值(如果有),保证后续代码正常执行
ASM代码实现示例
我们通过自定义MethodVisitor来拦截目标虚方法调用,并插入对应的指令:
import org.objectweb.asm.*; import org.objectweb.asm.Type; class InsertStaticCallVisitor extends MethodVisitor { // 目标虚方法的信息 private final String targetMethodOwner; private final String targetMethodName; private final String targetMethodDesc; // 要插入的静态方法信息 private final String staticMethodOwner; private final String staticMethodName; private final String staticMethodDesc; public InsertStaticCallVisitor(int api, MethodVisitor mv, String targetOwner, String targetName, String targetDesc, String staticOwner, String staticName, String staticDesc) { super(api, mv); this.targetMethodOwner = targetOwner; this.targetMethodName = targetName; this.targetMethodDesc = targetDesc; this.staticMethodOwner = staticOwner; this.staticMethodName = staticMethodName; this.staticMethodDesc = staticDesc; } @Override public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { // 拦截目标虚方法调用 if (opcode == Opcodes.INVOKEVIRTUAL && targetMethodOwner.equals(owner) && targetMethodName.equals(name) && targetMethodDesc.equals(descriptor)) { Type[] methodArgs = Type.getArgumentTypes(targetMethodDesc); int totalArgCount = methodArgs.length + 1; // 加上实例a,共7个参数 // 步骤1:把栈上的参数依次保存到局部变量表(从栈顶到栈底,也就是p6→p5→...→a) for (int i = totalArgCount - 1; i >= 0; i--) { Type argType = i == 0 ? Type.getObjectType(targetMethodOwner) : methodArgs[i - 1]; int storeOpcode = argType.getOpcode(Opcodes.ISTORE); mv.visitVarInsn(storeOpcode, i); // 这里假设原方法局部变量表前7个位置可用,实际可根据maxLocals调整 } // 步骤2:重新加载参数,执行原虚方法调用 for (int i = 0; i < totalArgCount; i++) { Type argType = i == 0 ? Type.getObjectType(targetMethodOwner) : methodArgs[i - 1]; int loadOpcode = argType.getOpcode(Opcodes.ILOAD); mv.visitVarInsn(loadOpcode, i); } super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); // 步骤3:处理原方法返回值(如果有) Type returnType = Type.getReturnType(targetMethodDesc); int returnVarIdx = totalArgCount; if (!returnType.equals(Type.VOID_TYPE)) { int storeOpcode = returnType.getOpcode(Opcodes.ISTORE); mv.visitVarInsn(storeOpcode, returnVarIdx); } // 步骤4:加载保存的参数,调用静态方法 for (int i = 0; i < totalArgCount; i++) { Type argType = i == 0 ? Type.getObjectType(targetMethodOwner) : methodArgs[i - 1]; int loadOpcode = argType.getOpcode(Opcodes.ILOAD); mv.visitVarInsn(loadOpcode, i); } mv.visitMethodInsn(Opcodes.INVOKESTATIC, staticMethodOwner, staticMethodName, staticMethodDesc, false); // 步骤5:恢复原方法返回值(如果有) if (!returnType.equals(Type.VOID_TYPE)) { int loadOpcode = returnType.getOpcode(Opcodes.ILOAD); mv.visitVarInsn(loadOpcode, returnVarIdx); } } else { // 非目标方法,直接转发指令 super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); } } @Override public void visitMaxs(int maxStack, int maxLocals) { // 调整局部变量表大小,确保能容纳我们保存的参数和返回值 Type[] methodArgs = Type.getArgumentTypes(targetMethodDesc); int totalArgCount = methodArgs.length + 1; int addedLocals = totalArgCount + (Type.getReturnType(targetMethodDesc) != Type.VOID_TYPE ? 1 : 0); super.visitMaxs(maxStack + totalArgCount, maxLocals + addedLocals); } }
关键细节说明
- 参数类型适配:代码中通过
Type.getOpcode自动适配不同参数类型的LOAD/STORE指令(比如引用类型用ALOAD/ASTORE,long用LLOAD/LSTORE等),保证对所有参数类型都有效。 - 局部变量表冲突避免:在
visitMaxs方法中我们主动增加了局部变量表的大小,确保保存参数和返回值的变量不会和原方法的局部变量冲突。 - 栈帧状态保持:无论原方法是否有返回值,我们都保证了栈帧状态和原代码一致,不会影响后续指令的执行。
内容的提问来源于stack exchange,提问作者user5549139
相关产品推荐
相关产品推荐

