问题
I am doing an online bytecode method inlining optimization using ASM. My changes are based on the example 3.2.6 Inline Method
(http://asm.ow2.org/current/asm-transformations.pdf). The test example (inline callee's calculate(int,int) at Caller::test) is:
public class Caller {
final Callee _callee;
public Caller(Callee callee){
_callee = callee;
}
public static void main(String[] args) {
new Caller(new Callee("xu", "shijie")).test(5, 100);
}
public void test(int a, int b){
int t = a;
int p = b;
int r = t+p-_callee.calculate(a, b);
int m = t-p;
System.out.println(t);
}
}
public class Callee {
final String _a;
final String _b;
public Callee(String a, String b){
_a = a;
_b = b;
}
public int calculate(int t, int p){
int tmp = _a.length()+_b.length();
tmp+=t+p;
return tmp;
}
}
Based on ASM 5.0 version, my code is:
//MainInliner.java
public class MainInliner extends ClassLoader{
public byte[] generator(String caller, String callee) throws ClassNotFoundException{
String resource = callee.replace('.', '/') + ".class";
InputStream is = getResourceAsStream(resource);
byte[] buffer;
// adapts the class on the fly
try {
resource = caller.replace('.', '/')+".class";
is = getResourceAsStream(resource);
ClassReader cr = new ClassReader(is);
ClassWriter cw = new ClassWriter(0);
ClassVisitor visitor = new BCMerge(Opcodes.ASM5, cw, callee);
cr.accept(visitor, 0);
buffer= cw.toByteArray();
} catch (Exception e) {
throw new ClassNotFoundException(caller, e);
}
// optional: stores the adapted class on disk
try {
FileOutputStream fos = new FileOutputStream("/tmp/data.class");
fos.write(buffer);
fos.close();
} catch (IOException e) {}
return buffer;
}
@Override
protected synchronized Class<?> loadClass(final String name,
final boolean resolve) throws ClassNotFoundException {
if (name.startsWith("java.")) {
System.err.println("Adapt: loading class '" + name
+ "' without on the fly adaptation");
return super.loadClass(name, resolve);
} else {
System.err.println("Adapt: loading class '" + name
+ "' with on the fly adaptation");
}
String caller = "code.sxu.asm.example.Caller";
String callee = "code.sxu.asm.example.Callee";
byte[] b = generator(caller, callee);
// returns the adapted class
return defineClass(caller, b, 0, b.length);
}
public static void main(final String args[]) throws Exception {
// loads the application class (in args[0]) with an Adapt class loader
ClassLoader loader = new MainInliner();
Class<?> c = loader.loadClass(args[0]);
Method m = c.getMethod("main", new Class<?>[] { String[].class });
}
}
class BCMerge extends ClassVisitor{
String _callee;
String _caller;
public BCMerge(int api, ClassVisitor cv, String callee) {
super(api, cv);
// TODO Auto-generated constructor stub
_callee = callee.replace('.', '/');
}
public void visit(int version, int access, String name, String signature,
String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
this._caller = name;
}
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
if(!name.equals("test")){
return super.visitMethod(access, name, desc, signature, exceptions);
}
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
ClassReader cr = null;
try {
cr = new ClassReader(this.getClass().getClassLoader().getResourceAsStream(_callee.replace('.', '/')+".class"));
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
ClassNode classNode = new ClassNode();
cr.accept(classNode, 0);
MethodNode inlinedMethod = null;
for(MethodNode node: classNode.methods){
if(node.name.equals("calculate")){
inlinedMethod = node;
break;
}
}
return new MethodCallInliner(access, desc, mv, inlinedMethod, _callee, _caller );
}
}
//MethodCallInliner.java
public class MethodCallInliner extends LocalVariablesSorter {
private final String oldClass;
private final String newClass;
private final MethodNode mn; //Method Visitor wrappers the mv.
private List blocks = new ArrayList();
private boolean inlining;
public MethodCallInliner(int access, String desc, MethodVisitor mv, MethodNode mn,
String oldClass, String newClass){
super(Opcodes.ASM5, access, desc, mv);
this.oldClass = oldClass;
this.newClass = newClass;
this.mn = mn;
inlining = false;
}
public void visitMethodInsn(int opcode, String owner, String name,
String desc, boolean itf) {
System.out.println("opcode:" + opcode + " owner:" + owner + " name:"
+ name + " desc:" + desc);
if (!canBeInlined(owner, name, desc)) {
mv.visitMethodInsn(opcode, owner, name, desc, itf);
return;
}
//if it is INVOKEVIRTUAL ../Callee::calculate(II), then..
Remapper remapper = new SimpleRemapper(oldClass, newClass);
Label end = new Label();
inlining = true;
mn.instructions.resetLabels();
mn.accept(new InliningAdapter(this,opcode == Opcodes.INVOKESTATIC ? Opcodes.ACC_STATIC : 0, desc,remapper, end));
inlining = false;
super.visitLabel(end);
}
private boolean canBeInlined(String owner, String name, String decs){
if(name.equals("calculate") && owner.equals("code/sxu/asm/example/Callee")){
return true;
}
return false;
}
}
//InliningAdapter.java
public class InliningAdapter extends RemappingMethodAdapter {
private final LocalVariablesSorter lvs;
private final Label end;
public InliningAdapter(LocalVariablesSorter mv,
int acc, String desc,Remapper remapper, Label end) {
super(acc, desc, mv, remapper);
this.lvs = mv;
this.end = end;
// int offset = (acc & Opcodes.ACC_STATIC)!=0 ?0 : 1;
// Type[] args = Type.getArgumentTypes(desc);
// for (int i = args.length-1; i >= 0; i--) {
// super.visitVarInsn(args[i].getOpcode(
// Opcodes.ISTORE), i + offset);
// }
// if(offset>0) {
// super.visitVarInsn(Opcodes.ASTORE, 0);
// }
}
public void visitInsn(int opcode) {
if(opcode==Opcodes.RETURN || opcode == Opcodes.IRETURN) {
super.visitJumpInsn(Opcodes.GOTO, end);
} else {
super.visitInsn(opcode);
}
}
public void visitMaxs(int stack, int locals) {
System.out.println("visit maxs: "+stack+" "+locals);
}
protected int newLocalMapping(Type type) {
return lvs.newLocal(type);
}
}
In the code, both InliningAdapter
and MethodCallInliner
extends LocalVariablesSorter
, which renumbers local variables. And the inline references coping body of Callee::calculate() at the Caller::test::invokevirtual(Callee::calculate) call site.
The bytecodes for Caller::test(), Callee::calculate, and generated::test are:
//Caller::test()
public void test(int, int);
flags: ACC_PUBLIC
Code:
stack=4, locals=7, args_size=3
0: iload_1
1: istore_3
2: iload_2
3: istore 4
5: iload_3
6: iload 4
8: iadd
9: aload_0
10: getfield #13 // Field _callee:Lcode/sxu/asm/example/Callee;
13: iload_1
14: iload_2
15: invokevirtual #39 // Method code/sxu/asm/example/Callee.calculate:(II)I //Copy calculate's body here
18: isub
19: istore 5
21: iload_3
22: iload 4
24: isub
25: istore 6
27: getstatic #43 // Field java/lang/System.out:Ljava/io/PrintStream;
30: iload_3
31: invokevirtual #49 // Method java/io/PrintStream.println:(I)V
34: getstatic #43 // Field java/lang/System.out:Ljava/io/PrintStream;
37: ldc #55 // String 1..........
39: invokevirtual #57 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
42: return
//Callee::calculate()
public int calculate(int, int);
flags: ACC_PUBLIC
Code:
stack=3, locals=4, args_size=3
0: aload_0
1: getfield #14 // Field _a:Ljava/lang/String;
4: invokevirtual #26 // Method java/lang/String.length:()I
7: aload_0
8: getfield #16 // Field _b:Ljava/lang/String;
11: invokevirtual #26 // Method java/lang/String.length:()I
14: iadd
15: istore_3
16: iload_3
17: iload_1
18: iload_2
19: iadd
20: iadd
21: istore_3
22: iload_3
23: ireturn
//data.class
public void test(int, int);
flags: ACC_PUBLIC
Code:
stack=4, locals=8, args_size=3
0: iload_1
1: istore_3
2: iload_2
3: istore 4
5: iload_3
6: iload 4
8: iadd
9: aload_0
10: getfield #14 // Field _callee:Lcode/sxu/asm/example/Callee;
13: iload_1
14: iload_2
15: aload_0
16: getfield #40 // Field _a:Ljava/lang/String;
19: invokevirtual #46 // Method java/lang/String.length:()I
22: aload_0
23: getfield #49 // Field _b:Ljava/lang/String;
26: invokevirtual #46 // Method java/lang/String.length:()I
29: iadd
30: istore 6
32: iload 6
34: iload_1
35: iload_2
36: iadd
37: iadd
38: istore 6
40: iload 6
42: goto 45
45: isub
46: istore 6
48: iload_3
49: iload 4
51: isub
52: istore 7
54: getstatic #59 // Field java/lang/System.out:Ljava/io/PrintStream;
57: iload_3
58: invokevirtual #65 // Method java/io/PrintStream.println:(I)V
61: getstatic #59 // Field java/lang/System.out:Ljava/io/PrintStream;
64: ldc #67 // String 1..........
66: invokevirtual #70 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
69: return
The javap result on the data.class shows that the body of Callee::calculate has been inserted to the right place (Caller::test line::15). However, there are two main problems:
The top three stack objects before invokevirtual
Callee::calculate(line 15) 9: aload_0
10: getfield #14 // Field _callee:Lcode/sxu/asm/example/Callee; 13: iload_1
14: iload_2should not be on the stack after inline.
The variable number 0 in the copied body (Callee::calculate()) should mapped to the right number
The variable number are not correct. First, the variable numbers of the copied body of Callee::calculate in the data.class (from line 15 to line 42) shoud begin 5 (instead of 0). Second, the variable numbers after Callee::calculate() should be renumbered by the rule: a) not change if it is between (0,4]; b) Renumber if it is conflict with the number in the copied body of Callee::calculate()
I went to check base class LocalVariablesSorter
's implementation. The problem seems to be at its construction:
protected LocalVariablesSorter(final int api, final int access,
final String desc, final MethodVisitor mv) {
super(api, mv);
Type[] args = Type.getArgumentTypes(desc);
nextLocal = (Opcodes.ACC_STATIC & access) == 0 ? 1 : 0;
for (int i = 0; i < args.length; i++) {
nextLocal += args[i].getSize();
}
firstLocal = nextLocal;
}
private int[] mapping = new int[40];
To me, it seems that the firstLocal
are always starts at 1+ args.length() (In this case it is 3). This class also provide private int remap(final int var, final Type type)
, which creates new local variables and keeps the mapping (from existing variable number to the new index) in the mapping array.
My problem is that how to use LocalVariablesSorter
and inline the bytecode method (Callee::calculate) correctly. Any suggestion for efficient inline is welcome.
For parameters on the stack before inline (before line 15). My idea is to store them as new created local variables which would be referred by the copied body of Callee::calculate. For example, add:
astore 5
after
10: getfield #14 // Field _callee:Lcode/sxu/asm/example/Callee;
and add mapping[0]=5+1
in the LocalVariablesSorter
But the main problem is that users are not allowed to update LocalVariablesSorter::mapping
(from old variable number to the new variable in the mapping array) because mapping
array is private and the only place for its update is in the method:
private int remap(final int var, final Type type) {
if (var + type.getSize() <= firstLocal) {
//Variable index will never be modified if it is less than firstLocal. 0 < 3. Nothing i can do for ALOAD 0.
return var;
}
int key = 2 * var + type.getSize() - 1;
int size = mapping.length;
if (key >= size) {
.....
}
int value = mapping[key];
if (value == 0) {
value = newLocalMapping(type);
setLocalType(value, type);
mapping[key] = value + 1;
} else {
value--;
}
if (value != var) {
changed = true;
}
return value;
}
Update1: The data.class after uncomment constructor of the InliningAdapter:
0: iload_1
1: istore_3
2: iload_2
3: istore 4
5: iload_3
6: iload 4
8: iadd
9: aload_0
10: getfield #14 // Field _callee:Lcode/sxu/asm/example/Callee;
13: iload_1
14: iload_2
15: istore_2 //should be istore 5
16: istore_1 //should be istore 6
17: astore_0 //should be astore 7
18: aload_0
19: getfield #40 // Field _a:Ljava/lang/String;
22: invokevirtual #46 // Method java/lang/String.length:()I
25: aload_0
26: getfield #49 // Field _b:Ljava/lang/String;
29: invokevirtual #46 // Method java/lang/String.length:()I
32: iadd
33: istore 6
35: iload 6
37: iload_1
38: iload_2
39: iadd
40: iadd
41: istore 6
43: iload 6
45: goto 48
48: isub
49: istore 6
51: iload_3
52: iload 4
54: isub
55: istore 7
57: getstatic #59
The new stored three variables (15,16,17) should be numbered as 5,6,7, rather than 2,1,0, and the mapping in *store/*load
in the inlined code should be like
0 => 7
1 => 6
2 => 5
3 => 8
4 => 9 ...
These mapping should be in the array:LocalVariablesSorter::mapping
which is updated by the method LocalVariablesSorter::remap()
. However, it seems not possible for i to insert them into mapping
array.
There are two kinds of remappering should be done:
- Remapping inside of inlined code (From line 18 t0 45) and variable index starts at 5. The max index is k
- Remapping after inlined code (From line 46 to the end), and any variable index should be remapped (new index starts at k+1) if it the original index is greater than 5
回答1:
As @Holger already suggested, start by uncommenting the lines in InliningAdapter
.
To tackle the main problems you listed: The
LocalVariablesSorter
(extended byInliningAdapter
) thinks that the arguments are already stored in the local variables at fixed locations - this is indeed the normal situation when entering a method. So it does not map those at all (see first line inLocalVariablesSorter.remap()
-firstLocal
is calculated in constructor). However in this case we get the arguments on the stack instead and need to allocate the local variables manually. The solution is to tellLocalVariablesSorter
that there are no parameters already stored in local variables (makefirstLocal = 0
). Then it will treat any reference to them as new variables and allocate new local variables for them. This we can achieve by foolingLocalVariablesSorter
to think that there are no arguments and that the method is static (even if it really isn't). So we change the first line inInliningAdapter
fromsuper(acc, desc, mv, remapper);
to
super(acc | Opcodes.ACC_STATIC, "()V", mv, remapper);
Now the variables 0,1,2,... get remapped to 5,6,7,... or similar (doesn't really matter what they are, the
LocalVariablesSorter
ofCaller
(i.e. theMethodCallInliner
instance) takes care of allocating them).There is another problem also that you map the
Callee
class toCaller
by havingInliningAdapter
extendRemappingMethodAdaptor
- however I guess you want to keep the_a
and_b
variables stored in theCallee
instance.- If my guess is correct then you should actually not remap the references from
Callee
toCaller
. You can just makeInliningAdapter
extendLocalVariableSorter
instead and get rid of the remapper. - If my guess is incorrect then I guess you will probably need to embed the variables of
Callee
intoCaller
as well, in which case you should keep theRemappingMethodAdaptor
you have.
- If my guess is correct then you should actually not remap the references from
When debugging the inlined code the linenumbers from the
Callee
will not make sense since the code was inlined into theCaller
class. So all linenumbers fromCallee
should probably be replaced with the line number of the line inCaller
where the inlined call occured. Unfortunately in Java you cannot specify different source code files on a line-by-line basis (like you can in C for example). So you would overridevisitLineNumber()
inInliningAdapter
using something like this (inlinedLine
would be passed to constructor ofInliningAdapter
):@Override public void visitLineNumber(int line, Label start) { super.visitLineNumber(inlinedLine, start); }
.. or perhaps skip the super call altogether, I'm not 100% sure about this.
来源:https://stackoverflow.com/questions/29802059/remapper-variables-during-bytecode-method-inlining-by-asm