ASM 浅析

一: ASM是什么

ASM是一个通用的Java字节码操作和分析框架。它可以直接以二进制形式用于修改现有类或动态生成类。ASM提供了一些常见的字节码转换和分析算法,可以从中构建定制的复杂转换和代码分析工具。ASM提供了与其他Java字节码框架类似的功能,但是侧重于性能。因为它的设计和实现是尽可能的小和尽可能快,所以它非常适合在动态系统中使用(但当然也可以以静态方式使用,例如在编译器中)。
上面这段话是摘自ASM官方的介绍,通俗的讲,ASM可以对现有的Java字节码进行增删改查,且更注重性能。
更多ASM详细的资料,可以参考其官网:
asm.ow2.io/

1. 能够做什么

(1):对现有的类进行分析,可做代码、权限检查

(2):生成新的Class文件

(3):对已有的类进行转换

2. ASM 组成部分

ASM 本质上是通过字节码修改 class 文件,再以 byte 流的形式写入到本地,所以在进行开发时,是需要对 Java 字节码有一定了解的。

Asm 分为两个部分,Core API(事件模型) 以及 Tree API(对象模型)

(1) Tree API(对象模型)

Tree ApI 是以对象模型为基础进行封装的,顾名思义是以 树状图 来描述一个类,包含多个子节点,例如方法、字段节点等等,又会产生新的节点。在 asm-tree.jar 中,我们主要关注 ClassNode、MethodNode、FieldNode 解析工具类,可以帮助我们快速获取 Class 结构,进行字段修改,或者用 Introduction 工具集做操作码修改。

下图是一个ClassNode对象包含信息,有版本号、访问标识符、名称、属性list、方法list等等,对应我们 ClassFile 结构。

(2) Core API(事件模型)

Core API 采用了访问者设计模式,其中最主要的有3个类 ClassReader、ClassVisitor、ClassWriter,通过调用 ClassReader 的 accept 方法接收一个访问者,然后通过它去访问类的各个组件,最后以 visitEnd 代表访问结束。ClassWriter 是负责将修改后的内容,重新组合生成新的.class文件。

二: ASM 与 ClassFile

上面我们都了解了 Asm 的介绍,是通过修改字节码,生成新的.class文件,那么这一 part 我们会讲到 ClassFile 是什么,以及 Asm 类方法与 ClassFile 对应关系。

1. Java 与 ClassFile

Java 的一大优势是“平台无关性”,那什么造就了它呢?

先来说说 Java 文件经过编译( javac )后,生成了.class文件,.class 是以一组8位字节为基础单位二进制流(ByteCode)。而 ByteCode 正是平台无关性的基石,Java 虚拟机不与任何语言绑定,是因为它只认这种二进制文件,并不关心 .class 的来源是何种语言,有了字节码,也使得 Java语言 与 Java虚拟器脱钩,这也可以解释为什么 kotlin 最后可以在 jvm 上运行。

Class File是严格按照顺序去进行排列的,中间没有添加任何分隔符,这使得整个 Class 文件中存储的内容几乎 全部是程序运行的必要数据,没有空隙存在

这是一个.class文件的示例,在命令行中运行 javac xx.java 编译后得到,可看到头部 cafe babe,也是 jvm 识别 class 文件的一个因素。.class 存储的为16进制数据,有兴趣的同学可以自己进行进制转换,在下节classFile结构速查时,可通过java提供的.class文件翻译工具,进行数据比对,加深自我理解。

cafe babe 0000 0037 0015 0a00 0500 100a
0004 0011 0800 1207 0013 0700 1401 0006
3c69 6e69 743e 0100 0328 2956 0100 0443
6f64 6501 000f 4c69 6e65 4e75 6d62 6572
5461 626c 6501 0003 6164 6401 0003 2829
4901 0004 6d61 696e 0100 1628 5b4c 6a61
7661 2f6c 616e 672f 5374 7269 6e67 3b29
5601 000a 536f 7572 6365 4669 6c65 0100
0f4c 6561 726e 436c 6173 732e 6a61 7661
0c00 0600 070c 000a 000b 0100 0e48 656c
6c6f e6a1 80e9 aa9c e5bb 9601 000a 4c65
6172 6e43 6c61 7373 0100 106a 6176 612f
6c61 6e67 2f4f 626a 6563 7400 2100 0400
0500 0000 0000 0300 0100 0600 0700 0100
0800 0000 1d00 0100 0100 0000 052a b700
01b1 0000 0001 0009 0000 0006 0001 0000
0005 0009 000a 000b 0001 0008 0000 0028
0002 0002 0000 0008 043b 053c 1a1b 60ac
0000 0001 0009 0000 000e 0003 0000 0009
0002 000a 0004 000b 0009 000c 000d 0001
0008 0000 0028 0001 0003 0000 0008 b800
023c 1203 4db1 0000 0001 0009 0000 000e
0003 0000 0011 0004 0012 0007 0014 0001
000e 0000 0002 000f 

