函数
我们在使用ASM相关API对函数进行操作之前,我们需要了解函数在字节码的存储格式及其执行模型。
执行模型
我们需要简单了解Java虚拟机的执行模型。Java代码是在线程中执行的,每个Java程序可能包含多个执行线程,而每个线程都有一个执行栈。这些执行栈是由许多frame(帧)组成的,每一个frame都代表一个函数调用。每当一个函数被调用的时候,一个frame就被推到执行线程的栈顶。当函数执行完毕(无论是正常还是异常返回)这个frame就会弹栈,然后执行下一个frame.
每一个frame都包含两个部分,一个本地的符号表和一个操作栈。符号表存储了可以被自由取值的变量,操作栈主要用于在执行时存储字节码指令。本地符号表大小及操作栈大小是由函数本身大小决定的。因此二者是在编译期由编译器计算并存储在字节码中的。
当每个frame创建的时候,其操作栈是空的,符号表由this对象和函数参数组成。符号表和操作栈中每一个item都可以防止任意的除long和double以外的Java对象。这是因为long和double需要两个item才能存下。
字节码指令
字节码指令是由一个opcode和几个固定的参数组成
opcode 是无符号byte类型。因此一般用助记符代替,例如一般使用NOP代替0.参数一般是静态的值,用于说明opcode的操作对象。参数是静态存储在class文件里的。opcode只有运行时才知道。
总体来说opcode主要分为两大类,其中一小部分主要用于将数据由符号表推送到操作栈或者相反。其余的指令只是操作操作栈上的对象。他们可以从操作栈弹出几个对象,根据这些对象计算出一些值,然后将结果再送回操作栈。
ILOAD LLOAD FLOAD DLOAD ALOAD 指令用于将符号表中的变量推动到操作栈上。他们接收变量在符号表的索引值。ILOAD用于操作boolean byte char short int. LOAD ,FLOAD DLOAD用于long,float,double的操作。ALOD主要用于非基本数据类型的操作。对应 ISTORE,LSTORE,FSTORE,DSTORE ASTORE 从操作站弹出并将其存储在符号表。字节码指令基本都是类型相关的。字节码指令一般可以分为:
- Stack(栈操作相关) 主要用于操作操作栈上的数据:POP,DUP(push复制栈顶的数据)SWAP(交换栈顶的两个元素)
- Constants(常量相关) 将一个常量push到栈顶ACONST_NULL(pushes null), ICONST_0(push 0) FCONST_0(push 0f) DCONST_0 BIPUSH b(push byte 类型的 b)SIPUSH s(push short 类型 s) LDC(push 任意类型 int float long double String 或者是class 常量等)
- Arithmentic and logic(逻辑运算相关) 弹出栈顶几个元素进行运算将结果push到栈顶。xADD xSUB xDIV xREM 分别代表 + - * / % 运算。x可以是‘I’ ‘L’ 'F' 'D' 类似还有和<< >> >>> | ^ & 等相对应的指令。
- Casts(类型转换相关) 这些指令将栈顶元素弹出,转换类型后入栈。它和java中类型转换相对应。I2F F2D L2D 等 CHECKCAST t将一个引用类型的对象转换为t类型
- Objects(对象相关)这些指令用于创建对象,锁定对象,检查类型等 NEW type 会将一个type类型的对象入栈。
- Fields(取值赋值相关) GETFIELD owner name desc 弹出对象引用,将其name对应的变量的值入栈。PUTFIELD将对象弹栈将其值存储在name对应的存储区。这两个指令中弹出的对象必须是owner类型,field的类型必须是desc类型。GETSTATIC和PUTSTATIC是类似的。不过其用于操作静态变量。
- Methods(方法相关) 这些指令可以调用一个函数或者构造函数。他们会弹出函数参数的个数加上1(调用者对象)个对象,将函数结果入栈。INVOKEVIRTUAL owner name desc 会调用定义在owner类中函数签名为desc名称为name的函数。INVOKESTATIC 用于调用静态方法,INVOKESPECIAL 用于private及构造函数。INVOKEINTERFACE 用于调用定义在接口中的方法。对于java7而言INVOKEDYNAMIC 是用于动态方法调用。
- ARRAYS(数组相关) 这些指令主要用于读写数组。xALOAD指令会弹出索引和数组,并且将数组中索引对应的值入栈。xSTORE会弹出一个值,索引和数组,将值存储在数组的索引位置。x可以是I L F D A B C S
- Jumps(跳转指令) 这些指令会在指定的条件为true或者false的时候跳转到任意指定的指令接着执行。他们对应于高级语言的if for do while break continue 等流程控制语句。IFEQ label会弹出int值,如果该值为0就跳转到label指定的指令。其他的跳转指令也类似:IFNE IFGE ... TABLESWITCH LOOKUPSWITCH 对应于java语言中switch语句。
- Return(返回)xRETURN 和 RETURN指令被用于终止函数的执行,返回相关值给函数调用者。RETURN对应于 return void, xRETURN用于返回其他值。
对于以下简单的函数,其对应的指令如下:
package pkg;
public class Bean {
private int f;
public int getF() {
return this.f;
}
public void setF(int f) {
this.f = f;
}
}
// getF函数对应的指令如下
ALOAD 0 //将this入栈
GETFIELD pkg/Bean f I //this弹出,将this.f入栈
IRETURN // 返回 this.f
// setF函数对应的指令
ALOAD 0 // this 入栈
ILOAD 1 // 将索引为1(类型为int)的变量入栈
PUTFIELD pkg/Bean f I // 弹出两个值,并且将栈顶的元素及this弹出,并将其值赋给this.f
RETURN // RETURN
Bean对象会有默认的构造函数,其对应的指令为:ALOAD 0 INVOKESPECIAL java/lang/Object <init> ()V RETURN
构造函数在字节码中的名称为<init>
一个复杂的例子如下:
public void checkAndSetF(int f) {
if (f >= 0) {
this.f = f;
} else {
throw new IllegalArgumentException();
}
}
// 其对应的字节码指令
ILOAD 1 // f入栈
IFLT label // f弹栈,如果小于0,跳转到label
ALOAD 0 // this 入栈
ILOAD 1 // f入栈
PUTFIELD pkg/Bean f I // this.f = f
GOTO end //跳转到end
label:
NEW java/lang/IllegalArgumentException //生成IllegalArgumentException对象
DUP // 赋值对象
INVOKESPECIAL java/lang/IllegalArgumentException <init> ()V // 弹栈并调用初始化方法
ATHROW // 抛出栈底异常,指令执行结束
end:
RETURN
异常处理
并没有catch对应的字节码指令,不过函数会和一系列exception handler(异常处理代码)相关联。当抛出指定的异常时对应的handler代码就会执行。因此exception handler就和try catch代码块类似。
public static void sleep(long d) {
try {
Thread.sleep(d);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//对应的指令如下
TRYCATCHBLOCK try catch catch java/lang/InterruptedException // 声明handler,如果try: 和 catch: 之间代码发生异常,则生成InterruptedException实例,跳转到catch:
try:
LLOAD 0
INVOKESTATIC java/lang/Thread sleep (J)V
RETURN
catch:
INVOKEVIRTUAL java/lang/InterruptedException printStackTrace ()V
RETURN
Frames(帧)
Java6及以上版本编译的class文件,包含一系列stack map frames来加速jvm对class的校验。它们甚至在运行前就能告知jvm某个frame的符号表及操作栈的详细信息。为此,我们可以为frame中的每一个指令创建一个frame来查看其运行时的状态
//运行前的state frame 对应的指令
[pkg/Bean] [] ALOAD 0
[pkg/Bean] [pkg/Bean] GETFIELD
[pkg/Bean] [I] IRETURN
//对于 throw new IllegalArgumentException的代码:
[pkg/Bean I] [] NEW
[pkg/Bean I] [Uninitialized(label)] DUP
[pkg/Bean I] [Uninitialized(label) Uninitialized(label)] INVOKESPECIAL
[pkg/Bean I] [java/lang/IllegalArgumentException] ATHROW
上述的Uninitialized只存在于stack map frame中,代表内存分配完毕但是构造函数还没调用。UNINITIALIZED_THIS 代表被初始化为0 TOP代表未定义类型 NULL代表null
对于编译后的class为了节省空间,实际上并不是每一个指令都对应一个state frame,而是只有跳转指令和异常处理handler 和无条件跳转后面的第一个指令包含state frame. 而其他指令可以从已有的state frames推断出来。为了进一步节省空间,每一个frame只有在和上一个frame不同的时候才会被存储。初始帧由于可以很容易从函数参数中推断,因此不会存储,而后续的帧如果和初始帧相同只需存储 F_SAME即可
ILOAD 1
IFLT label
ALOAD 0
ILOAD 1
PUTFIELD pkg/Bean f I
GOTO end
label:
F_SAME
NEW java/lang/IllegalArgumentException
DUP
INVOKESPECIAL java/lang/IllegalArgumentException <init> ()V
ATHROW
end:
F_SAME
RETURN
接口及组件
函数的生成改写需要使用MethodVisitor(在ClassVisitor的visitMethod函数中返回),生成函数时各个部分的调用顺序也是固定的:
visitAnnotationDefault?
( visitAnnotation | visitParameterAnnotation | visitAttribute )*
( visitCode
( visitTryCatchBlock | visitLabel | visitFrame | visitXxxInsn |
visitLocalVariable | visitLineNumber )*
visitMaxs )?
visitEnd
注解和参数需要首先被生成,然后是方法体,最后需要调用visitMax。 visitCode和visitMax可以看做函数体的开始和结束。最后需要调用visitEnd代表事件结束。
ClassVisitor cv = ...;
cv.visit(...);
MethodVisitor mv1 = cv.visitMethod(..., "m1", ...);
mv1.visitCode();
mv1.visitInsn(...);
...
mv1.visitMaxs(...);
mv1.visitEnd();
MethodVisitor mv2 = cv.visitMethod(..., "m2", ...);
mv2.visitCode();
mv2.visitInsn(...);
...
mv2.visitMaxs(...);
mv2.visitEnd();
cv.visitEnd();
于我们而言计算某个方法的stack frame绝非易事。幸好ASM可以帮我们计算,当你声明一个ClassWriter的时候你可以声明让ASM自动计算这些值。例如:new ClassWriter(ClassWriter.COMPUTE_MAX)
符号表及操作栈的大小会自动帮你计算。但你仍然需要调用visitMax,此时你传什么值都可以(它们会被忽略),但你仍要自己计算frames。而new ClassWriter(ClassWriter.COMPUTE_FRAMES)
所有的都会自动被计算,但仍要调用visitMax.但是COMPUTE_MAX会使得ClassWriter慢10%左右,而COMPUTE_FRAMES会慢一倍。
//生成getF的代码如下
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, "pkg/Bean", "f", "I");
mv.visitInsn(IRETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
//
mv.visitCode();
mv.visitVarInsn(ILOAD, 1);
Label label = new Label();
mv.visitJumpInsn(IFLT, label);
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ILOAD, 1);
mv.visitFieldInsn(PUTFIELD, "pkg/Bean", "f", "I");
Label end = new Label(); // 这里又创建一个label,虽然不创建直接用前一个label也合法,不过最好和字节码保持一致,这样更清晰
mv.visitJumpInsn(GOTO, end);
mv.visitLabel(label);
mv.visitFrame(F_SAME, 0, null, 0, null);
mv.visitTypeInsn(NEW, "java/lang/IllegalArgumentException");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL,
"java/lang/IllegalArgumentException", "<init>", "()V");
mv.visitInsn(ATHROW);
mv.visitLabel(end);
mv.visitFrame(F_SAME, 0, null, 0, null);
mv.visitInsn(RETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();
函数也可以像类那样被修改,我们可以直接使用MethodVisitor即可。而MethodVisitor可以包含多分枝:
public MethodVisitor visitMethod(int access, String name,
String desc, String signature, String[] exceptions) {
MethodVisitor mv1, mv2;
mv1 = cv.visitMethod(access, name, desc, signature, exceptions);
mv2 = cv.visitMethod(access, "_" + name, desc, signature, exceptions);
return new MultiMethodAdapter(mv1, mv2);
}
测量类中所有方法的执行时间
public class C {
public static long timer;
public void m() throws Exception {
timer -= System.currentTimeMillis();
Thread.sleep(100);
timer += System.currentTimeMillis();
}
}
我们可以使用TraceClassVisitor来查看该类生成的字节码:使用Textifier或者使用ASMifier
GETSTATIC C.timer : J
INVOKESTATIC java/lang/System.currentTimeMillis()J
LSUB
PUTSTATIC C.timer : J
LDC 100
INVOKESTATIC java/lang/Thread.sleep(J)V
GETSTATIC C.timer : J
INVOKESTATIC java/lang/System.currentTimeMillis()J
LADD
PUTSTATIC C.timer : J
RETURN
MAXSTACK = 4
MAXLOCALS = 1
我们看到我们需要在函数开始的时候插入4行代码结束的时候插入另外4行代码。我们还需要更新最大操作栈的大小,因此我们在visitCode中添加如下代码
public void visitCode() {
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J");
mv.visitInsn(LSUB);
mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
}
我们需要添加另外4条指令,在return或者xRETURN、ATHROW之前。这些指令都没有参数,都是使用visitInsn方法访问的
public void visitInsn(int opcode) {
if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J");
mv.visitInsn(LADD);
mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
}
mv.visitInsn(opcode);
}
最后我们需要更新最大操作栈的大小。由于我们向操作栈添加了两个long型变量,因此最坏情况maxsize+4
public void visitMaxs(int maxStack, int maxLocals) {
mv.visitMaxs(maxStack + 4, maxLocals);
}
当然我们也可以依赖COMPUTE_MAX来计算。
我们的ClassVisitor可以这样来写:
public class AddTimerAdapter extends ClassVisitor {
private String owner;
private boolean isInterface;
public AddTimerAdapter(ClassVisitor cv) {
super(ASM4, cv);
}
@Override
public void visit(int version, int access, String name,
String signature, String superName, String[] interfaces) {
cv.visit(version, access, name, signature, superName, interfaces);
owner = name;
isInterface = (access & ACC_INTERFACE) != 0;
}
@Override
public MethodVisitor visitMethod(int access, String name,
String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature,
exceptions);
if (!isInterface && mv != null && !name.equals("<init>")) {
mv = new AddTimerMethodAdapter(mv);
}
return mv;
}
@Override
public void visitEnd() {
if (!isInterface) {
FieldVisitor fv = cv.visitField(ACC_PUBLIC + ACC_STATIC, "timer",
"J", null, null);
if (fv != null) {
fv.visitEnd();
}
}
cv.visitEnd();
}
}
对于label和frames而言,我们知道visitLabel的调用是在其关联代码前的。如果代码跳转到ICONST_0 IADD ,当我们把这两个指令删除后跳转后直接执行后续的代码,这正是我们期望的。而如果是跳转到IADD 我们就不能直接删除相关指令了,不过这样的话ICONST_0和IADD之前肯定有一个label.如果在两个指令之间我们访问了stack map frame, 我们也不能删除这些指令。这些case都可以通过把label和frame当做需要match的指令(我们要记录指令出现的位置及后续指令的状态)来处理。编译后的class文件同时包含了其对应源代码中的行号信息用于异常栈的处理。
很多时候,我们需要记录某一指令调用时所处的状态才能识别固定的pattern,例如如果我们想找出class中自赋值语句(f=f this.f = this.f)等就需要记录ALOAD 0的状态
class RemoveGetFieldPutFieldAdapter extends PatternMethodAdapter {
private final static int SEEN_ALOAD_0 = 1;
private final static int SEEN_ALOAD_0ALOAD_0 = 2;
private final static int SEEN_ALOAD_0ALOAD_0GETFIELD = 3;
private String fieldOwner;
private String fieldName;
private String fieldDesc;
public RemoveGetFieldPutFieldAdapter(MethodVisitor mv) {
super(mv);
}
@Override
public void visitVarInsn(int opcode, int var) {
switch (state) {
case SEEN_NOTHING: // S0 -> S1
if (opcode == ALOAD && var == 0) {
state = SEEN_ALOAD_0;
return;
}
break;
case SEEN_ALOAD_0: // S1 -> S2
if (opcode == ALOAD && var == 0) {
state = SEEN_ALOAD_0ALOAD_0;
return;
}
break;
case SEEN_ALOAD_0ALOAD_0: // S2 -> S2
if (opcode == ALOAD && var == 0) {
mv.visitVarInsn(ALOAD, 0);
return;
}
break;
}
visitInsn();
mv.visitVarInsn(opcode, var);
}
@Override
public void visitFieldInsn(int opcode, String owner, String name,
String desc) {
switch (state) {
case SEEN_ALOAD_0ALOAD_0: // S2 -> S3
if (opcode == GETFIELD) {
state = SEEN_ALOAD_0ALOAD_0GETFIELD;
fieldOwner = owner;
fieldName = name;
fieldDesc = desc;
return;
}
break;
case SEEN_ALOAD_0ALOAD_0GETFIELD: // S3 -> S0
if (opcode == PUTFIELD && name.equals(fieldName)) {
state = SEEN_NOTHING;
return;
}
break;
}
visitInsn();
mv.visitFieldInsn(opcode, owner, name, desc);
}
@Override
protected void visitInsn() {
switch (state) {
case SEEN_ALOAD_0: // S1 -> S0
mv.visitVarInsn(ALOAD, 0);
break;
case SEEN_ALOAD_0ALOAD_0: // S2 -> S0
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ALOAD, 0);
break;
case SEEN_ALOAD_0ALOAD_0GETFIELD: // S3 -> S0
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, fieldOwner, fieldName, fieldDesc);
break;
}
state = SEEN_NOTHING;
}
}
工具类
ClassVisitor中介绍的工具类在这里仍然是适用的。
Type xLOAD xADD xRETURN 均是一些类型相关的指令,而Type提供了getOpcode可以用于获取适当的指令。t.getOpcode(IMUL)会返回FMUL如果t是Type.FLOAT_TYPE
-
TraceClassVisitor 和之前用法一样,不过如若你指向打印某个方法的指令,你可以使用TraceMethodVisitor
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions); if (debug && mv != null && ...) { // if this method must be traced Printer p = new Textifier(ASM4) { @Override public void visitMethodEnd() { print(aPrintWriter); // print it after it has been visited } }; mv = new TraceMethodVisitor(mv, p); } return new MyMethodAdapter(mv); }
CheckClassAdapter,同样你也可以选择CheckMethodAdapter
ASMifier 可以用于生成产生某个类的ASM代码
AnalyzerAdapter 这个adapter主要用于计算stack map frames,其基于visitFrame.它帮助我们做frame的压缩,包括删除可快速推断出来的及重复的。
LocalVariablesSorter 这个adapter会重新计算本地符号表的大小。
AdviceAdapter 当需要在函数开始或者结束的时候插入代码可以使用这个Adapter。它会自动处理构造函数构造函数开始到调用完super是不允许插入代码的。