所以我有一些类已插入“虚拟方法调用”;即具有空主体的专用类中的静态方法。
这个想法是获取在方法调用之前推送到堆栈的参数,将它们存储在局部变量中,然后用实际实现替换方法调用。
为了了解如何处理本地人,我运行
A.java
package asmvisit;
public class A {
long y;
public long doSomething(int x, A a){
if(a == null){
this.y = (long)x;
return -1L;
}
else{
long old = y;
this.y += (long)x;
return old;
}
}
}
通过文本编辑器(代码位于帖子底部)。
正如您在输出中看到的那样(也在帖子的底部),局部变量
LOCALVARIABLE old J L4 L6 3
LOCALVARIABLE this Lasmvisit/A; L0 L6 0
LOCALVARIABLE x I L0 L6 1
LOCALVARIABLE a Lasmvisit/A; L0 L6 2
在方法的最后访问。
从技术上讲,我们可以更早地访问它们,但我明白为什么在任意位置插入本地变量可能会搞乱编号 - 以及程序。
因此,在我看来,添加更多局部变量的唯一安全方法是对每个方法运行两次:
- 除了计算局部变量访问次数之外,什么都不做
- 一旦实际修改代码,跟踪“生成”的本地变量,但将实际生成(即访问本地变量)延迟到
visitMaxs
之前,使用计数器来跟踪索引新本地人最终将拥有。
是否有更简单的替代方案,不需要两次传递?
文本化器
package asmvisit;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.util.Printer;
import org.objectweb.asm.util.Textifier;
import org.objectweb.asm.util.TraceMethodVisitor;
import java.io.PrintWriter;
import java.util.Arrays;
public class MyClassVisitor extends ClassVisitor {
public MyClassVisitor(ClassVisitor cv) {
super(Opcodes.ASM5, cv);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
System.out.println(String.format("\nvisitMethod: %d, %s, %s, %s, %s", access,name,desc,signature, Arrays.toString(exceptions)));
Printer p = new Textifier(api) {
@Override
public void visitMethodEnd() {
PrintWriter pw = new PrintWriter(System.out);
print(pw); // print it after it has been visited
pw.flush();
}
};
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
if(mv != null){
return new TraceMethodVisitor(mv,p);
}
return mv;
}
}
输出
visitMethod: 1, <init>, ()V, null, null
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lasmvisit/A; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
visitMethod: 1, doSomething, (ILasmvisit/A;)J, null, null
L0
LINENUMBER 7 L0
ALOAD 2
IFNONNULL L1
L2
LINENUMBER 8 L2
ALOAD 0
ILOAD 1
I2L
PUTFIELD asmvisit/A.y : J
L3
LINENUMBER 9 L3
LDC -1
LRETURN
L1
LINENUMBER 12 L1
FRAME SAME
ALOAD 0
GETFIELD asmvisit/A.y : J
LSTORE 3
L4
LINENUMBER 13 L4
ALOAD 0
DUP
GETFIELD asmvisit/A.y : J
ILOAD 1
I2L
LADD
PUTFIELD asmvisit/A.y : J
L5
LINENUMBER 14 L5
LLOAD 3
LRETURN
L6
LOCALVARIABLE old J L4 L6 3
LOCALVARIABLE this Lasmvisit/A; L0 L6 0
LOCALVARIABLE x I L0 L6 1
LOCALVARIABLE a Lasmvisit/A; L0 L6 2
MAXSTACK = 5
MAXLOCALS = 5
最佳答案
由 visitLocalVariable
报告的局部变量只是存储在 LocalVariableTable
attribute 中的调试信息和 LocalVariableTypeTable
attribute 。如果这些属性不存在,则不会报告此类声明。
此外,它们不需要关于字节码级别变量的完整性,即它们不报告由 long
和 double
值占用的第二个变量。它们也可能不包含合成变量,例如由 for-each 构造(保存隐藏迭代器)、try-with-resource 构造(保存挂起的异常)或挂起值(如
)引入的
尝试{返回表达式; } 最后 { otherAction(); }
构造。
在字节码级别,局部变量是通过实际存储值来建立的(仅指索引)。在源代码级别具有分离作用域的变量可以在堆栈帧中使用相同的索引。对于字节码来说,对同一索引的两次写入实际上是对同一变量的更改还是对不同作用域的两个变量的更改并不重要。但 visitMaxs
报告的大小必须足够大,以容纳操作数堆栈元素以及方法堆栈帧中使用的所有变量索引。对于指定分支目标的预期类型的新类文件来说,堆栈映射表框架也是必需的。
由于 ASM 在访问结束时报告旧的最大局部变量,因此您不能使用它来预先使用大于该值的索引,但这不是必需的。如上所述,变量索引不需要是唯一的。您的用例就像引入一个新的变量作用域,因此您可以使用在此之前未使用过的索引,并且如果在注入(inject)的代码结束后后续代码再次使用这些索引,则没有问题。
如果您可以只支持具有 StackMapTable
attributes 的较新类文件,那么获取在某一点之前使用过的索引并不难。 。对于这些类,您只需要关心两个事件。在分支目标处,visitFrame
将报告此时正在使用哪些变量。当向 ClassReader
指定 EXPAND_FRAMES
时,使用此信息会更容易。另一个需要关心的事件是实际变量使用指令(实际上,仅存储内容),通过 visitVarInsn
报告。把它们放在一起,草图看起来像
classReader.accept(new ClassVisitor(Opcodes.ASM5) {
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
return new MyMethodVisitor(access, desc);
}
}, ClassReader.EXPAND_FRAMES);
class MyMethodVisitor extends MethodVisitor {
private int used, usedAfterInjection;
public MyMethodVisitor(int acc, String signature) {
super(Opcodes.ASM5);
used = Type.getArgumentsAndReturnSizes(signature)>>2;
if((acc&Opcodes.ACC_STATIC)!=0) used--; // no this
}
@Override
public void visitFrame(
int type, int nLocal, Object[] local, int nStack, Object[] stack) {
if(type != Opcodes.F_NEW)
throw new IllegalStateException("only expanded frames supported");
int l = nLocal;
for(int ix = 0; ix < nLocal; ix++)
if(local[ix]==Opcodes.LONG || local[ix]==Opcodes.DOUBLE) l++;
if(l > used) used = l;
super.visitFrame(type, nLocal, local, nStack, stack);
}
@Override
public void visitVarInsn(int opcode, int var) {
int newMax = var+(opcode==Opcodes.LSTORE || opcode==Opcodes.DSTORE? 2: 1);
if(newMax > used) used = newMax;
super.visitVarInsn(opcode, var);
}
@Override
public void visitMethodInsn(
int opcode, String owner, String name, String desc, boolean itf) {
if(!shouldReplace(owner, name, desc)) {
super.visitMethodInsn(opcode, owner, name, desc, itf);
}
else {
int numVars = (Type.getArgumentsAndReturnSizes(desc)>>2)-1;
usedAfterInjection = used+numVars;
/*
use local vars between [used, usedAfterInjection]
*/
}
}
@Override
public void visitMaxs(int maxStack, int maxLocals) {
super.visitMaxs(maxStack, Math.max(used, usedAfterInjection));
}
}
需要注意的是,当将long
或double
值存储到变量中时,index + 1
处的变量必须是也被视为正在使用。相反,在堆栈映射表属性的框架中,这些 long
和 double
被报告为单个条目,因此我们必须查找它们并提高使用的数量适本地变量。
通过跟踪使用的
变量,我们可以在visitMethodInsn
中简单地使用超出该数量的变量,如上所述,只需将值存储到这些索引中,而无需报告它们通过visitLocalVariable
。之后也无需采取任何行动来声明它们超出范围,后续代码可能会也可能不会覆盖这些索引。
然后,如果大于旧大小,visitMaxs
必须报告更改后的大小(除非您无论如何都使用 COMPUTE_MAXS
或 COMPUTE_FRAMES
)。
关于java - 获取方法中局部变量的数量,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47674972/