本篇将介绍使用ASM来修改字节码
Android-ASM修改字节码
本篇将介绍ASM的使用
(本篇对Android 进阶之路:ASM 修改字节码,这样学就对了! - 掘金 (juejin.cn)有一定的借鉴与参考)
引言
众所周知,JAVA文件经过JAVAC编译之后会生成Class文件,之后这个Class文件就可以被JAVA虚拟机所加载。
既然JAVA虚拟机可以加载这种Class文件,那么肯定存在某种规则能够读取Class文件的内容。
那么如果我们能掌握这种规则,岂不是我们可以对编译好的Class文件进行修改,之后在给它保存回去?YES,这是完全没问题的
所以今天我们要讲述的重点来了,那就是ASM-操作字节码
分析Class文件
首先,如果你需要知道这个类中有那些方法,那你希望可以怎么获取?那肯定是越简单越好,最好是返回一个List回来,List存储着所以方法的名称
classNode
哦豁,还真有这个东西,那就是ClassNode。但是这个ClassNode相当于是个容器,它本身并不会去获取这个类的所有方法。那我们该如何使用呢?
首先,我们先创建一个User类,来模拟一下
1
2
3
4
5
6
7
8
9
10
11
12public class User{
private String name;
private int age;
public String getName(){
return name;
}
public int getAge(){
return age;
}
}然后我们通过一些操作(也就是通过ClassReader去读取给定路径下的类,然后遍历内部),再将数据填充到ClassNode内部
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public static void main(String[] args){
// 这里为了方便就直接获取了
Class class = User.class;
// 这里也就是通过class类去获取路径
String package = Utils.getClassFilePack(class);
// 这里通过一个输入流将路径放入其中进行解析到ClassReader中去
ClassReader cr = new ClassReader(new FileInputStream(package));
// 这里是指定了ASM的版本
ClassNode cn = new ClassNode(Opcodes.ASM5);
// 让classreader内部去遍历,将结果都存放到classNode中去
cr.accept(cn,0);
// 之后我们就可以通过ClassNode的methods和fields来获取类的方法和属性
List<MethodNode> methods = cn.methods;
List<FieldNode> fields = cn.fields;
}上述代码中,有一个getClassFilePack()方法,是用于通过class文件来获取路径
1
2
3
4
5
6public static String getClassFilePath(Class clazz) {
String buildDir = clazz.getProtectionDomain().getCodeSource().getLocation().getFile();
String fileName = clazz.getSimpleName() + ".class";
File file = new File(buildDir + clazz.getPackage().getName().replaceAll("[.]", "/") + "/", fileName);
return file.getAbsolutePath();
}通过上面的这几步,现在已经知道了这个类中有那些属性和方法,我们通过打印在来看一下
1
2
3
4
5
6
7methods:
<init>, ()V
getName, ()Ljava/lang/String; // 对应 String getName()
getAge, ()I // 对应 int getAge()
fields:
name, Ljava/lang/String; // 对应 String name
age, I // 对应 int age
这样去获取类中的方法和属性会不会有点麻烦,还得先让ClassReader去遍历一遍,在将数据放入到ClassNode中去,最后我们才可以进行访问
确实,所以还有一种方法可以知道类中的方法和属性,还是可以实时”监听”。这就要请出ClassVisitor
ClassVisitor
ClassVisitor与ClassNode的区别就是,一个相当于是在遍历的时候就通知你,一个是全部遍历完了,装填到ClassNode再让你知道
1 | public static void main(String[] args) throws Exception { |
我们来看一下输出:
1 | field:name , type = Ljava/lang/String; |
可以看到,这其实和上面的ClassNode差不多的。如果我们自己实现一个ClassVisitor,再把遍历结果保存起来,那岂不是相当于我们自己实现了个ClassNode?
我们再看一下ClassNode是继承于谁?
1 | public class ClassNode extends ClassVisitor |
哦豁,ClassNode就是继承与ClassVisitor的。所以说ClassNode相关的原理就是我们上面使用ClassVisitor那样的
修改字节码
既然我们可以知道到这个类中有那些属性和方法,接下来我们是不是得尝试对其中的内容进行修改呢?
下面我们将会以官方给出的代码为例子,进行讲解
官方源代码:
1 | public class C { |
通过ASM修改字节码后的代码:
1 | public class C { |
就是创建了个局部变量time,然后在m()
中与System.currentTimeMillis()
进行运算
System.currentTimeMillis():获取当前的系统时间戳
ClassWriter
上面已经介绍了如何在遍历中获取类属性、方法信息,那应该也有一个Api是可以将信息按Class格式写入获取的把?
有的,刚好是有的,那就是ClassWriter
。它所做的事前就是,在遍历时保存信息,最后将信息按Class格式写入到文件
诶,ClassWriter
也可以保存信息?,那岂不是?
1 | public class ClassWriter extends ClassVisitor |
是的,ClassWriter
也是继承与ClassVisitor
的
1 | public static void main(String[] args){ |
上述操作,也就是完成了一个类的复制,如果输出路径传入的是原本类的路径,那么就是完成了覆盖。也就是完成了保存的操作
修改
接下来我们就对源文件进行添加代码,等等,ClassReader
只接受一个ClassVisitor
参数,因为我们最后要保存,所以ClassWriter
是一定要传入的,那ClassVisitor
监听怎么办呢?别急,我们看一下ClassVisitor
的构造方法
1 | public ClassVisitor(final int api, final ClassVisitor classVisitor) |
这不就明白了吗,ClassVisitor
构造方法里,还可以再传一个对象。也就是说自己作为代理对象,需要拦截的方法,我们复写做操作
所以,我们先创建一个类继承
ClassVisitor
1
2
3
4
5
6public class TestClassVisitor extends ClassVisitor implements Opcodes {
public TestClassVisitor(ClassVisitor cv) {
super(Opcodes.ASM5, cv);
}
}Opcodes.ASM5:是ASM的版本,在构建的时候需要指明
接下来我们需要先知道一下
ClassVisitor
内部一些方法的执行顺序,以便于插入代码1
2visit -> visitSource? -> visitOuterClass? -> ( visitAnnotation | visitAttribute )*
-> ( visitInnerClass | visitField | visitMethod )* -> visitEnd?:带问号的表明可能不会执行
*: 带 * 号的表明可能会执行多次
那么我们需要一个只会执行一次的方法,那么就选
visitEnd
把位置选择好之后,我们还需要了解一下,
ClassWriter
内部的生成代码机制ClassWriter
内部会更具一个field来判断是否生成此代码,它本身也会通过visitField
去收集相关信息也就是说,只要我们手动调用一次
ClassWriter.visitField()
,ClassWriter
就会以为真的有这个field,然后记录下来,后面进行生成那就好办了,我们只需要在选好的地方,调用一下
ClassWriter.visitField()
,将想插入的代码放入其中就ok了写代码
1
2
3
4
5
6
7
8
9
10
11
12
13public class TestClassVisitor extends ClassVisitor implements Opcodes {
public TestClassVisitor(ClassVisitor cv) {
super(Opcodes.ASM5, cv);
}
public void visitEnd(){
// cv:这里调用ClassVisitor的委托者,也就是我们设置的ClassWriter
// 往里添加了一个field,内容为 public static Long time
FieldVisitor fv = cv.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "timer","J", null, null);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13public abstract class ClassVisitor {
/**
* The ASM API version implemented by this visitor. The value of this field must be one of {@link
* Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6} or {@link Opcodes#ASM7}.
*/
protected final int api;
/** The class visitor to which this visitor must delegate method calls. May be null. */
protected ClassVisitor cv;
...
}cv 是ClassVisitor内部的委托者
之后我们将代码组装在一起
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public static void main(String[] args){
Class class = Test.class;
String cp = Utils.getClassFilePath(clazz);
ClassReader cr = new ClassReader(new FileInputStream(cp));
ClassWriter cw = new ClassWriter(0);
// 将ClassWriter放入到刚刚创建的TestClassVisitor中去当代理
TestClassVisitor tc = TestClassVisitor(cw);
cr.accept(tc,0);
byte[] by = cw.toByteArray();
FileOutputStream out = new FileOutputStream("/输出路径");
out.write(by);
out.flush();
out.close();
}运行查看一下反编译源码
1
2
3
4public Class Test{
public static long time;
public void m() throws java.lang.Eception;
}ok,添加代码是完成了。接下来就来修改代码
修改方法
既然是要修改方法,那么就可以直接上方介绍的ClassVisitor.visitMethod()
1 |
|
可以看到这个方法中包含了,方法所申明的相关信息。但是没有实际运行时相关代码信息,这可咋整?
别急,这个方法的返回值是MethodVisitor
。所以我们 ClassReader
遍历class 文件的思路肯定是:先给你方法声明相关信息,然后我们给它返回一个 MethodVisitor
,它拿到这个 MethodVisitor
,再通过 MethodVisitor
开始遍历这个方法内部的所有信息。
1 |
|
接下来应该也是要选择在那个方法中进行插入,所以这就要求我们知道MethodVisitor
中的各个方法执行顺序
1 | visitAnnotationDefault? -> (visitAnnotation | visitParameterAnnotation | visitAttribute)* -> (visitCode(visitTryCatchBlock |visitLabel |visitFrame |visitXxxInsn | visitLocalVariable |visitLineNumber )*visitMaxs )? -> visitEnd |
以上方法中,一开始会先遍历一些注解或者参数信息等,之后在从visirCode
开始遍历整个方法。所以我们选择在visitCode ()
方法中进行修改代码的操作,在visitXxxInsn
内部判断是否return
1 | public class MyMethodVisitor extends MethodVisitor { |
换成人看的代码就是:
1 timer -= System.currentTimeMillis();
同样的,我们再在visitInsn
,把return
前的方法也改一下
1 |
|