您的位置:首页 > 编程语言 > Java开发

JAVA字节码增强技术之ASM分析2

2017-03-17 10:10 288 查看
3、使用ASMAPI生成和转换class中的方法

前面已经简单介绍过已编译的class文件中的方法是由字节码指令序列构成,因此使用ASMAPI生成和转换class文件中的方法需要具备基本的字节码指令知识和基本的字节码指令运行原理。

3.1、执行模型

我们知道java代码是运行在java虚拟机的线程中,每一个线程拥有各自的执行栈,执行栈由多个帧组成。每个帧可以代表一个方法的运行过程,当方法被触发时,一个新的帧将会被push到当前线程的执行栈中。当方法正常返回或者发生异常,当前线程会将该方法帧pop出执行栈。

每个帧由本地变量和操作栈两部分组成。本地变量部分由一个数组组成,可通过数据下标访问变量。操作栈部分是由一组字节码指令序列组成,指令序列将按照后进先出的顺序访问。每一个在线程执行栈中的帧都包括它自己的操作栈。

帧中的本地变量数组和操作栈的大小由类中方法的代码决定,在类编译过程中计算帧的内容并将它以字节码指令的方式储存在已编译的class文件中。在同一方法的执行过程中,该方法对应的每一帧的大小相同,但是不同的方法帧大小可以不同,它们的大小由本地变量数组和操作栈部分的大小决定。

下图展示了一个执行栈中帧的例子,帧1包含3个本地变量,它的操作栈的最大容量为4;帧2包含2个本地变量,它的操作栈中有2个值;同样的,帧3中包含4个本地变量和2个操作栈值;



图3.1一个执行栈中的3个帧

当非静态(static)方法被创建时,执行栈初始化一个新的帧,该帧初始化它的操作栈并且将this和方法的参数压入到它的本地变量数组中。比如执行a.equals(b)时,线程执行栈中初始化一个帧,初始化帧的操作栈并且将a,b参数压入到帧的本地变量数组中。任何java类型的变量都能被加入到帧的本地变量数组中,但是有别于其他类型的变量只在本地变量数组中占一个单位的位置,long和double类型有点特别,在加入后它们在数组中各占2个单位的位置。因此第i个方法的参数不一定存储在帧本地变量数组的第i个位置。比如Math.max(1L,2L),在本地变量数组中占4个单位的位置,1L和2L各占2个单位的位置,本地变量数组的长度为4。
3.2、字节码指令
一个字节码指令由操作码和固定数量个参数组成。操作码是一个非负整数,因此字节码指令的名字由一个数字来标识。比如操作码0使用符号NOP来助记,其中NOP指令不做任何事情。操作码后一般跟参数,用来定义指令具体的行为,比如助记符号GOTO(操作码167)后跟的参数用来指定下一条要执行的指令位置。
字节码指令主要分成两类,一类用来对变量进行从帧本地变量数组到操作栈之间的正反向转化;另一类在操作栈中对变量进行操作,比如从栈中pop出一个值,做某种计算后再重新把该值push回操作栈中。
1、用来进行做转换的指令:ILOAD,LLOAD, FLOAD, DLOAD,和ALOAD指令用来读取帧本地变量数组中的值然后push到操作栈;ILOAD指令用来读取boolean,byte, char, short或者int类型的变量;LLOAD,FLOAD, DLOAD指令用来读取long,float, double类型的变量;ALOAD指令用来读取非原始类型变量,比如对象,数组的引用等;同时ISTORE,LSTORE, FSTORE, DSTORE和ASTORE指令用来将相应类型的变量从操作栈中pop,然后存储到帧本地变量数组中。
2、用来在操作栈中运算的指令;
l 栈操作:POP, PUSH, DUP, SWAP
l 常量:ACONST_NULL(push null到操作栈),ICONST_0(pushint类型的0到操作栈),FCONST_0,DCONST_0,BIPUSHb(push byte类型的b到操作栈),SIPUSHs(push short类型的s到操作栈),LDCcst(push int,float,long,double,String,class常量到操作栈)
l 算数逻辑,从操作栈中pop出值做完相应的算数逻辑运算后再push回操作栈,涉及到的指令有:xADD,xSUB, xMUL, xDIV和xREM(分别对应+,-,*,/,%计算逻辑)
l 类型转换,从操作栈中pop出值做完相应的类型转换后再push回操作栈,涉及到的指令有:I2F,F2D, L2D(分别对应int转float,float转double,long转double操作),CHECKCASTt(将原引用类型转换成t类型)
l 对象,创建,锁定,测试一个对象:NEW type(push 一个新的type类型的对象到操作栈中)
l fields:GETFIELD owner name desc(pop对象的引用,pushname的值到操作栈中),PUTFIELDowner name desc(pop对象的值和引用,将值存储到name的field),GETSTATIC和PUTSTATIC指令用来操作静态对象,作用一样;
l 方法,该类指令用来触发一个方法或者构造函数,先pop出对应数量的参数,方法运算后,push方法的返回值到操作栈中:INVOKEVIRTUALowner name desc(指令用来触发ownerclass中的name方法),INVOKESTATICowner name desc(指令用来触发静态方法),同理INVOKESPECIAL指令用来触发类中的私有方法和构造函数,INVOKEINTERFACE指令用来触发接口方法
l 数组,用来读写数组中的值;xALOAD(pop index和array,push array中index个位置值到操作栈中),xASTORE(popindex, value和array,存储value到array中的index个位置上)
l 条件判断:IFEQ label(pop出一个int值然后判断是否为0,若为0则跳转到label位置),同理IFGE,IFNE,TABLESWITCH,LOOKUPSWITCH
l 返回,终止一个方法的执行并返回结果给caller:xRETURN和RETURN分别返回对应类型的值和void

