ASM 概述

0x00 什么是 ASM

ASM is an all purpose Java bytecode manipulation and analysis framework. It can be used to modify existing classes or to dynamically generate classes, directly in binary form.

ASM offers similar functionality as other Java bytecode frameworks, but is focused on performance. Because it was designed and implemented to be as small and as fast as possible.

0x10 为什么要操纵分析字节码

  • 程序分析,发现 bug,检测无用代码
    • JaCoCo(Java Code Coverage Library 用于检查单元测试覆盖率)
  • 产生代码
    • openJDK lambda、Groovy 编译器、Kotlin 编译器
  • 优化、混淆代码,注入调试及监控代码等
    • Aspectj
    • Proguard

可以利用 ASM 实现自己的代码混淆工具

0x20 ASM 编程框架简介

Core API

代理模式、访问者模式

  • ClassReader

    • 解析字节码文件,调用 ClassVisitor 的特定的方法访问 Class 的字段、方法以及字节码
  • ClassVisitor

    • 用于访问 Java 类文件
    • 必须以下面的顺序调用该类的方法
    {@code visit} [ {@code visitSource} ] [ {@code visitModule} ][ {@code visitNestHost} ][ {@code
    visitOuterClass} ] ( {@code visitAnnotation} | {@code visitTypeAnnotation} | {@code
    visitAttribute} )* ( {@code visitNestMember} | {@code visitInnerClass} | {@code visitField} |
    {@code visitMethod} )* {@code visitEnd}.
    
    • 如果需要增加或者删除类的字段/方法,可以自定义该类实现
  • FieldVisitor

    • 用于访问 Java 类的字段
    • 必须以下面的顺序调用该类的方法
    ( {@code visitAnnotation} | {@code visitTypeAnnotation} | {@code visitAttribute} )* {@code visitEnd}.
    
    • 修改类字段的内容,通过自定义 ClassVisitor 已经可以实现,自定义该类的场景不多
  • MethodVisitor

    • 用于访问 Java 类的方法
    • 必须以下面的顺序调用该类的方法
    ( {@code visitParameter} )* [ {@code visitAnnotationDefault} ] ( {@code visitAnnotation} |
    {@code visitAnnotableParameterCount} | {@code visitParameterAnnotation} {@code
    visitTypeAnnotation} | {@code visitAttribute} )* [ {@code visitCode} ( {@code visitFrame} |
    {@code visit<i>X</i>Insn} | {@code visitLabel} | {@code visitInsnAnnotation} | {@code
    visitTryCatchBlock} | {@code visitTryCatchAnnotation} | {@code visitLocalVariable} | {@code
    visitLocalVariableAnnotation} | {@code visitLineNumber} )* {@code visitMaxs} ] {@code visitEnd}.
    
    • 如果需要修改类方法的实现,需要自定义该类
  • ClassWriter

    • 存储类文件字节码信息,用于生成类文件的 ClassVisitor
      • ClassWriter 内部有个 FieldWriter 链表用于保存类文件的所有 Field 信息
      • ClassWriter 内部有个 MethodWriter 链表用于保存类文件的所有 Method 信息
    • 与一个或多个 ClassReader 及 ClassVisitor 一起使用修改类文件

Core API 实现原理

下面通过一个简单的字节码文件拷贝的例子来分析 ASM 的原理

ASM 实现字节码拷贝关键代码

......
// inputClass 为输入字节码文件
InputStream inputStream = new FileInputStream(inputClass);
final ClassReader classReader = new ClassReader(inputStream);
final ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
classReader.accept(classWriter, ClassReader.EXPAND_FRAMES);
final byte[] newClassFile = classWriter.toByteArray();
......

WorkFlow

  • ASM Core API 主要用到了两种关键的设计模式,代理模式及访问者模式
  • ClassReader 负责读取要修改的字节码文件,同时根据字节码文件的格式规定了访问方式(也就是 ClassVisitor 以及 FieldVisitor、MethodVisitor 的 visitXxx 方法的调用顺序)
  • ClassWriter 继承自 ClassVisitor,如果 ClassWriter 直接作为访问者访问 ClassReader,也就是上面的例子。字节码最终不会修改只是保存到 ClassWriter 中
  • 如果我们需要增加、删除 Filed 或者 Method,我们只需要自定义 ClassVisitor 代理 ClassWriter,用自定义的 ClassVisitor 访问 ClassReader 即可,这样在 ClassReader 使用我们自定义的 ClassVisitor 的 visitXxx 遍历字节码的 Field、Method 时,我们可以重写 visitXxx 方法来实现相应的功能
  • 如果需要修改 Method 的内部实现,我们不仅需要自定义 ClassVisitor 还需要自定义 MethodVisitor 用于代理 ClassWriter 内部 MethodWriter 访问 Method 的实现,并在访问过程中做出修改
  • ClassWriter 的 toByteArray 方法实际只是 dump 出内部两个链表中保存的字节码信息

