42 - ASM之Class Transformation总结
Class Transformation,从Core API的角度来说(第二个层次),我们介绍了asm.jar当中的ClassReader和Type两个类;同时,从应用的角度来说(第一个层次),我们也介绍了Class Transformation的原理和示例。
asm学习层次Class Transformation的原理
在Class Transformation的过程中,我们主要使用到了ClassReader、ClassVisitor和ClassWriter三个类;其中ClassReader类负责“读”Class,ClassWriter负责“写”Class,而ClassVisitor则负责进行“转换”(Transformation)。
ClassVisitor传递示意图在Java ASM当中,Class Transformation的本质就是利用了“中间人公(攻)鸡(击)”的方式来实现对已有的Class文件进行修改或转换。
中间人攻击详细的来说,我们自己定义的ClassVisitor类就是一个“中间人”,那么这个“中间人”可以做什么呢?可以做三种类型的事情:
- 对“原有的信息”进行篡改,就可以实现“修改”的效果。对应到ASM代码层面,就是对ClassVisitor.visitXxx()和MethodVisitor.visitXxx()的参数值进行修改。
- 对“原有的信息”进行扔掉,就可以实现“删除”的效果。对应到ASM代码层面,将原本的FieldVisitor和MethodVisitor对象实例替换成null值,或者对原本的一些ClassVisitor.visitXxx()和MethodVisitor.visitXxx()方法不去调用了。
- 伪造一条“新的信息”,就可以实现“添加”的效果。对应到ASM代码层面,就是在原来的基础上,添加一些对于ClassVisitor.visitXxx()和MethodVisitor.visitXxx()方法的调用。
ASM能够做哪些转换操作
在类层面所做的修改,主要是通过ClassVisitor类来完成。我们将类层面可以修改的信息,分成以下三个方面:
- 类自身信息:修改当前类、父类、接口的信息,通过ClassVisitor.visit()方法实现。
- 字段:添加一个新的字段、删除已有的字段,通过ClassVisitor.visitField()方法实现。
- 方法:添加一个新的方法、删除已有的方法,通过ClassVisitor.visitMethod()方法实现。
public class HelloWorld extends Object implements Cloneable {
public int intValue;
public String strValue;
public int add(int a, int b) {
return a + b;
}
public int sub(int a, int b) {
return a - b;
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
为了让大家更明确的知道需要修改哪一个visitXxx()方法的参数,我们做了如下总结:
- ClassVisitor.visit(int version, int access, String name, String signature, String superName, String[] interfaces)
- version: 修改当前Class版本的信息
- access: 修改当前类的访问标识(access flag)信息。
- name: 修改当前类的名字。
- signature: 修改当前类的泛型信息。
- superName: 修改父类。
- interfaces: 修改接口信息。
ClassVisitor.visitField(int access, String name, String descriptor, String - signature, Object value)
- access: 修改当前字段的访问标识(access flag)信息。
- name: 修改当前字段的名字。
- descriptor: 修改当前字段的描述符。
- signature: 修改当前字段的泛型信息。
- value: 修改当前字段的常量值。
ClassVisitor.visitMethod(int access, String name, String descriptor, String signature, String[] exceptions)
- access: 修改当前方法的访问标识(access flag)信息。
- name: 修改当前方法的名字。
- descriptor: 修改当前方法的描述符。
- signature: 修改当前方法的泛型信息。
- exceptions: 修改当前方法可以招出的异常信息。
再有,如何删除一个字段或者方法呢?其实很简单,我们只要让中间的某一个ClassVisitor在遇到该字段或方法时,不向后传递就可以了。在具体的代码实现上,我们只要让visitField()或visitMethod()方法返回一个null值就可以了。
最后,如何添加一个字段或方法呢?我们只要让中间的某一个ClassVisitor向后多传递一个字段和方法就可以了。在具体的代码实现上,我们是在visitEnd()方法完成对字段或方法的添加,而不是在visitField()或visitMethod()当中添加。因为我们要避免“一个类里有重复的字段和方法出现”,在visitField()或visitMethod()方法中,我们要判断该字段或方法是否已经存在;如果该字段或方法不存在,那我们就在visitEnd()方法进行添加;如果该字段或方法存在,那么我们就不需要在visitEnd()方法中添加了。
方法体层面的修改
在方法体层面所做的修改,主要是通过MethodVisitor类来完成。
在方法体层面的修改,更准确的地说,就是对方法体内包含的Instruction进行修改。就像数据库的操作“增删改查”一样,我们也可以对Instruction进行添加、删除、修改和查找。
为了让大家更直观的理解,我们假设有如下代码:
public class HelloWorld {
public int test(String name, int age) {
int hashCode = name.hashCode();
hashCode = hashCode + age * 31;
return hashCode;
}
}
其中,test()方法的方法体包含的Instruction内容如下:
public test(Ljava/lang/String;I)I
ALOAD 1
INVOKEVIRTUAL java/lang/String.hashCode ()I
ISTORE 3
ILOAD 3
ILOAD 2
BIPUSH 31
IMUL
IADD
ISTORE 3
ILOAD 3
IRETURN
MAXSTACK = 3
MAXLOCALS = 4
有的时候,我们想实现某个功能,但是感觉无从下手。这个时候,我们需要解决两个问题。第一个问题,就是要明确需要修改什么?第二个问题,就是“定位”方法,也就是要使用哪个方法进行修改。我们可以结合这两个问题,和下面的示例应用来理解。
- 添加
- 在“方法进入”时和“方法退出”时,
- 打印方法参数和返回值
- 方法计时
- 在“方法进入”时和“方法退出”时,
- 删除
- 移除NOP
- 移除打印语句、加零、字段赋值
- 清空方法体
- 修改
- 替换方法调用(静态方法和非静态方法)
- 查找
- 当前方法调用了哪些方法
- 当前方法被哪些方法所调用
由于MethodVisitor类里定义了很多的visitXxxInsn()方法,我们就不详细介绍了。但是,大家可以的看一下 asm4-guide.pdf的一段描述:
Methods can be transformed, i.e. by using a method adapter that forwards the method calls it receives with some modifications:
- changing arguments can be used to change individual instructions,
- not forwarding a received call removes an instruction,
- and inserting calls between the received ones adds new instructions.
需要要注意一点:无论是添加instruction,还是删除instruction,还是要替换instruction,都要保持operand stack修改前和修改后是一致的。
小结
本文内容总结如下:
- 第一点,希望大家可以理解Class Transformation的原理。
- 第二点,在Class Transformation中,ASM究竟能够帮助我们修改哪些信息。