3.3、例子
package pkg;
public class Bean {
private int f;
public int getF() {
return this.f;
}
public void setF(int f) {
this.f = f;
}
}


1)、getF方法的字节码指令:

ALOAD 0
GETFIELD pkg/Bean f I
IRETURN


具体执行过程:首先从帧本地变量数组中读取下标为0的值(初始化帧时设置为this)push到操作栈中,然后pop出this,pushthis的pkg/Bean类f对象到操作栈中(this.f),最后从操作栈中pop出值并返回给caller,该帧每阶段的变化如下图所示。



图3.2(a)初始化状态,(b)执行ALOAD0后,(c)执行GETFIELD指令后

2)、setF方法的字节码指令:

ALOAD 0
ILOAD 1
PUTFIELD pkg/Bean f I
RETURN

具体执行过程:第一条指令从帧本地变量数组中读取下标为0的值(初始化帧时设置为this)push到操作栈中;第二条指令从帧本地变量数组中读取下标为1的值push到操作栈中用作赋值的int值;第三条指令pop出this和int值, 存储pkg/Bean类f对象的引用(也就是this.f)到本地变量数组中,第四条指令返回void到caller,该帧每阶段的变化如下图所示。



图3.3(a)初始化状态,(b)执行ALOAD0后,(c)执行LOAD1后,(c)执行PUTFIELD指令后

3)、Bean的默认构造函数Bean(){ super(); }字节码指令:
ALOAD 0
INVOKESPECIAL java/lang/Object <init> ()V
RETURN

具体执行过程:第一条指令从帧本地变量数组中读取下标为0的值(初始化帧时设置为this)push到操作栈中;第二条指令pop出this调用java/lang/Object类的<init>方法;第三条指令返回void到caller。

3.4、更复杂的例子

public void checkAndSetF(int f) {
if (f >= 0) {
this.f = f;
} else {
throw new IllegalArgumentException();
}
}

字节码指令如下:

ILOAD 1                   //从本地变量数组中读入下标1的int变量f
IFLT label                  //判断f<0 跳label标记处
ALOAD 0                 //从本地变量数组中读入下标0的this
ILOAD 1                 //读入下标1的参数f
PUTFIELD pkg/Bean f I   //pop this和f值,赋值给this.f,存储到本地变量数组中
GOTO end                    //跳end标记处
label:
NEW java/lang/IllegalArgumentException  //new一个exception,push到操作栈
DUP                     //复制一份操作栈中的内容
INVOKESPECIAL java/lang/IllegalArgumentException <init> ()V       //pop出操作栈中的一份拷贝内容,call java/lang/IllegalArgumentException的构造函数
ATHROW                  //pop出剩下的一份操作栈拷贝内容,然后抛出一个异常,这样就不会再执行下面的指令
end:
RETURN                  //返回void给caller


3.4.1、捕获异常的例子

