Android-ASM修改字节码

本篇将介绍使用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相当于是个容器,它本身并不会去获取这个类的所有方法。那我们该如何使用呢?

  1. 首先,我们先创建一个User类,来模拟一下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class User{
    private String name;
    private int age;

    public String getName(){
    return name;
    }

    public int getAge(){
    return age;
    }
    }
  2. 然后我们通过一些操作(也就是通过ClassReader去读取给定路径下的类,然后遍历内部),再将数据填充到ClassNode内部

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public 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
    6
    public 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();
    }

    /ps: 具体请参考Android 进阶之路:ASM 修改字节码,这样学就对了! - 掘金 (juejin.cn)

  3. 通过上面的这几步,现在已经知道了这个类中有那些属性和方法,我们通过打印在来看一下

    1
    2
    3
    4
    5
    6
    7
    methods:
    <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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static void main(String[] args) throws Exception {
// 老样子获取到Class文件
Class clazz = User.class;
// 获取路径
String clazzFilePath = Utils.getClassFilePath(clazz);
// 获取到ClasReader
ClassReader classReader = new ClassReader(new FileInputStream(clazzFilePath));
// 这里就不一样了,不在是创建ClassNode,而是创建了ClassVisitor,还实现了两个抽象方法
ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM5) {
// visitField的作用是,当遍历到属性时,就会调用
@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
System.out.println("field:" + name + " , type = " + descriptor);
return super.visitField(access, name, descriptor, signature, value);
}

// visitMethod的作用是,当遍历到方法时,就会调用
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
System.out.println("method:" + name + " , type = " + descriptor);
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
};
// 将ClassVisitor放入,继续让ClassReader进行遍历
classReader.accept(classVisitor, 0);
}

我们来看一下输出:

1
2
3
4
5
field:name , type = Ljava/lang/String;
field:age , type = I
method:<init> , type = ()V
method:getName , type = ()Ljava/lang/String;
method:getAge , type = ()I

可以看到,这其实和上面的ClassNode差不多的。如果我们自己实现一个ClassVisitor,再把遍历结果保存起来,那岂不是相当于我们自己实现了个ClassNode?

我们再看一下ClassNode是继承于谁?

1
public class ClassNode extends ClassVisitor 

哦豁,ClassNode就是继承与ClassVisitor的。所以说ClassNode相关的原理就是我们上面使用ClassVisitor那样的

修改字节码

既然我们可以知道到这个类中有那些属性和方法,接下来我们是不是得尝试对其中的内容进行修改呢?

下面我们将会以官方给出的代码为例子,进行讲解

官方源代码:

1
2
3
4
5
 public class C {
public void m() throws Exception {
Thread.sleep(100);
}
}

通过ASM修改字节码后的代码:

1
2
3
4
5
6
7
8
9
public class C {
public static long timer;

public void m() throws Exception {
timer -= System.currentTimeMillis();
Thread.sleep(100);
timer += System.currentTimeMillis();
}
}

就是创建了个局部变量time,然后在m()中与System.currentTimeMillis()进行运算

System.currentTimeMillis():获取当前的系统时间戳

ClassWriter

上面已经介绍了如何在遍历中获取类属性、方法信息,那应该也有一个Api是可以将信息按Class格式写入获取的把?

有的,刚好是有的,那就是ClassWriter。它所做的事前就是,在遍历时保存信息,最后将信息按Class格式写入到文件

诶,ClassWriter也可以保存信息?,那岂不是?

1
public class ClassWriter extends ClassVisitor

是的,ClassWriter也是继承与ClassVisitor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public 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);
cr.accept(cw,0);

// 上面的还是之前那样,这里将ClassWriter内部保存的信息,转变为byte[]
byte[] by = cw.toByteArray();
// 在通过输出流,输出到指定位置
FileOutputStream out = new FileOutputStream("/输出路径");
out.write(by);
out.flush();
out.close();
}

上述操作,也就是完成了一个类的复制,如果输出路径传入的是原本类的路径,那么就是完成了覆盖。也就是完成了保存的操作

修改

接下来我们就对源文件进行添加代码,等等,ClassReader只接受一个ClassVisitor参数,因为我们最后要保存,所以ClassWriter是一定要传入的,那ClassVisitor监听怎么办呢?别急,我们看一下ClassVisitor的构造方法

