问题
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 intry { 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