public static void sleep(long d) {
try {
Thread.sleep(d);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

字节码指令如下:

TRYCATCHBLOCK try catch catch java/lang/InterruptedException  //指定一个exception的handler用来处理try和catch label之间的指令
try:
LLOAD 0
INVOKESTATIC java/lang/Thread sleep (J)V
RETURN
catch:
INVOKEVIRTUAL java/lang/InterruptedException printStackTrace ()V
RETURN


3.5、ASM接口和组件

使用ASMAPI产生和转换class文件的方法是基于MethodVisitor抽象类进行的,其中MethodVisitor通过ClassVisitor的visitMethod方法返回。MethodVisitor类中定义了许多访问class文件的方法内容和属性的方法;其中,使用继承自MethodVisitor的类对class文件的方法内容和属性的访问顺序如下;class文件的方法的注释和属性被先访问,随后才是访问目标method的bytecode;另外,visitCode和visitMaxs方法可以被用来判断class文件方法字节码指令序列的开始和结束;

visitAnnotationDefault?
( visitAnnotation | visitParameterAnnotation | visitAttribute )*
( visitCode
( visitTryCatchBlock | visitLabel | visitFrame | visitXxxInsn |
visitLocalVariable | visitLineNumber )*
visitMaxs )?
visitEnd


MethodVisitor类的主要方法:

abstract class MethodVisitor { // public accessors ommited MethodVisitor(int api);
MethodVisitor(int api, MethodVisitor mv);
AnnotationVisitor visitAnnotationDefault();
AnnotationVisitor visitAnnotation(String desc, boolean visible); AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible);
void visitAttribute(Attribute attr);
void visitCode();
void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack);
void visitInsn(int opcode);
void visitIntInsn(int opcode, int operand);
void visitVarInsn(int opcode, int var);
void visitTypeInsn(int opcode, String desc);
void visitFieldInsn(int opc, String owner, String name, String desc); void visitMethodInsn(int opc, String owner, String name, String desc); void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs);
void visitJumpInsn(int opcode, Label label);
void visitLabel(Label label);
void visitLdcInsn(Object cst);
void visitIincInsn(int var, int increment);
void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels); void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels);
void visitMultiANewArrayInsn(String desc, int dims);
void visitTryCatchBlock(Label start, Label end, Label handler, String type);
void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index);
void visitLineNumber(int line, Label start); void visitMaxs(int maxStack, int maxLocals); void visitEnd();
}


ClassVisitor和MethodVisitor结合起来使用,产生一个class文件的大概过程;

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

ClassReader、ClassWriter和MethodVisitor结合起来使用,转换一个class文件方法;ClassReader载入class文件,在调visitMethod方法时会返回一个继承自MethodVisitor类的对象,在这里用自己实现的MethodVisitor子类对象作为返回值返回即可,其中,在自己的类中实现各种对目标class方法的指令转换工作;最后ClassWriter输出新的class字节码文件。

3.6、产生方法

package pkg;
public class Bean {
private int f;
public int getF() {
return this.f;
}
public void setF(int f) {
this.f = f;
}
}

产生getF()方法:(方法的字节码指令请见3.3.1)

mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, "pkg/Bean", "f", "I");
mv.visitInsn(IRETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();


public void checkAndSetF(int f) {
if (f >= 0) {
this.f = f;
} else {
throw new IllegalArgumentException();
}
}


产生checkAndSetF方法:(方法的字节码指令请见3.4)

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


3.7、转换方法

去掉class方法(非构造函数)中NOP指令的例子

实现继承自MethodVisitor的类适配器(methodadapter)

public class RemoveNopAdapter extends MethodVisitor {
public RemoveNopAdapter(MethodVisitor mv) {
super(ASM4, mv);
}
@Override
public void visitInsn(int opcode) {
if (opcode != NOP) {
mv.visitInsn(opcode);
}
}
}


实现继承自ClassVisitor的类适配器(classadapter)

public class RemoveNopClassAdapter extends ClassVisitor {
public RemoveNopClassAdapter(ClassVisitor cv) {
super(ASM4, cv);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv;
mv = cv.visitMethod(access, name, desc, signature, exceptions);
if (mv != null && !name.equals("<init>")) {
mv = new RemoveNopAdapter(mv);
}
return mv;
}
}


class文件的方法转换时序图;



图3.4RemoveNopAdapter的时序图

3.8、无状态的转换(当前指令执行状态不依赖其他指令状态,不需要在methodAdapter中保存指令状态)

为class文件中方法增加计时器的例子;

实现效果相当:

public class C {
public void m() throws Exception {
Thread.sleep(100);
}
}

<strong><span style="color:#ff0000;">转换成</span></strong>
public class C {
public static long timer;
public void m() throws Exception {
timer -= System.currentTimeMillis();
Thread.sleep(100);
timer += System.currentTimeMillis();
}
}

AddTimerAdapter的实现;

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

class AddTimerMethodAdapter extends MethodVisitor {
public AddTimerMethodAdapter(MethodVisitor mv) {
super(ASM4, mv);
}
@Override 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");
}
@Override 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);
}
@Override public void visitMaxs(int maxStack, int maxLocals) {
mv.visitMaxs(maxStack + 4, maxLocals);
}
}
}


3.9、有状态的转换(本指令依赖上个指令的内容)

例子,去掉ALOAD0 ALOAD 0 GETFIELD f PUTFIELD f这样的指令
状态机图如下;



图3.5 ALOAD0 ALOAD 0 GETFIELD f PUTFIELD f指令的状态机

代码实现:
public abstract class PatternMethodAdapter extends MethodVisitor {
...
@Override public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) {
visitInsn();
mv.visitFrame(type, nLocal, local, nStack, stack);
}
@Override public void visitLabel(Label label) { visitInsn();
mv.visitLabel(label);
}
@Override public void visitMaxs(int maxStack, int maxLocals) { visitInsn();
mv.visitMaxs(maxStack, maxLocals);
}
}

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;
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息