ASM字节码插桩技术

ASM概述
  • ASM是一个功能比较齐全的java字节码操作与分析框架,通过ASM框架,我们可以动态的生成类或者增强已有类的功能。
  • ASM可以直接生成二进制.class文件,也可以在类被加载入java虚拟机之前动态改变现有类的行为。
  • java的二进制文件被存储在严格格式定义的.class文件里,这些字节码文件拥有足够的元数据信息用来表示类中的所有元素,包括类名称、方法、属性以及java字节码指令。ASM从字节码文件读入这些信息后,能够改变类行为、分析类的信息,甚至还可以根据具体的要求生成新的类。
  • ASM 通过树这种数据结构来表示复杂的字节码结构,因为需要处理字节码结构是固定的,所以可以利用Visitor(访问者) 设计模式来对树进行遍历,在遍历过程中对字节码进行修改。
我们能用ASM来做什么

(1):对现有的类进行分析,可做代码、权限检查
(2):生成新的Class文件
(3):对已有的类进行转换


字节码插桩在应用层能做什么?
  • 函数耗时监听
  • 无埋点框架
  • 隐私合规检测
  • 安装包防破解
查看文件字节码

1.在Plugins中搜索 ASM Bytecode Viewer,然后Restart。


2.编译之后,打开编译后的.class文件,使用ASM Bytecode Viewer命令。


3.生成字节码文件。


ASM Core API
  • ClassReader:这个类会将 .class 文件读入到 ClassReader 中的字节数组中,它的 accept 方法接受一个 ClassVisitor 实现类,并按照顺序调用 ClassVisitor 中的方法

  • ClassVisitor:主要负责访问类的成员信息。包括标记在类上的注解、类的构造方法、类的字段、类的方法、静态代码块等

  • ClassWriter:ClassWriter 是一个 ClassVisitor 的子类,是和 ClassReader 对应的类,ClassReader 是将 .class 文件读入到一个字节数组中,ClassWriter 是将修改后的类的字节码内容以字节数组的形式输出。

  • AdviceAdapter:MethodVisitor 是一个抽象类,当 ASM 的 ClassReader 读取到 Method 时就转入 MethodVisitor 接口处理。AdviceAdapter 是 MethodVisitor 的子类,封装了一些常用的方法,方便我们使用。我们重写其中的进入方法和退出方法的方法,并在其中加入我们要插入的字节码。AdviceAdapter其中几个重要方法如下:

void visitCode():表示 ASM 开始扫描这个方法
void onMethodEnter():进入这个方法
void onMethodExit():即将从这个方法出去
void onVisitEnd():表示方法扫描完毕

关键类ClassVisitor
  • visit
    /**
     * 可以拿到类的详细信息
     *
     * @param version jdk的版本: 52 代表jdk版本 1.8;51 代表jdk版本 1.7
     * @param access 类的修饰符:ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED、ACC_FINAL、ACC_SUPER
     * @param name 类的名称:以路径的形式表示 com/joker/demo/TestClass
     * @param signature 泛型信息:未定义泛型,则该参数为null
     * @param superName 表示当前类所继承的父类
     * @param interfaces 表示类所实现的接口列表
     */
    @Override
    void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces)
    }
  • visitAnnotation
/**
     * 当扫描器扫描到类注解声明时进行调用
     *
     * @param desc 注解类型(签名类型)
     * @param visible 注解是否可以在 JVM 中可见
     * @return
     */
    @Override
    AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        return super.visitAnnotation(desc, visible)
    }
  • visitField
/**
     * 当扫描器扫描到类中字段时进行调用
     *
     * @param access 修饰符
     * @param name 字段名
     * @param desc 字段类型
     * @param signature 泛型描述
     * @param value 默认值
     * @return
     */
    @Override
    FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
        return super.visitField(access, name, desc, signature, value)
    }
  • visitMethod
/**
     * 当扫描器扫描到类的方法时调用
     *
     * @param access 方法的修饰符
     * @param name 方法名
     * @param desc 方法签名
     * @param signature 表示泛型相关的信息
     * @param exceptions 表示将会抛出的异常,如果方法没有抛出异常,则参数为空
     * @return
     */
    @Override
    MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        return super.visitMethod(access, name, desc, signature, exceptions)
    }