Tree API

  • 不介绍

0x30 使用 ASM 进行 AOP 编程 (Android studio 插件开发)

类中添加 Field

  • 例子
public class ASMAddField {
    public static final String LOG_TAG = "ASM.ASMAddField";
    // 使用 ASM 增加 addField 字段
//    public Object addField;

    private ASMAddField() {

    }
}
  • 关键实现代码
......
private class AddFieldAdapter extends ClassVisitor {
    public AddFieldAdapter(int api, ClassVisitor cv) {
        super(api, cv);
    }
    @Override
    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
        // 检查字段是否已经存在
        if (name.equals(fieldName)) {
            isFieldPresent = true;
        }
        return super.visitField(access, name, desc, signature, value);
    }
    @Override
    public void visitEnd() {
        if (!isFieldPresent) {
            // 添加字段
            FieldVisitor fieldVisitor =
                    cv.visitField(fieldACC, fieldName, fieldDesc, null, null);
            if (fieldVisitor != null) {
                fieldVisitor.visitEnd();
            }
        }
        super.visitEnd();
    }
}
......

类中删除 Field

  • 例子
public class ASMDeleteField {
    public static final String LOG_TAG = "ASM.ASMDeleteField";
    // 使用 ASM 删除该字段
    public Object deleteField = new Object();

    private ASMDeleteField() {

    }
}
  • 关键实现代码
private class DeleteFieldAdapter extends ClassVisitor {
    public DeleteFieldAdapter(int api, ClassVisitor cv) {
        super(api, cv);
    }

    @Override
    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
        if (name.equals(fieldName)) {
            return null;
        }
        return super.visitField(access, name, desc, signature, value);
    }
}

类中增加 Method

  • 例子
public class ASMAddMethod {
    public static final String LOG_TAG = "ASM.ASMAddMethod";

    private ASMAddMethod() {
    }

    // 使用 ASM 增加下面方法
    // public static void addMethod() {
    //     Log.i(LOG_TAG, "this is add method");
    // }
}
  • 关键实现代码
private class AddMethodAdapter extends ClassVisitor {
    public AddMethodAdapter(int api, ClassVisitor cv) {
        super(api, cv);
    }
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        if (name.equals(methodName) && desc.equals(methodDesc)) {
            isMethodPresent = true;
        }
        return super.visitMethod(access, name, desc, signature, exceptions);
    }
    @Override
    public void visitEnd() {
        if (!isMethodPresent) {
            // 增加方法及方法的内部实现,可以借助 ASM Bytecode Outline 2017 插件事先获得方法的内部实现
            MethodVisitor methodVisitor = cv.visitMethod(methodACC,
                    methodName, methodDesc, null, null);
            if (methodVisitor != null) {
                methodVisitor.visitCode();
                methodVisitor.visitLdcInsn("ASM.ASMAddMethod");
                methodVisitor.visitLdcInsn("this is add method");
                methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC,
                        "android/util/Log",
                        "i",
                        "(Ljava/lang/String;Ljava/lang/String;)I",
                        false);
                methodVisitor.visitInsn(POP);
                methodVisitor.visitInsn(Opcodes.RETURN);
                methodVisitor.visitMaxs(2, 0);
                methodVisitor.visitEnd();
            }
        }
        super.visitEnd();
    }
}

类中删除 Method

  • 例子
public class ASMDeleteMethod {
    public static final String LOG_TAG = "ASM.ASMDeleteMethod";

    private ASMDeleteMethod() {
    }

    // 使用 ASM 删除该方法
    public static void deleteMethod() {
        Log.i(LOG_TAG, "this is delete method");
    }
}
  • 关键实现代码
private class DeleteMethodAdapter extends ClassVisitor {
    public DeleteMethodAdapter(int api, ClassVisitor cv) {
        super(api, cv);
    }
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        if (access == methodACC
                && name.equals(methodName)
                && desc.equals(methodDesc)) {
            return null;
        }
        return super.visitMethod(access, name, desc, signature, exceptions);
    }
}

修改类中的 Method 实现

  • 例子
public class ASMModifyMethod {
    public static final String LOG_TAG = "ASM.ASMModifyMethod";

    private ASMModifyMethod() {

    }

    // 使用 ASM 增加方法耗时打印
    public static void modifyMethod() {
//        long startTime = System.currentTimeMillis();
        Log.i(LOG_TAG, "this is modify method");
//        long endTime = System.currentTimeMillis();
//        Log.i(LOG_TAG, "method consume time: " + (endTime - startTime) + "ms");
    }
}
  • 关键实现代码
