【问题标题】:getting the number of local variables in a method获取方法中局部变量的数量
【发布时间】:2017-12-06 13:02:38
【问题描述】:

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

这个想法是把在方法调用之前被压入堆栈的参数,将它们存储在局部变量中,然后用实际的实现替换方法调用。

要查看本地人的处理方式,我运行

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

【问题讨论】:

  • 如果你想消除对空体方法的调用,那么为什么不首先检测所有这些方法然后解析它们的签名以计算参数的数量,然后替换相应的push-es 和invokestatic-s 与 nop-s 的适当计数?在这种情况下,nops 允许您保持方法主体中的所有偏移量不变。
  • @user882813:我想,你误会了。被调用的方法是空的,因为它只是一个占位符(它可以被声明为native)。在调用站点,将注入一个实际的重要实现。

标签: java bytecode instrumentation java-bytecode-asm


【解决方案1】:

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

此外,它们不需要完整的字节码级变量,即它们不报告longdouble 值占用的第二个变量。它们也可能不包括合成变量,例如由 for-each 构造(保存隐藏的迭代器)、try-with-resource 构造(保存待处理的异常)或待处理的值(例如 in
try { return expression; } finally { otherAction(); } 构造)引入。

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

由于 ASM 在访问结束时会报告旧的 max locals,因此您不能使用它来使用比之前更大的索引,但这不是必需的。如上所述,变量索引不需要是唯一的。您的用例就像引入了一个新的变量范围,因此您可以使用在此之前未使用过的索引,如果在您注入的代码结束后后续代码再次使用这些索引,则没有问题。

如果您可以只支持具有StackMapTable attributes 的较新类文件,那么获取在某个时间点之前使用过的索引并不难。对于这些课程,您只需要关心两个事件。在分支目标处,visitFrame 将报告此时正在使用哪些变量。将EXPAND_FRAMES 指定为ClassReader 时,使用此信息更容易。另一个需要关注的事件是实际的变量使用指令(实际上,只存储物质),通过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));
    }
}

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

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

然后visitMaxs 必须报告更改后的大小,如果大于旧大小(除非您使用的是COMPUTE_MAXSCOMPUTE_FRAMES)。

【讨论】:

  • 非常感谢您写下这一切。这对我有很大帮助。您说“仅支持具有 StackMapTable 属性的较新类文件”。我假设这意味着“为 Java 6 或更高版本编译”并且包括在 ASM 中从头开始编写的类?我可以以某种方式测试byte[] 形式的类文件是否有它们?)
  • 您在visit method 中获得类文件版本。版本必须是50 或更高版本,然后堆栈映射表是强制性的。不要测试属性本身,因为该属性仅在方法具有分支时才存在。当您使用 ASM 从头开始​​编写类时,您必须自己发出适当的 visitFrame 调用或使用 COMPUTE_FRAMES 标志让 ClassWriter 生成它们。
  • 当注入的代码本身包含方法调用(this.visitMethodInsn)时,我是否必须以不同的方式处理usedAfterInjection
  • 不,这没什么区别,调用只是一条指令,它可能会从堆栈中弹出一些项目,并可能将项目推入堆栈。但如果该调用不是可替换的调用,请使用super.visitMethodInsn
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-25
  • 1970-01-01
  • 2016-10-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多