史上最全的ASM原理解析与应用
ASM简介
- ASM是一个操作Java字节码类库,其操作的对象是字节码数据,处理字节码方式是“拆分-修改-合并”
- 将.class文件拆分成多个部分;
- 对某一个部分的信息进行修改;
- 将多个部分重新组织成一个新的.class文件;
- 版本发展:Java语言在不断发展,那么,ASM版本也要不断发展来跟得上Java的发展;在选择ASM版本的时候,要注意它支持的Java版本,来确保兼容性;
- 学习目标:使用ASM实现1)无状态转换:查找方法;替换方法; 2)有状态转换:获取Hybrid使用Action集合,RN使用modules集合数据;
- 既然要操作字节码数据,我们需要了解字节码存储结构:The class File Format
ClassFile
- 既然ASM操作的是.class文件,则我们需要了解class文件结构:在.class文件中,存储的是ByteCode数据,但是,这些ByteCode数据并不是杂乱无章的,而是遵循一定的数据结构,这些结构定义在 Java Virtual Machine Specification中的The class File Format,如下所示。
ClassFile {
u4 magic; //魔数
u2 minor_version; //次版本号
u2 major_version; //主版本号
u2 constant_pool_count; //常量池容量:从1开始,0:不引用任何一个常量池数据
cp_info constant_pool[constant_pool_count-1]; //常量数据,数据构成有17种,以首位u1表示tag类型
u2 access_flags; //访问标记,识别类或接口的访问信息如:ACC_PUBLIC;ACC_ABSTRACT,由于每个标记占用二进制位不同,使用|表示交集;
u2 this_class; //当前类索引:常量池中偏移量指向一个类型为CONSTANT_Class_info的类描述符常量
u2 super_class; //当前类的父类索引:java单继承机制
u2 interfaces_count; //当前类实现的接口计数器
u2 interfaces[interfaces_count]; //接口索引表:常量池中偏移量
u2 fields_count; //字段计数器
field_info fields[fields_count]; //字段表:由类级变量static和实例变量(全局),不包括局部变量
u2 methods_count; //方法数
method_info methods[methods_count]; //方法表
u2 attributes_count; //属性数
attribute_info attributes[attributes_count]; //属性表
}
- u* : 表示占用字节数,有u1,u2,u4,u8等构成;
字段表
- field_info字段表格式如下:
field_info {
u2 access_flags; //字段的访问标记
u2 name_index; //字段的名称
u2 descriptor_index; //字段的描述符
u2 attributes_count; //字段属性
attribute_info attributes[attributes_count];
}
-
全限定名:类的全限定名是将类全名的
.
全部替换为/
,如java.lang.String ,全限定名java/lang/String -
简单名称:方法main() 简单名称为main,全局变量num为名称num
-
描述符:基本数据类型及void的由大写字母表示,对象类型有L+全限定名=> Ljava/lang/String;表示String字段描述符;对于数组类型根据维度在前面加上“[”,如int[] => [I ; String[] => [Ljava/lang/String;
desc_asm.png -
attribute_info:字段的属性表,存储一些额外信息;
方法表
- 方法表格式如下,方法的描述与字段的描述采用了几乎完全一致的方式
method_info {
u2 access_flags; //访问标记
u2 name_index; //方法简单名称常量池索引
u2 descriptor_index; //方法描述符索引
u2 attributes_count; //方法属性表
attribute_info attributes[attributes_count];
}
- 注意class文件中编译器添加的实例构造器<init> 和类构造器<clint>两个方法
属性表
- Class文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息,参考如下事例表示:
public class HelloWorld {
public void foo() {
int i = 0;
int j = 0;
if (i > 0) {
int k = 0;
}
int l = 0;
}
}
//使用javap -v 获取foo字节码code数据如下:
public void foo();
descriptor: ()V //方法描述符
flags: (0x0001) ACC_PUBLIC //方法访问标记
Code: 033C033D 1B9E0005 033E033E B1
stack=1, locals=4, args_size=1 //stack:操作数栈深度,locals:局部变量表,args_size:方法参数的个数,包括方法参数、this
0: iconst_0
1: istore_1
2: iconst_0
3: istore_2
4: iload_1 // 1B
/**
* ifle 字节码 9E -->后面u2类型字段为 0005 表示偏移量,当前5 + 偏移量 = 10
* 10由code字节码偏移量6,7位置,code属性中字节码长度由u4表示,
* 但是《Java虚拟机规范》中明确限制了一个方法不允许超过65535条字节码指令,即实际只使用了u2的长度,如果超过这个限制,Javac编译器就会拒绝编译,因此这里使用u2表示跳转字节码偏移量
*/
5: ifle 10 //9E 0005
8: iconst_0 //03
9: istore_3
10: iconst_0
11: istore_3
12: return
LineNumberTable: //源码行数与code中偏移量对应关系,常用于log中输出日志
line 11: 0
line 12: 2
line 13: 4
line 14: 8
line 18: 10
line 20: 12
LocalVariableTable: //描述栈帧中局部变量表的变量与Java源码中定义的变量之间的关系
//start:局部变量的生命周期开始的字节码偏移量
//Length:其作用范围覆盖的长度,两者结合起来就是这个局部变量在字节码之中的作用域范围
//Slot: 局部变量表位置:对应上方最大locals=4,根据字节码可以验证stack最大为1
Start Length Slot Name Signature
0 13 0 this Lsample/HelloWorld;
2 11 1 i I
4 9 2 j I
12 1 3 l I
StackMapTable: number_of_entries = 1 //栈映射帧Stack Map Frame个数:1
//虚拟机类加载的字节码验证阶段被新类型检查验证器使用,目的代替以前比较消耗性能的基于数据流分析的类型推导验证器;其中记录的是一个方法中操作数栈与局部变量区的类型在一些特定位置的状态。
frame_type = 253 /* append */ //基本块开头处的状态:frame_type = 251,表示多了2个局部变量,append 增加变量,chop 减少变量
offset_delta = 10 //栈映射帧的code偏移量为10,记录的是if跳转语句
locals = [ int, int ] //增量设置,进入时有默认Frame局部变量区:[this],在10位置变量k已经过了作用域局部变量区:[ this, int, int],增量为 locals = [ int, int ]
- 这里我们主要关注StackMapTable中包含的栈映射桢:它有什么用呢?
- 在使用ASM的classWriter修改字节码时构造函数如下,flags属性一般使用COMPUTE_FRAMES,其于COMPUTE_MAXS,默认0有何区别呢?
public ClassWriter(ClassReader classReader, int flags)
- 默认值0:ASM即不会自动计算max stacks和max locals,也不会自动计算stack map frames;
- COMPUTE_MAXS:ASM只会自动计算max stacks和max locals,但不会自动计算stack map frames;
- COMPUTE_FRAMES:ASM即会自动计算max stacks和max locals,也会自动计算stack map frames;
ASM组成
- 组成结构上来说,ASM分成两部分,一部分为Core API,另一部分为Tree API
- Core API包括asm.jar、asm-util.jar和asm-commons.jar
- Tree API包括asm-tree.jar和asm-analysis.jar
-
我们常用的是asm.jar中的ClassReader,classVisitor,ClassWrite这三个类,他们的关系如下:
asm_relation_chart.png
ClassReader拆分
- 主要负责读取.class文件里的内容,然后拆分成各个不同的部分;如何实现呢?
public class ClassReader {
//真实的数据部分
final byte[] classFileBuffer;
//数据的索引信息:标识了classFileBuffer中数据里包含的常量池位置
private final int[] cpInfoOffsets;
//标记访问标识(access flag)在classFileBuffer中的位置信息
public final int header;
/**
* 全局变量初始化
* classFileOffset :默认为0,起始位置
*/
ClassReader(byte[] classFileBuffer, int classFileOffset, boolean checkClassVersion) {
this.classFileBuffer = classFileBuffer; //class文件数据字节数组
this.b = classFileBuffer;
if (checkClassVersion && this.readShort(classFileOffset + 6) > 60) {
throw new IllegalArgumentException("Unsupported class file major version " + this.readShort(classFileOffset + 6));
} else {
//读取第8个字节位置:常量池大小constant_pool_count
int constantPoolCount = this.readUnsignedShort(classFileOffset + 8);
this.cpInfoOffsets = new int[constantPoolCount];
//当前常量池起始位置:注意ClassFile由1开始,保留0位置用于未指定任何数据
int currentCpInfoIndex = 1;
//起始偏移量,首位常量池位置:(魔数u4,次版本号u2,主版本号u2,常量池大小u2)
int currentCpInfoOffset = classFileOffset + 10; //从第10个字节开始保存常量
//当前各个常量的偏移量
int cpInfoSize;
for(hasConstantDynamic = false; currentCpInfoIndex < constantPoolCount; currentCpInfoOffset += cpInfoSize) {
/**
* 常量池的数据:u1:表示当前数据类型
CONSTANT_Utf8_info {
u1 tag; // == 1
u2 length;
u1 bytes[length];
}
*/
this.cpInfoOffsets[currentCpInfoIndex++] = currentCpInfoOffset + 1; //去掉u1数据类型保存常量数据
switch(classFileBuffer[currentCpInfoOffset]) { //currentCpInfoOffset记录的为当前常量类型tag
case 1: //字符串计算字符串长度作为偏移量cpInfoSize
//tag:u1 length:u2 = 3 加上Short位的length表示bytes数组长度的
cpInfoSize = 3 + this.readUnsignedShort(currentCpInfoOffset + 1);
...
break;
...
}
/**
* currentCpInfoOffset:常量池数据已经全部遍历完存入cpInfoOffsets中,此时位置为:access_flags
*/
this.header = currentCpInfoOffset;
- int[] cpInfoOffsets:由class文件往后读取8个字节,在classFile中可知(魔数u4,次版本号u2,主版本号u2)constant_pool_count即常量池大小为cpInfoOffsets数组大小,数组中数据为当前常量在classFile中的偏移量,用于快速获取常量;
- header:存储当前类的access_flags标识位在字节码数组中位置:快速定位到当前类,父类,字段,方法等数据内容,如何验证,看一下accept()方法:
public void accept(ClassVisitor classVisitor, Attribute[] attributePrototypes, int parsingOptions) {
...
int currentOffset = this.header; //currentOffset指定位置为:u2 access_flags
int accessFlags = this.readUnsignedShort(currentOffset); //获取类标识位 Short
String thisClass = this.readClass(currentOffset + 2, charBuffer); // +2获取当前类索引 this_class
String superClass = this.readClass(currentOffset + 4, charBuffer); // +4 获取当前父类索引 super_class
String[] interfaces = new String[this.readUnsignedShort(currentOffset + 6)]; //接口集合数据:大小 + 6
currentOffset += 8; //u2:access_flags, u2:this_class, u2:super_class , u2:interfaces_count
//获取实现接口数据
int innerClassesOffset;
for(innerClassesOffset = 0; innerClassesOffset < interfaces.length; ++innerClassesOffset) {
interfaces[innerClassesOffset] = this.readClass(currentOffset, charBuffer);
currentOffset += 2; //interfaces[] 数组:数据类型u2
}
...
//获取属性表位置:attribute_info
int currentAttributeOffset = this.getFirstAttributeOffset();
//属性表个数:attributes_count
int fieldsCount;
for(fieldsCount = this.readUnsignedShort(currentAttributeOffset - 2); fieldsCount > 0; --fieldsCount) {
String attributeName = this.readUTF8(currentAttributeOffset, charBuffer);
int attributeLength = this.readInt(currentAttributeOffset + 2);
currentAttributeOffset += 6;
...
if ("Signature".equals(attributeName)) { //若当前类属性有泛型,则读取其信息
signature = this.readUTF8(currentAttributeOffset, charBuffer);
}
...
currentAttributeOffset += attributeLength;
}
//调用visit方法,每个类只会调用一次,参数为我们读取到字节码数据
classVisitor.visit(this.readInt(this.cpInfoOffsets[1] - 7), accessFlags, thisClass, signature, superClass, interfaces);
...
//获取filed字段个数
fieldsCount = this.readUnsignedShort(currentOffset);
//readField() 调用classVisitor.visitField()方法
for(currentOffset += 2; fieldsCount-- > 0; currentOffset = this.readField(classVisitor, context, currentOffset)) {}
//获取method方法个数
methodsCount = this.readUnsignedShort(currentOffset);
//readMethod() 调用classVisitor.visitMethod()方法
for(currentOffset += 2; methodsCount-- > 0; currentOffset = this.readMethod(classVisitor, context, currentOffset)) {}
classVisitor.visitEnd();
- accept()方法调用:
- 根据header快速获取类标识位,当前类索引,父类索引,接口数据,属性表等数据后调用classVisitor.visit()方法,这也就解释了为何classVisitor.visit()及classVisitor.visitEnd()只会调用一次,且一个在前,一个在最后调用;
- 根据字段个数,调用readField中对字段解析调用classVisitor.visitField()方法
- 根据方法个数,调用readMethod中对字段解析调用classVisitor.visitMethod()方法
private int readField(ClassVisitor classVisitor, Context context, int fieldInfoOffset) {
//descriptor :字段描述符 int:I ; constantValue :字段默认值
FieldVisitor fieldVisitor = classVisitor.visitField(accessFlags, name, descriptor, signature,constantValue);
...
fieldVisitor.visitEnd();
return currentOffset;
}
private int readMethod(ClassVisitor classVisitor, Context context, int methodInfoOffset) {
//调用classVisitor.visitMethod()扫描类中每一个方法
MethodVisitor methodVisitor = classVisitor.visitMethod(context.currentMethodAccessFlags, context.currentMethodName, context.currentMethodDescriptor, signatureIndex == 0 ? null : this.readUtf(signatureIndex, charBuffer), exceptions);
...
//获取方法注解
if (annotationDefaultOffset != 0) {
AnnotationVisitor annotationVisitor = methodVisitor.visitAnnotationDefault();
this.readElementValue(annotationVisitor, annotationDefaultOffset, (String)null, charBuffer);
if (annotationVisitor != null) {
annotationVisitor.visitEnd();
}
}
...
//如果方法存在code属性
if (codeOffset != 0) {
methodVisitor.visitCode();
this.readCode(methodVisitor, context, codeOffset);
}
methodVisitor.visitEnd();
return currentOffset;
}
- MethodVisitor:调用顺序由classVisitor.accept() -> classVisitor.visitMethod() -> methodVisitor.visitCode() -> readCode() ->methodVisitor.visitEnd()
- methodVisitor.visitCode()方法起始位置调用,methodVisitor.visitEnd()方法结束时调用,且只会调用一次
- readCode() 读取code中属性值调用MethodVisitor对应方法
private void readCode(MethodVisitor methodVisitor, Context context, int codeOffset) {
byte[] classBuffer = this.classFileBuffer;
//常用加载字符串字节码命令编号:0x12 -> 18 ldc 表示int、float或String型常量从常量池推送至栈顶
case 18:
//调用visitLdcInsn获取常量池中数据readConst读取utf-8字符串
methodVisitor.visitLdcInsn(this.readConst(classBuffer[currentOffset + 1] & 255, charBuffer));
currentOffset += 2;
break;
...
case 178: //0xb2 getstatic
case 179: //0xb3 putstatic
case 180: //0xb4 getfield
case 181: //0xb5 putfield
case 182: //0xb6 invokevirtual
case 183: //0xb7 invokespecial
case 184: //0xb8 invokestatic
case 185: //0xb9 invokeinterface
typeAnnotationOffset = this.cpInfoOffsets[this.readUnsignedShort(currentOffset + 1)];
targetType = this.cpInfoOffsets[this.readUnsignedShort(typeAnnotationOffset + 2)];
annotationDescriptor = this.readClass(typeAnnotationOffset, charBuffer);
descriptor = this.readUTF8(targetType, charBuffer);
signature = this.readUTF8(targetType + 2, charBuffer);
if (startPc < 182) {
//如果字节码是对字段操作,调用methodVisitor.visitFieldInsn
methodVisitor.visitFieldInsn(startPc, annotationDescriptor, descriptor, signature);
} else {
//如果字节码是对方法操作,则调用methodVisitor.visitMethodInsn
boolean isInterface = classBuffer[typeAnnotationOffset - 1] == 11;
methodVisitor.visitMethodInsn(startPc, annotationDescriptor, descriptor, signature, isInterface);
}
if (startPc == 185) {
currentOffset += 5;
} else {
currentOffset += 3;
}
break;
...
//对方法栈桢中的最大操作数栈和最大局部变量表进行赋值
methodVisitor.visitMaxs(maxStack, maxLocals);
}
- 对方法code中的字节码进行解析,调用methodVisitor对应方法,最后调用 methodVisitor.visitMaxs 设置栈桢数据,若ClassWriter中设置了COMPUTE_FRAMES属性,则visitMaxs设置无效;
- 综上:classReader对.class文件读取调用accept(ClassVisitor cv)拆分成各个不同部分,将传递给cv对应方法,其可以负责将各个不同的部分重新组合成一个完整的.class文件;
ClassWriter组合
- 将各个不同的部分重新组合成一个完整的.class文件,如何实现呢?
public class ClassWriter extends ClassVisitor {
public static final int COMPUTE_MAXS = 1;
public static final int COMPUTE_FRAMES = 2;
private int version; //版本号
private final SymbolTable symbolTable; //常量池信息
private int accessFlags; //标识位
private int thisClass; //当前类索引
private int superClass; //当前类父类索引
private int interfaceCount; //接口数据
private int[] interfaces;
private FieldWriter firstField; //字段表
private FieldWriter lastField;
private MethodWriter firstMethod; //方法表
private MethodWriter lastMethod;
private Attribute firstAttribute; //属性表
//通过构造函数封装为SymbolTable对象:主要是解析类信息中,主要是常量池信息
public ClassWriter(ClassReader classReader, int flags) {
super(589824);
this.symbolTable = classReader == null ? new SymbolTable(this) : new SymbolTable(this, classReader);
}
public final void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
this.version = version;
this.accessFlags = access;
//根据类名全限定名获取在常量池中下标
this.thisClass = this.symbolTable.setMajorVersionAndClassName(version & '\uffff', name);
if (signature != null) {
this.signatureIndex = this.symbolTable.addConstantUtf8(signature);
}
this.superClass = superName == null ? 0 : this.symbolTable.addConstantClass(superName).index;
if (interfaces != null && interfaces.length > 0) {
this.interfaceCount = interfaces.length;
this.interfaces = new int[this.interfaceCount];
for(int i = 0; i < this.interfaceCount; ++i) {
this.interfaces[i] = this.symbolTable.addConstantClass(interfaces[i]).index;
}
}
}
//字段及方法通过链表连接,由firstField -> lastField; firstMethod -> lastMethod
public final FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
FieldWriter fieldWriter = new FieldWriter(this.symbolTable, access, name, descriptor, signature, value);
if (this.firstField == null) {
this.firstField = fieldWriter;
} else {
this.lastField.fv = fieldWriter;
}
return this.lastField = fieldWriter;
}
public final MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
...
}
//属性表也是通过链表
public final void visitAttribute(Attribute attribute) {
attribute.nextAttribute = this.firstAttribute;
this.firstAttribute = attribute;
}
}
- ClassWriter通过构造函数将传入的ClassVisitor信息解析封装为SymbolTable对象并将用到的classFile中数据保存为全局变量,字段field,method,Attribute等数据均由链表表示;
组装.class文件
- 组装过程大致分为以下三步:
- 计算byte[]数组,即class文件大小size;
- 向byte数组中按照classFile格式添加对应元素;
- 将byte[] 数据返回;
计算size
- 按照classFile格式分析各个部分占用数据大小
public byte[] toByteArray() {
/**
* 24: u4 magic , 10个必须字段u2(minor_version, major_version, constant_pool_count, access_flags,
* this_class , super_class, interfaces_count, fields_count, methods_count, attributes_count)
* 接口字段集合为 u2 * interfaceCount
* 剩余未计算: cp_info , field_info , method_info , attribute_info
*/
int size = 24 + 2 * this.interfaceCount;
int fieldsCount = 0;
/**
* 链表计算字段占用大小 field_info
*/
FieldWriter fieldWriter;
for(fieldWriter = this.firstField; fieldWriter != null; fieldWriter = (FieldWriter)fieldWriter.fv) {
++fieldsCount;
size += fieldWriter.computeFieldInfoSize();
}
/**
* 链表计算方法占用大小 method_info
*/
int methodsCount = 0;
MethodWriter methodWriter;
for(methodWriter = this.firstMethod; methodWriter != null; methodWriter = (MethodWriter)methodWriter.mv) {
++methodsCount;
size += methodWriter.computeMethodInfoSize();
}
/**
* 计算属性表占用大小 attribute_info
*/
int attributesCount = 0;
......
if (firstAttribute != null) {
attributesCount += firstAttribute.getAttributeCount();
size += firstAttribute.computeAttributesSize(symbolTable);
}
/**
* 计算常量池占用大小 cp_info
*/
size += this.symbolTable.getConstantPoolLength();
- 计算数组大小
- 必要位: 由classFile格式可知总计有24个字节,接口数据有 2*interfaceCount
- 其他位: 依次计算剩下的常量池,字段,方法,属性大小
- 汇总以上数据获取.class文件大小
添加数据
- 严格按照classFile格式添加对应数据
public byte[] toByteArray() {
...
//创建ByteVector存储对象,大小为上方计算的size
ByteVector result = new ByteVector(size);
//添加魔数,version int u4:次版本号+主版本号
result.putInt(0xCAFEBABE).putInt(this.version);
/**
* 添加常量池数据:常量池大小u2 + 常量数组内容大小
* void putConstantPool(ByteVector output) {
output.putShort(this.constantPoolCount).putByteArray(this.constantPool.data, 0,this.constantPool.length);
}
*/
this.symbolTable.putConstantPool(result);
int mask = (version & 0xFFFF) < Opcodes.V1_5 ? Opcodes.ACC_SYNTHETIC : 0;
//添加类标识位,当前类索引,父类索引
result.putShort(this.accessFlags & ~mask).putShort(this.thisClass).putShort(this.superClass);
//添加接口长度
result.putShort(this.interfaceCount);
//添加接口数组
for(int i = 0; i < this.interfaceCount; ++i) {
result.putShort(this.interfaces[i]);
}
//添加字段长度u2
result.putShort(fieldsCount);
//循环添加字段信息
for(fieldWriter = this.firstField; fieldWriter != null; fieldWriter = (FieldWriter)fieldWriter.fv) {
fieldWriter.putFieldInfo(result);
}
//添加方法长度
result.putShort(methodsCount);
boolean hasFrames = false;
boolean hasAsmInstructions = false;
for(methodWriter = this.firstMethod; methodWriter != null; methodWriter = (MethodWriter)methodWriter.mv) {
hasFrames |= methodWriter.hasFrames();
hasAsmInstructions |= methodWriter.hasAsmInstructions();
methodWriter.putMethodInfo(result);
}
//添加属性长度
result.putShort(attributesCount);
···
//添加属性表
if (this.firstAttribute != null) {
this.firstAttribute.putAttributes(this.symbolTable, result);
}
- 添加数据:
- 创建大小为size的字节集合对象ByteVector
- 按照classFile格式由前往后依次添加元素
返回byte数据
- 将获取到的byte返回后重新写入文件
public byte[] toByteArray() {
...
// Third step: replace the ASM specific instructions, if any.
if (hasAsmInstructions) { //如果有ASM特定说明,需要替换为JVM字节码,否则JVM不认识的
return replaceAsmInstructions(result.data, hasFrames);
} else {
return result.data;
}
}
应用
- 通过以上分析,我们对ClassReader,ClassWriter,ClassVisitor运行原理有所了解,那有什么应用点呢?这里我们解决三个问题:无状态转换:查找方法,替换方法,有状态转换:获取Map<String , T>添加key集合
无状态转换
- 转换是局部的,不会依赖于在当前指令之前访问的指令
查找方法
- 给定一个方法包括(类信息,方法名及方法描述符)查找所有调用的地方,输出类和方法集合
- 创建自定义MethodFindRefVisitor继承自ClassVisitor,重写visitMethod()判断code中是否调用了指定查找信息
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
boolean isAbstractMethod = (access & ACC_ABSTRACT) != 0;
boolean isNativeMethod = (access & ACC_NATIVE) != 0;
if (!isAbstractMethod && !isNativeMethod) {
return new MethodFindRefAdaptor(api, null, owner, name, descriptor);
}
return null;
}
- 创建自定义MethodFindRefAdaptor继承自MethodVisitor,对方法体code进行解析重写visitMethodInsn判断是否调用查找方法
@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
// 首先,处理自己的代码逻辑,判断当前方法体中调用了指定查找的方法,则存储当前类和方法信息
if (methodOwner.equals(owner) && methodName.equals(name) && methodDesc.equals(descriptor)) {
String info = String.format("%s.%s%s", currentMethodOwner, currentMethodName, currentMethodDesc);
if (!resultList.contains(info)) {
resultList.add(info);
}
}
// 其次,调用父类的方法实现
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
- 在合适时机对获取数据解析输出打印或文件中
替换方法
-
若想替换掉某一条instruction,应该如何实现呢?当然是首先找到该instruction,然后在同样的位置替换成另一个instruction就可以啦!
- 需要特别注意: 在替换instruction过程中,operand stack(栈桢数据)在修改前和修改后是一致的
-
没有什么比示例更加方便的说明以上operand stack前后保持一致含义
public class HelloWorld {
public void test(int a, int b){
add(a , b); //调用非静态方法
getDesc("HelloWorld"); //调用静态方法
}
private int add(int a , int b){
return a + b;
}
public static String getDesc(String clazzName){
return "current class " + clazzName;
}
}
//输出字节码如下
public void test(int, int);
descriptor: (II)V
flags: (0x0001) ACC_PUBLIC
Code:
stack=3, locals=3, args_size=3 //局部变量表中位置: 0:this; 1:a ; 2:b
0: aload_0 //加载this到操作数栈
1: iload_1 //加载a到操作数栈
2: iload_2 //加载b到操作数栈
3: invokespecial #2 // Method add:(II)I 调用非静态方法add
6: pop
7: ldc #3 // String HelloWorld
9: invokestatic #4 // Method getDesc:(Ljava/lang/String;)Ljava/lang/String;
12: pop
13: return
//asm代码如下
{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "test", "(II)V", null, null);
methodVisitor.visitCode();
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitVarInsn(ILOAD, 1);
methodVisitor.visitVarInsn(ILOAD, 2);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "sample/HelloWorld", "add", "(II)I", false); //调用non-static add方法,上面三个visitVarInsn方法是方法所需参数
methodVisitor.visitInsn(POP);
methodVisitor.visitLdcInsn("HelloWorld");
methodVisitor.visitMethodInsn(INVOKESTATIC, "sample/HelloWorld", "getDesc", "(Ljava/lang/String;)Ljava/lang/String;", false); //调用static getDesc需要一个参数
methodVisitor.visitInsn(POP);
methodVisitor.visitInsn(RETURN);
methodVisitor.visitMaxs(3, 3);
methodVisitor.visitEnd();
}
- Java虚拟机规范中定义如下:
- invokespecial: 在操作数栈中obj之后必须跟随n个参数值,他们的数量,数据类型和顺序都必须与方法描述符保持一致,若调用不是本地方法,n个参数值和obj将从操作数栈中出栈,方法调用时,将在java虚拟机栈中创建一个新的栈桢,obj和连续的n个参数值将存储到新栈桢的局部变量表中,obj存储到局部变量表0,n个参数依次往后,新栈桢创建后就成为当前栈桢,JVM的PC寄存器指向待调用方法中首条指令,程序就从这里开始继续执行;
- invokestatic: 在操作数栈中必须包含连续n个参数值,这些参数的数量,数据类型和顺序都必须遵循实例方法的描述符,若调用不是本地方法,n个参数值将从操作数栈中出栈,方法调用时,将在java虚拟机栈中创建一个新的栈桢,连续的n个参数值将存储到新栈桢的局部变量表中,新栈桢创建后就成为当前栈桢,JVM的PC寄存器指向待调用方法中首条指令,程序就从这里开始继续执行;
- invokespecial调用add方法时会将已经加载到操作数栈上的b , a, this依次出栈消耗掉,因此当我们替换该instruction时的方法也是需要消耗掉操作数栈上的 b, a, this数据,否则后续执行可能会报错;
- invokestatic调用之前只执行了ldc将常量池中3位置加载到操作数栈后执行getDesc方法,只消耗了该String对象;
- 使用工具类AnalyzerAdapter 源码对上方test方法的每行Instruction执行时栈桢中局部变量表和操作数栈数据如下
test:(II)V 局部变量表 | 操作数栈
// {this, int, int} | {}
0000: aload_0 // {this, int, int} | {this}
0001: iload_1 // {this, int, int} | {this, int}
0002: iload_2 // {this, int, int} | {this, int, int}
0003: invokespecial #2 // {this, int, int} | {int} //将操作数栈上数据消耗掉后存储返回值
0006: pop // {this, int, int} | {}
0007: ldc #3 // {this, int, int} | {String}
0009: invokestatic #4 // {this, int, int} | {String} //消耗掉String后存储返回值
0012: pop // {this, int, int} | {}
0013: return // {} | {}
- 通过以上观察,对方法替换分为静态方法和非静态方法
//自定义MethodReplaceInvokeAdapter extends MethodVisitor替换visitMethodInsn方法
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
if (oldOwner.equals(owner) && oldMethodName.equals(name) && oldMethodDesc.equals(desc)){
//这里是我们自定义替换的方法
super.visitMethodInsn(newOpcode , newOwner , newMethodName , newMethodDesc , false);
}else{
super.visitMethodInsn(opcode, owner, name, desc, itf);
}
}
/**
* 调用区别newMethodDesc方法描述符是否需要消耗掉this
*/
//静态方法替换,描述符中不添加this
ClassVisitor cv = new MethodReplaceInvokeVisitor(Opcodes.ASM9, cw,
"sample/HelloWorld", "getDesc", "(Ljava/lang/String;)Ljava/lang/String;",
Opcodes.INVOKESTATIC, "com/asm/method/ReplaceMethodManager", "getDesc", "(Ljava/lang/String;)Ljava/lang/String;");
//非静态方法替换
ClassVisitor cv = new MethodReplaceInvokeVisitor(Opcodes.ASM9, cw, "sample/HelloWorld", "add", "(II)I", Opcodes.INVOKESTATIC, "com/asm/method/ReplaceMethodManager", "add", "(Lsample/HelloWorld;II)I");
- 由于替换方法由我们定义,一般使用的是静态方法newOpcode设为Opcodes.INVOKESTATIC,最后一个参数是boolean isInterface,若为true表示调用为一个接口里的方法,若为false表示调用为一个类中方法,由于操作我们拥有自主权,一般写成在一个类里的static方法
有状态转换
- The stateful transformation require memorizing some state about the instructions that have been visited before the current one. This requires storing state inside the method adapter 有状态转换需要记住一些在当前指令之前访问过的指令状态,这需要在方法适配器中存储状态
- 官方示例:
- 删除指令: 移除ICONST_0 IADD。例如,int d = c + 0;与int d = c;两者效果是一样的,所以+ 0的部分可以删除掉
- 删除指令: 移除ALOAD_0 ALOAD_0 GETFIELD PUTFIELD。例如,this.val = this.val;,将字段的值赋值给字段本身,无实质意义
- 为何说stateless transformation实现起来比较容易,而stateful transformation会实现起来比较困难呢?
- stateless transformation不依赖于其他Instruction,只需关注自身,因此实现起来比较简单
- stateful transformation 依赖其他一条或多条Instruction同时判断,多个指令是一个组合,不能轻易拆散。
- 实现步骤一般分为三步:
- 将问题转换成Instruciton指令,然后对多个指令组合的特征或遵循的模式进行总结;
- 根据总结的特征或模式对指令进行识别,在识别的过程中,每一条Instruction的加入都会引起原有状态state的变化,这就是stateful的含义;
- 识别成功之后,要对Class文件进行转换,这就对应transformation部分,无非就是对Instruction的内容进行增删改等操作;
- 如何根据识别记录状态(state)变化呢,这里就要用到state machine(状态机)
state machine
-
有限状态机(Finite-state machine,FSM),又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型 , FSM是一种算法思想,简单而言,有限状态机由一组状态、一个初始状态、输入和根据输入及现有状态转换为下一个状态的转换函数组成,其作用主要是描述对象在它的生命周期内所经历的状态序列,以及如何响应来自外界的各种事件。
-
对于ASM来说,一般设定一个统一原始的state machine,这里命名为MethodPatternAdapter类
- class info:MethodPatternAdapter抽象类,继承自MethodVisitor
- Fields: 其中定义两个字段,一个常量SEEN_NOTHING表示“初始状态” ,一个state字段用于记录不断变化的状态
- Methods:MethodPatternAdapter类中定义的visitXxxInsn()方法,都会去调用一个自定义的visitInsn()方法:该方法是一个抽象方法,作用就是让所有其他状态(state)都回归到“初始状态”;
- 创建MethodPatternAdapter抽象类,visitXxxInsn方法中调用visitInsn
public abstract class MethodPatternAdapter extends MethodVisitor {
protected final static int SEEN_NOTHING = 0; //初始状态
protected int state; //记录状态变化
public MethodPatternAdapter(int api, MethodVisitor methodVisitor) {
super(api, methodVisitor);
}
@Override
public void visitLdcInsn(Object value) {
visitInsn();
super.visitLdcInsn(value);
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
visitInsn();
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
......
protected abstract void visitInsn();
- 将问题转换成Instruciton指令,然后对多个指令组合的特征或遵循的模式进行总结:如下Hybrid添加action示例中如何获取action列表
Map<String, Class<? extends RegisteredActionCtrl>> actions = new HashMap<>();
actions.put(CarPublishBackParser.ACTION, CarPublishBackActionCtrl.class);
actions.put(CarPublishGuideParser.ACTION, CarPublishGuideActionCtrl.class);
Hybrid.add(actions);
//转换为asm代码如下
methodVisitor.visitCode();
methodVisitor.visitTypeInsn(NEW, "java/util/HashMap");
methodVisitor.visitInsn(DUP);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/util/HashMap", "<init>", "()V", false);
methodVisitor.visitVarInsn(ASTORE, 0);
methodVisitor.visitVarInsn(ALOAD, 0);
//添加第一个
methodVisitor.visitLdcInsn("publish_car_go_back");
methodVisitor.visitLdcInsn(Type.getType("Lcom/wuba/carLib/manager/CarPublishBackActionCtrl;"));
methodVisitor.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", true);
methodVisitor.visitInsn(POP);
methodVisitor.visitVarInsn(ALOAD, 0);
//添加第二个
methodVisitor.visitLdcInsn("show_publish_leadingpage");
methodVisitor.visitLdcInsn(Type.getType("Lcom/wuba/carLib/manager/CarPublishGuideActionCtrl;"));
methodVisitor.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", true);
methodVisitor.visitInsn(POP);
a. 通过以上观察,对于actions.put方法调用实际上是三个instruction:visitLdcInsn(key), visitLdcInsn(value),visitMethodInsn
b. 总共调用三个方法,我们只需要在添加两个状态即可满足,状态转换如下图所示
//调用visitLdcInsn(key)后的状态
private static final int SEEN_LDCTYPE = 1
//调用visitLdcInsn(value)后的状态
private static final int SEEN_LDC_METHOD = 2
state_transforme.png
- 状态转移清楚后,写代码就很轻松了
public class ReadActionMapMethod extends MethodPatternAdapter {
/**
* 需要校验的字节码顺序:Map.put()/HashMap.put()
*/
private static final int SEEN_LDCTYPE = 1
private static final int SEEN_LDC_METHOD = 2
//当前存储业务线
private String businessLine
/**
* 当前第一步缓存数据
*/
private String mMapKey
/**
* 当前第二步缓存数据
*/
private Type mMapValue
protected ReadActionMapMethod(int api, MethodVisitor methodVisitor, String businessLine) {
super(api, methodVisitor)
this.businessLine = businessLine
}
@Override
void visitLdcInsn(Object value) {
switch (state) {
case SEEN_NOTHING:
if (value instanceof String) {
state = SEEN_LDCTYPE
mMapKey = value
mv.visitLdcInsn(value)
return
}
break
case SEEN_LDCTYPE:
if (value instanceof Type) {
state = SEEN_LDC_METHOD
mMapValue = value
mv.visitLdcInsn(value)
return
}
break
}
super.visitLdcInsn(value)
}
@Override
protected void visitInsn() {
// switch (state) {
//
// case SEEN_LDCTYPE:
// //将拦截的数据发送出去
// mv.visitLdcInsn(mMapKey)
// break
//
// case SEEN_LDC_METHOD:
// //将拦截的数据发送出去
// mv.visitLdcInsn(mMapKey)
// mv.visitLdcInsn(mMapValue)
// break
// }
state = SEEN_NOTHING
}
@Override
void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
switch (state) {
case SEEN_LDC_METHOD:
boolean flag = ((opcode == Opcodes.INVOKEVIRTUAL && owner == "java/util/HashMap" && name == "put")
|| (opcode == Opcodes.INVOKEINTERFACE && owner == "java/util/Map" && name == "put"))
if (flag) { //需要存储数据啦
HybridActionManager.install.addAction(businessLine, mMapKey)
}
break
}
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
}
思考:
- 当前只是检查三个状态,如何提升准确性?
- 存储上方获取的String -> Type对应值(类的符号引用),在此扫描获取Type class文件,解析其是否实现Action接口RegisteredActionCtrl,若实现该接口在添加,提高准确性;若想了解这部分内容,请联系我提供demo事例中对RN module字段获取内容,先获取具体类型再根据是否添加类注解,重写getName方法等确定字段值;
@Override
protected List<ModuleSpec> createWubaNativeModules(final ReactApplicationContextWrapper reactApplicationContext) {
List<ModuleSpec> moduleSpecList = new ArrayList<ModuleSpec>();
moduleSpecList.add(new ModuleSpec(new Provider<NativeModule>() {
@Override
public NativeModule get() {
return new WBSingleSelector(reactApplicationContext);
}
},WBSingleSelector.class.getName()));
return moduleSpecList;
//解析字节码指令:校验状态共计四步
methodVisitor.visitLdcInsn(Type.getType("Lcom/wuba/wubaaction/rn/selector/WBMultiUnlinkSelector;"));
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getName", "()Ljava/lang/String;", false);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "com/facebook/react/bridge/ModuleSpec", "<init>", "(Ljavax/inject/Provider;Ljava/lang/String;)V", false);
methodVisitor.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "add", "(Ljava/lang/Object;)Z", true);
//viewModule注入
@Override
protected List<WubaViewManager> createWubaViewManagers(ReactApplicationContextWrapper reactApplicationContext) {
List<WubaViewManager> list = new ArrayList<WubaViewManager>();
list.add(new WBPublishLoadingView());
list.add(new WBErrorView());
return list;
}
//解析字节码指令:校验状态共计四步
methodVisitor.visitTypeInsn(NEW, "com/wuba/wubaaction/rn/selector/view/WBPublishLoadingView");
methodVisitor.visitInsn(DUP);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "com/wuba/wubaaction/rn/selector/view/WBPublishLoadingView", "<init>", "()V", false);
methodVisitor.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "add", "(Ljava/lang/Object;)Z", true);
- 当引用一些业务库中对异常只简单捕获并未输出堆栈信息导致问题查找困难时,也可以尝试使用ASM对catch()函数校验后增加自定义逻辑:输出堆栈信息或调用自定义方法等; catch.png
总结
- ASM操作字节码文件,其前提是熟记classFile文件格式,其各个数据项严格按顺序排列,没有任何分隔符,对照该文件格式在看ASM中的各个操作才能一目了然;
- 若文章对你有帮助,欢迎添加好友一起交流!
参考资料
- 《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》周志明
- ASM4使用中英文手册 中文版 英文版
- jvm字节码指令集
- Oracle: The Java Virtual Machine Specification, Java SE 8 Edition