方法的签名格式:
在ASM中不同的类型对应不同的代码,我们做JNI开发的时候,调用JNI的方法,传的参数类型也需要转成方法签名的格式,详细的对应关系如下表:


示例:完成指定方法耗时监测

对该类使用javac命令生成.class文件,放入当前目录下:

public class InjectTest {

    public void sayHello() {
        long l = System.currentTimeMillis();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        long cost = System.currentTimeMillis() - l;
        System.out.println("The cost time of " + cost + "ms");
    }
}

在执行类中做test检测方法,将InjectTest.class进行字节码插桩操作,再输出新的字节码文件。

    public void test() {
        try {
            File file = new File("src/main/java/com/example/asmbytecode/simpledemo/InjectTest.class");
            FileInputStream fis = new FileInputStream(file);
            //将class文件转成流
            ClassReader cr = new ClassReader(fis);
            //ClassWriter.COMPUTE_FRAMES 参数意义: 自动计算栈帧 和 局部变量表的大小
            ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);

            //执行分析
            cr.accept(new MyClassVisitor(Opcodes.ASM5, cw), ClassWriter.COMPUTE_FRAMES);

            System.out.println("Success!");

            //执行了插桩之后的字节码数据输出
            byte[] bytes = cw.toByteArray();
            FileOutputStream fos = new FileOutputStream("src/main/java/com/example/asmbytecode/simpledemo/InjectTest2.class");
            fos.write(bytes);
            fos.close();

        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

自定义Classvisitor:

    static class MyClassVisitor extends ClassVisitor {

        public MyClassVisitor(int api, ClassVisitor classVisitor) {
            super(api, classVisitor);
        }

        @Override
        public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
            //类似于动态代理的机制,会将执行的方法进行回调,然后在方法执行之前和之后做操作
            MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
            return new MyMethodVisitor(api, methodVisitor, access, name, descriptor);
        }

    }

自定义AdviceAdapter处理访问到的属性,包括注解,方法执行的开始和结束等。

    static class MyMethodVisitor extends AdviceAdapter {
        private int startTimeId = -1;
        /**
         * 用变量区分方法是否需要执行插桩
         */
        boolean inject = false;
        private String methodName = null;

        protected MyMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
            super(api, methodVisitor, access, name, descriptor);
            methodName = name;
        }

        @Override
        public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
            //descriptor为方法的注解类型 行如: Lcom/example/bytecodeProject/ASMTest
            //如果方法的注解为ASMTest,则执行插桩代码
            if (descriptor.equals("Lcom/example/asmbytecode/simpledemo/ASMTest")) {
                inject = true;
            }
            return super.visitAnnotation(descriptor, visible);
        }

        @Override
        protected void onMethodEnter() {  //代码插入到方法头部
            super.onMethodEnter();

            if (!inject) {
                return;
            }

            //在Java kotlin中写代码直接写,但是ASM写代码有最大区别,就是需要用方法签名的格式来写。

            //long l = System.currentTimeMillis();
            //要写如上一行代码的字节码,需要执行一个静态方法,类是System,方法名是currentTimeMillis,所以有如下代码:
            startTimeId = newLocal(Type.LONG_TYPE);
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitIntInsn(LSTORE, startTimeId);
        }

        @Override
        protected void onMethodExit(int opcode) { //代码插入到方法结尾
            super.onMethodExit(opcode);

            if (!inject) {
                return;
            }

            int durationId = newLocal(Type.LONG_TYPE);
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitVarInsn(LLOAD, startTimeId);
            mv.visitInsn(LSUB);
            mv.visitVarInsn(LSTORE, durationId);
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
            mv.visitInsn(DUP);
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
            mv.visitLdcInsn("The cost time of " + methodName + "() is ");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitVarInsn(LLOAD, durationId);
            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(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

        }
    }

指定了使用了ASMTest注解的方法,才会进行插桩。

留下问题:

如何在Android中找到所有class文件,而不是自己生成读取固定的文件?

参考:
https://www.jianshu.com/p/abba54baf617

Demo地址:

https://github.com/running-libo/ASMByteCode

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容