获取方法中局部变量的数量

Use*_*291 2 java instrumentation bytecode java-bytecode-asm

所以我有一些类已插入“虚拟方法调用”;即具有空主体的专用类中的静态方法。

这个想法是获取在方法调用之前推送到堆栈的参数,将它们存储在局部变量中,然后用实际实现替换方法调用。

为了看看当地人是如何处理的,我运行

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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

通过文本编辑器(代码在帖子底部)。

正如您在输出中看到的(也在帖子的底部),局部变量

    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
Run Code Online (Sandbox Code Playgroud)

在方法的最后被访问。

从技术上讲,我们将被允许更早地访问它们,但我明白为什么在任意位置插入局部变量可能会搞乱编号 - 以及程序。

因此,在我看来,添加更多局部变量的唯一安全方法是对每个方法运行两次:

  • 除了计算局部变量访问次数之外,什么都不做
  • 一旦实际修改代码,跟踪“生成”的局部变量,但将实际生成(即访问局部变量)延迟到 之前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;
    }
}
Run Code Online (Sandbox Code Playgroud)

输出

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
Run Code Online (Sandbox Code Playgroud)

Hol*_*ger 5

由 报告的局部变量visitLocalVariable只是存储在attributeattribute中的调试信息。如果这些属性不存在,则不会报告此类声明。LocalVariableTableLocalVariableTypeTable

\n\n

此外,它们不要求关于字节码级变量是完整的,即它们不报告由long和占用的第二变量double的值。它们也可能不包含合成变量,例如由 for-each 构造(保存隐藏迭代器)、try-with-resource 构造(保存挂起的异常)或挂起值(如
\ntry { return expression; } finally { otherAction(); }构造中的值)引入的变量。

\n\n

在字节码级别,局部变量是通过实际存储值来建立的(仅指索引)。在源代码级别具有分离作用域的变量可以在堆栈帧中使用相同的索引。对于字节码来说,对同一索引的两次写入实际上是同一变量的更改还是具有不同作用域的两个变量并不重要。但报告的大小visitMaxs必须足够大,以容纳操作数堆栈元素以及 method\xe2\x80\x99s 堆栈帧中使用的所有变量索引。对于指定分支目标的预期类型的​​新类文件来说,堆栈映射表框架也是必需的。

\n\n

由于 ASM 在访问结束时报告旧的最大局部变量,因此您可以\xe2\x80\x99t 使用它来预先使用大于该值的索引,但这\xe2\x80\x99s 不是必需的。如上所述,变量索引不需要是唯一的。您的用例就像引入一个新的变量作用域,因此您可以使用在此之前未使用过的索引,并且如果在注入的代码结束后后续代码再次使用这些索引,则没有问题。

\n\n

StackMapTable如果您可以只支持具有attribute的较新类文件,那么获取在某个点之前使用过的索引并不难。对于这些类,您只需要关心两个事件。在分支目标处,visitFrame将报告此时正在使用哪些变量。EXPAND_FRAMES当指定到 时,使用此信息会更容易ClassReader。另一个需要关心的事件是实际的变量使用指令(实际上,仅存储问题),通过 报告visitVarInsn。把它们放在一起,草图看起来像

\n\n
classReader.accept(new ClassVisitor(Opcodes.ASM5) {\n    @Override\n    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {\n        return new MyMethodVisitor(access, desc);\n    }\n}, ClassReader.EXPAND_FRAMES);\n
Run Code Online (Sandbox Code Playgroud)\n\n\n\n
class MyMethodVisitor extends MethodVisitor {\n    private int used, usedAfterInjection;\n\n    public MyMethodVisitor(int acc, String signature) {\n        super(Opcodes.ASM5);\n        used = Type.getArgumentsAndReturnSizes(signature)>>2;\n        if((acc&Opcodes.ACC_STATIC)!=0) used--; // no this\n    }\n\n    @Override\n    public void visitFrame(\n            int type, int nLocal, Object[] local, int nStack, Object[] stack) {\n        if(type != Opcodes.F_NEW)\n            throw new IllegalStateException("only expanded frames supported");\n        int l = nLocal;\n        for(int ix = 0; ix < nLocal; ix++)\n            if(local[ix]==Opcodes.LONG || local[ix]==Opcodes.DOUBLE) l++;\n        if(l > used) used = l;\n        super.visitFrame(type, nLocal, local, nStack, stack);\n    }\n\n    @Override\n    public void visitVarInsn(int opcode, int var) {\n        int newMax = var+(opcode==Opcodes.LSTORE || opcode==Opcodes.DSTORE? 2: 1);\n        if(newMax > used) used = newMax;\n        super.visitVarInsn(opcode, var);\n    }\n\n    @Override\n    public void visitMethodInsn(\n            int opcode, String owner, String name, String desc, boolean itf) {\n        if(!shouldReplace(owner, name, desc)) {\n            super.visitMethodInsn(opcode, owner, name, desc, itf);\n        }\n        else {\n            int numVars = (Type.getArgumentsAndReturnSizes(desc)>>2)-1;\n            usedAfterInjection = used+numVars;\n            /*\n              use local vars between [used, usedAfterInjection]\n            */\n        }\n    }\n    @Override\n    public void visitMaxs(int maxStack, int maxLocals) {\n        super.visitMaxs(maxStack, Math.max(used, usedAfterInjection));\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

需要注意的是,当将值存储longdouble变量中时,变量 atindex + 1也必须被视为正在使用。相反,在堆栈映射表属性的框架中,这些longdouble被报告为单个条目,因此我们必须查找它们并适当地增加使用的变量的数量。

\n\n

通过跟踪used变量,我们可以简单地使用超出该数量的变量visitMethodInsn,如上所述,只需将值存储到这些索引中,而不需要通过 报告它们visitLocalVariable。之后也不需要声明它们超出范围,后续代码可能会也可能不会覆盖这些索引。

\n\n

然后visitMaxs必须报告更改后的大小(如果大于旧大小)(除非您\xe2\x80\x99正在使用COMPUTE_MAXSCOMPUTE_FRAMES无论如何)。

\n