Class文件是一组以8个字节为基础单位的二进制字节流,当遇到需要占用 8 位字节以上空间的数据项 时,则会按照高位在前的方式分割成若干个 8 位字节进行存储。(高位在前指 ”Big-Endian",即指最高位字节在地址最低位,最低位字节在地址最高位的顺序来存储数据,而 X86 等处理器则是使用了相反的 “Little-Endian” 顺序来存储数据

示例:0x1234abcd BigEndian则按0xcd 0xab 0x34...顺序存储,Little-Endian则按0x12 0x34...顺序存储

2. ClassFile 结构速查

命令行输入 javap -v xx.class 可查看Jvm解析的classFile结构,可以理解为帮我们翻译.class文件的内容。

根据 JVM 定义的规范,Class 文件格式以类似 C 语言结构体的伪结构来表示存储数据,这种伪结构只有两种类型:无符号数和表

  ClassFile { 
        u4 magic;  // 魔法数字,表明当前文件是.class文件,固定0xCAFEBABE
        u2 minor_version; // 分别为Class文件的副版本和主版本
        u2 major_version; 
        u2 constant_pool_count; // 常量池计数
        cp_info constant_pool[constant_pool_count-1];  // 常量池内容
        u2 access_flags; // 类访问标识
        u2 this_class; // 当前类
        u2 super_class; // 父类
        u2 interfaces_count; // 实现的接口数
        u2 interfaces[interfaces_count]; // 实现接口信息
        u2 fields_count; // 字段数量
        field_info fields[fields_count]; // 包含的字段信息 
        u2 methods_count; // 方法数量
        method_info methods[methods_count]; // 包含的方法信息
        u2 attributes_count;  // 属性数量
        attribute_info attributes[attributes_count]; // 各种属性
    }

无符号数可以用来 描述数字、索引引用、数量值或者按照UTF-8 码构成字符串值。 u4 表示占用4个字节,u2 表示占用1个字节,而cp_info、field_info、method_info、attribute_info表示的更复杂一点,但是他们本身也是以u2、u1等结构来存储的。

Java定义了许多指令,下面对指令进行归类:
push:将给定的数压入操作数栈
load:将局部变量表中的第n个数压入操作数栈
store:从操作数栈顶弹出一个元素保存到局部变量表的第n个位置
dup:duplicate复制栈顶元素并且继续压入栈
pop:从栈顶弹出,直接抛弃
iadd、ladd、dadd、fadd :相加
isub、lsub、dsub、fsub:相减
imul、lmul、dmul、fmul:相乘
idiv、ldiv、ddiv、fdiv:相除
irem、lrem、drem、frem:取余
ineg、lneg、dneg、fneg:取反
iinc:自增
new、newarray:创建指令
getfield、putfield、getstatic、putstatic:操作类field
ifeq、iflt、ifle......:条件跳转
invokeXXX函数调用指令
monitorenter、monitorexit:同步指令
const,、ldc:将常量压入操作数栈

不需要硬记,用到的时候查 juejin.cn/post/684490…

三: ASM 实战

1. AOP 方法统计耗时

AOP 是能在不影响我们业务代码情况下,做软件的横向扩展功能。在我们用 ASM 去做一个 AOP 方案时,也需要了解 AOP 知识点:

切面: 拦截器类,定义切点以及通知

切点: 具体拦截的某个业务点。

通知: 切面当中的方法,声明通知方法在目标业务层的执行位置。

如下图,切面可理解为我们的 CostClassVisitor,切入点为不属于 abstract、interface 类,且方法描述带有 CostTime 信息,通知则在方法的前后增加Asm代码,进行插桩,打印耗时信息

class CostClassVisitor(cv: ClassVisitor) : ClassVisitor(Opcodes.ASM7, cv) {

    private var className: String? = null
    private var isABSClass = false
    private var isChange = false

    override fun visit(
        version: Int,
        access: Int,
        name: String?,
        signature: String?,
        superName: String?,
        interfaces: Array<out String>?,
    ) {
        super.visit(version, access, name, signature, superName, interfaces)
        this.className = name
        //抽象方法或者接口 不做方法耗时计算
        if (access and Opcodes.ACC_ABSTRACT > 0 || access and Opcodes.ACC_INTERFACE > 0) {
            this.isABSClass = true
        }
    }

    override fun visitMethod(
        access: Int,
        name: String?,
        descriptor: String?,
        signature: String?,
        exceptions: Array<out String>?,
    ): MethodVisitor {
        if (isABSClass) return super.visitMethod(access, name, descriptor, signature, exceptions)
        var mv = cv.visitMethod(access, name, descriptor, signature, exceptions)
//        println("visitMethod $name $descriptor")
        isChange = false
        mv = object : AdviceAdapter(Opcodes.ASM5, mv, access, name, descriptor) {

            private var currentTimeVarIndex = 1

            override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor {
                descriptor?.let {
                    isChange = it.contains("CostTime")  // 判断方法描述带 CostTime
                }
                return super.visitAnnotation(descriptor, visible)
            }

            override fun onMethodEnter() {
                super.onMethodEnter()
                if (isChange) {
                    println("visitMethod Enter $name $descriptor")
                    mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                    mv.visitLdcInsn("=========start");
                    mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
                    currentTimeVarIndex = newLocal(Type.LONG_TYPE); //创建一个局部变量
                    mv.visitMethodInsn(Opcodes.INVOKESTATIC,
                        "java/lang/System",
                        "currentTimeMillis",
                        "()J",
                        false);
                    mv.visitVarInsn(LSTORE, currentTimeVarIndex);
                }


            }

            override fun onMethodExit(opcode: Int) {
                super.onMethodExit(opcode)
                if (isChange) {
                    //这里相当于 System.out.println(System.currentTimeMillis() - currentTime);
                    mv.visitFieldInsn(Opcodes.GETSTATIC,
                        "java/lang/System",
                        "out",
                        "Ljava/io/PrintStream;");
                    //INVOKESTATIC java/lang/System.currentTimeMillis ()J
                    mv.visitMethodInsn(Opcodes.INVOKESTATIC,
                        "java/lang/System",
                        "currentTimeMillis",
                        "()J",
                        false);
                    //LLOAD 0
                    mv.visitVarInsn(Opcodes.LLOAD, currentTimeVarIndex);
                    //LSUB
                    mv.visitInsn(Opcodes.LSUB);
                    //INVOKEVIRTUAL java/io/PrintStream.println (J)V
                    mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
                        "java/io/PrintStream",
                        "println",
                        "(J)V",
                        false);
                    mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                    mv.visitLdcInsn("=========end");
                    mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
                }
            }

        }
        return mv
    }
}