1
public ClassVisitor(final int api, final ClassVisitor classVisitor) 

这不就明白了吗,ClassVisitor构造方法里,还可以再传一个对象。也就是说自己作为代理对象,需要拦截的方法,我们复写做操作

  1. 所以,我们先创建一个类继承ClassVisitor

    1
    2
    3
    4
    5
    6
    public class TestClassVisitor extends ClassVisitor implements Opcodes {

    public TestClassVisitor(ClassVisitor cv) {
    super(Opcodes.ASM5, cv);
    }
    }

    Opcodes.ASM5:是ASM的版本,在构建的时候需要指明

  2. 接下来我们需要先知道一下ClassVisitor内部一些方法的执行顺序,以便于插入代码

    1
    2
    visit  -> visitSource? -> visitOuterClass? -> ( visitAnnotation | visitAttribute )*
    -> ( visitInnerClass | visitField | visitMethod )* -> visitEnd

    ?:带问号的表明可能不会执行

    *: 带 * 号的表明可能会执行多次

    那么我们需要一个只会执行一次的方法,那么就选visitEnd

  3. 位置选择好之后,我们还需要了解一下,ClassWriter内部的生成代码机制

    ClassWriter内部会更具一个field来判断是否生成此代码,它本身也会通过visitField 去收集相关信息

    也就是说,只要我们手动调用一次ClassWriter.visitField()ClassWriter就会以为真的有这个field,然后记录下来,后面进行生成

    那就好办了,我们只需要在选好的地方,调用一下ClassWriter.visitField(),将想插入的代码放入其中就ok了

  4. 写代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class TestClassVisitor extends ClassVisitor implements Opcodes {

    public TestClassVisitor(ClassVisitor cv) {
    super(Opcodes.ASM5, cv);
    }

    @Override
    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
    13
    public 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内部的委托者

  5. 之后我们将代码组装在一起

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public 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();
    }
  6. 运行查看一下反编译源码

    1
    2
    3
    4
    public Class Test{
    public static long time;
    public void m() throws java.lang.Eception;
    }

    ok,添加代码是完成了。接下来就来修改代码

修改方法

既然是要修改方法,那么就可以直接上方介绍的ClassVisitor.visitMethod()

1
2
3
4
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
...
}

可以看到这个方法中包含了,方法所申明的相关信息。但是没有实际运行时相关代码信息,这可咋整?

别急,这个方法的返回值是MethodVisitor。所以我们 ClassReader 遍历class 文件的思路肯定是:先给你方法声明相关信息,然后我们给它返回一个 MethodVisitor,它拿到这个 MethodVisitor,再通过 MethodVisitor开始遍历这个方法内部的所有信息。

1
2
3
4
5
6
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
MyMethodVisitor(mv);
return mv;
}

接下来应该也是要选择在那个方法中进行插入,所以这就要求我们知道MethodVisitor中的各个方法执行顺序

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

以上方法中,一开始会先遍历一些注解或者参数信息等,之后在从visirCode开始遍历整个方法。所以我们选择在visitCode ()方法中进行修改代码的操作,在visitXxxInsn内部判断是否return

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyMethodVisitor extends MethodVisitor {

public MyMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM5, mv);
}

@Override
public void visitCode() {
super.visitCode();
// 将静态变量timer,压到操作栈
mv.visitFieldInsn(GETSTATIC, mOwner, "timer", "J");
// 调用System.currentTimeMillis,将返回结果压到操作栈
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System","currentTimeMillis", "()J");
// 进行相减
mv.visitInsn(LSUB);
// 将操作栈中的值赋值给timer
mv.visitFieldInsn(PUTSTATIC, mOwner, "timer", "J");
}
}

换成人看的代码就是:

1
timer -= System.currentTimeMillis();

同样的,我们再在visitInsn,把return前的方法也改一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public void visitInsn(int opcode) {
// 判断当前方法的code,也就是状态是否是小于return状态
if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
mv.visitFieldInsn(GETSTATIC, mOwner, "timer", "J");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J");
// 上面代码和上面那一步基本相同,都是先将timer压入操作栈,在将另一个数也压出
// 在操作栈中进行相加操作
mv.visitInsn(LADD);
// 最后在将操作栈中的值赋值给timer
mv.visitFieldInsn(PUTSTATIC, mOwner, "timer", "J");
}
mv.visitInsn(opcode);
}

栈的深度