private class ModifyMethodAdapter extends ClassVisitor {
    public ModifyMethodAdapter(int api, ClassVisitor cv) {
        super(api, cv);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
        if (mv != null
                && access == methodACC
                && name.equals(methodName)
                && desc.equals(methodDesc)) {
            mv = new MethodVisitorImpl(ASM5, methodACC, methodDesc, mv);
        }
        return mv;
    }
}
private class MethodVisitorImpl extends LocalVariablesSorter {
    private int start;
    private int end;
    protected MethodVisitorImpl(int api, int access, String desc, MethodVisitor mv) {
        super(api, access, desc, mv);
    }

    @Override
    public void visitCode() {
        super.visitCode();
        mv.visitMethodInsn(INVOKESTATIC,
                "java/lang/System",
                "currentTimeMillis",
                "()J",
                false);
        start = newLocal(Type.LONG_TYPE);
        mv.visitVarInsn(LSTORE, start);
    }
    @Override
    public void visitInsn(int opcode) {
        if (opcode == RETURN || opcode == ATHROW) {
            mv.visitMethodInsn(INVOKESTATIC,
                    "java/lang/System",
                    "currentTimeMillis",
                    "()J",
                    false);
            end = newLocal(Type.LONG_TYPE);
            mv.visitVarInsn(LSTORE, end);
            mv.visitLdcInsn("ASM.ASMModifyMethod");
            mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
            mv.visitInsn(DUP);
            mv.visitMethodInsn(INVOKESPECIAL,
                    "java/lang/StringBuilder",
                    "<init>",
                    "()V",
                    false);
            mv.visitLdcInsn("method consume time: ");
            mv.visitMethodInsn(INVOKEVIRTUAL,
                    "java/lang/StringBuilder",
                    "append",
                    "(Ljava/lang/String;)Ljava/lang/StringBuilder;",
                    false);
            mv.visitVarInsn(LLOAD, end);
            mv.visitVarInsn(LLOAD, start);
            mv.visitInsn(LSUB);
            mv.visitMethodInsn(INVOKEVIRTUAL,
                    "java/lang/StringBuilder",
                    "append",
                    "(J)Ljava/lang/StringBuilder;",
                    false);
            mv.visitLdcInsn("ms");
            mv.visitMethodInsn(INVOKEVIRTUAL,
                    "java/lang/StringBuilder",
                    "append",
                    "(Ljava/lang/String;)Ljava/lang/StringBuilder;",
                    false);
            mv.visitMethodInsn(INVOKEVIRTUAL,
                    "java/lang/StringBuilder",
                    "toString",
                    "()Ljava/lang/String;",
                    false);
            mv.visitMethodInsn(INVOKESTATIC,
                    "android/util/Log",
                    "i",
                    "(Ljava/lang/String;Ljava/lang/String;)I",
                    false);
            mv.visitInsn(POP);
        }
        super.visitInsn(opcode);
    }

    @Override
    public void visitMaxs(int maxStack, int maxLocals) {
        super.visitMaxs(maxStack + 4, maxLocals + 4);
    }
}

文章中的样例 Demo 完成实现已放在 Github

Android Studio 字节码插件:ASM Bytecode Outline 2017

0x40 ASM 在 Android 中的应用

  • R 内联
  • D8 desugar
  • Instant Run

参考

推荐阅读更多精彩内容

  • 前言 很早之前就写过面向切面的编程思想,主要学习了AOP的思想(参考:AOP简介)以及使用 AspectJ 实现简...
    Whyn阅读 8,389评论 4 38
  • 引言 什么是 ASM ? ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。AS...
    Chauncey_Chen阅读 1,100评论 0 6
  • 前言 之前一直使用greys及其内部升级二次开发版来排查问题。最近周末刚好事情不多,作为一名程序员本能地想要弄懂这...
    LNAmp阅读 7,210评论 1 11
  • https://blog.csdn.net/luanlouis/article/details/24589193 ...
    小陈阿飞阅读 543评论 1 1
  • 反射,它就像是一种魔法,引入运行时自省能力,赋予了 Java 语言令人意外的活力,通过运行时操作元数据或对象,Ja...
    小刀爱编程阅读 401评论 0 4
  • 洞察力是指深入事物或问题的能力,从字面上看来洞察是指对于山洞的观察,山洞除了洞口的地方可以被阳光照射外其他地方...
    faye26阅读 112评论 0 0
  • 从前,父亲的火车很慢 一走便是一年 车厢里遗落父亲的汗 无数个夜里彻夜难眠 父亲穿梭在座位和两个车厢相连的地方之间...
    于十六阅读 297评论 8 8
  • 马丁·路德·金50年前说过:“如果你不能飞,那就跑。如果你不能跑,那就走。如果你不能走路,那就爬行,但无论你做什么...
    guoery阅读 394评论 0 50
  • 风吹过我的脸颊 雨落在我的头发 我听见你在清晨的车水马龙里 回头张望进我为你搭的青石板街巷 石板缝隙里长出的叶子是...
    皙小摇同学阅读 431评论 0 6