2. ASM 权限合规检查

使用 ClassNode 进行.class 文件解析,进行method解析,找到是否含有 getNetworkType 的方法,如果已找到,则插入代码打印信息。

class TestPrivateTransformer : ClassTransformer {

    override fun transform(context: TransformContext, klass: ClassNode): ClassNode {
        val hookedClassName = klass.name
        var hookedMethodName = ""
        val iterator = klass.methods.iterator()
        while (iterator.hasNext()) {
            val method = iterator.next()
            method.instructions?.iterator()?.forEach {
            	// 查找TelephonyManager.getNetworkType
                if (it.opcode == Opcodes.INVOKEVIRTUAL && it is MethodInsnNode) {
                    if (it.owner == "android/telephony/TelephonyManager" && it.name == "getNetworkType") {
                        hookedMethodName = method.name

                        println("====find private success : $hookedClassName  /  $hookedMethodName")
                    }
                }
            }
			// 如果hookedMethodName不为空那么表示找到了,那么就插入代码
            if (!TextUtils.isEmpty(hookedMethodName)) {
                method?.instructions?.iterator()?.asIterable()?.filter {
                    it.opcode == Opcodes.RETURN
                }?.forEach {
                    method.instructions?.apply {
                        insertBefore(it, LdcInsnNode(hookedClassName))
                        insertBefore(it, LdcInsnNode(hookedMethodName))
                        insertBefore(it, LdcInsnNode("android/telephony/TelephonyManager"))
                        insertBefore(it, LdcInsnNode("getNetworkType"))
                        insertBefore(
                            it,
                            MethodInsnNode(
                                Opcodes.INVOKESTATIC,
                                "com/remote/neacy/PrivateUtil",
                                "reportPrivateApi",
                                "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",
                                false
                            )
                        )
                    }
                }
            }
        }

        return super.transform(context, klass)
    }
}

四: ASM 参考

blog.51cto.com/lsieun/2924… ASM Core 学习

xingyun.xiaojukeji.com/docs/dokit/… didi

bytebuddy.net/ bytebuddy

blog.csdn.net/qq_27512671… ASM 跳坑指南

猜你喜欢

转载自juejin.im/post/7116442617218334728
ASM