getting the number of local variables in a method

微笑、不失礼 提交于 2020-01-05 09:15:37

问题


So I have some classes into which "dummy method calls" have been inserted; i.e. static methods in a dedicated class that have an empty body.

The idea is to take the arguments that were pushed to the stack prior to the method invokation, store them in local variables, then replace the method invocation with the actual implementation.

To see how locals are handled, I run

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;
        }
    }
}

through a textifier (code at the bottom of the post).

As you can see in the output (also at the bottom of the post), the local variables

    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

get visited at the very end of the method.

Technically speaking, we would be allowed to visit them earlier, but I get why inserting locals at arbitrary places may screw up the numbering -- and with it, the program.

So the way I see it, the only safe way to add more local variables would be to run twice through every method:

  • once doing absolutely nothing except counting the number of local variable visits
  • once actually modifying the code, keeping track of the locals "generated", but delaying actual generation (i.e. visiting the local) until just before the visitMaxs, using a counter to keep track of the indices the new locals will end up having.

Is there a simpler alternative that doesn't require two passes?

textifier

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;
    }
}

output

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

回答1:


The local variables, as reported by visitLocalVariable, are only debug information, as stored in the LocalVariableTable attribute and LocalVariableTypeTable attribute. If these attributes are not present, no such declarations will be reported.

Further, they are not required to be complete regarding bytecode level variables, i.e. they do not report the second variable occupied by long and double values. They also may not include synthetic variables, like introduced by the for-each construct (holding the hidden iterator), try-with-resource construct (holding pending exceptions) or pending values like in
try { return expression; } finally { otherAction(); } constructs.

On the bytecode level, local variables are established by actually storing values into them (referring to the index only). Variables having disjunct scopes on the source code level may use the same index in the stack frame. To the bytecode, it doesn’t matter whether two writes to the same index are actually a change of the same variable or two variables with different scope. But the sizes as reported by visitMaxs must be large enough to hold the operand stack elements and all variable indices used in the method’s stack frame. Also stack map table frames are mandatory for new class files specifying the expected types for branch targets.

Since ASM reports the old max locals at the end of the visiting, you can’t use that to use indices larger than that beforehand, but it’s not necessary. As said above, variable indices are not required to be unique. Your use case is like introducing a new variable scope, so you may use indices that haven’t been used before that point and there is no problem if these indices are used again by subsequent code after your injected code ended.

Getting the indices which have been used before a certain point is not so hard, if you can live with only supporting newer class files having StackMapTable attributes. For these classes you only have to care for two events. At branch targets, visitFrame will report which variables are in use at this point. Using this information is easier when specifying EXPAND_FRAMES to the ClassReader. The other event to care are actual variable use instructions (actually, only stores matter), which are reported via visitVarInsn. Putting it together, the sketch looks like

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));
    }
}

Things to pay attention to, is that when storing long or double values into a variable, the variable at index + 1 must be considered as being in use as well. In contrast, in the frames of a stack map table attribute, these long and double are reported as single entries, so we have to look for them and raise the number of used variables appropriately.

By tracking the used variables, we can simply use variables beyond that number within visitMethodInsn, as said, by simply storing values into these indices without the need to report them via visitLocalVariable. There is also no action need for declaring that they are out of scope afterwards, the subsequent code may or may not overwrite these indices.

Then visitMaxs must report the changed size, if bigger than the old size (unless you’re using COMPUTE_MAXS or COMPUTE_FRAMES anyway).



来源:https://stackoverflow.com/questions/47674972/getting-the-number-of-local-variables-in-a-method

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!