Análisis MAPE

Uno: ¿Qué es ASM?

ASM es un marco general de análisis y manipulación de código de bytes de Java. Puede usarse directamente en forma binaria para modificar clases existentes o para generar clases dinámicamente. ASM proporciona algunos algoritmos de análisis y transformación de código de bytes comunes a partir de los cuales se pueden crear herramientas de análisis de código y transformación complejas personalizadas. ASM proporciona una funcionalidad similar a otros marcos de código de bytes de Java, pero con un enfoque en el rendimiento. Debido a que está diseñado e implementado para ser lo más pequeño y rápido posible, es muy adecuado para su uso en sistemas dinámicos (pero, por supuesto, también de forma estática, por ejemplo, en compiladores).
El párrafo anterior está tomado de la introducción oficial de ASM. En términos sencillos, ASM puede agregar, eliminar, modificar y verificar el código de bytes de Java existente, y prestar más atención al rendimiento.
Para obtener información más detallada sobre ASM, puede consultar su sitio web oficial:
asm.ow2.io/

1. Qué se puede hacer

(1): analice las clases existentes y realice comprobaciones de código y permisos

(2): generar un nuevo archivo de clase

(3): Convertir la clase existente

2. Componentes de MAPE

ASM esencialmente modifica el archivo de clase a través del código de bytes y luego lo escribe en el local en forma de flujo de bytes, por lo que al desarrollar, es necesario tener una cierta comprensión del código de bytes de Java.

Asm se divide en dos partes, Core API (modelo de eventos) y Tree API (modelo de objetos) .

(1) API de árbol (modelo de objeto)

Tree ApI se encapsula en función del modelo de objetos. Como sugiere el nombre, es un diagrama de árbol para describir una clase, incluidos varios nodos secundarios, como métodos, nodos de campo, etc., y se generarán nuevos nodos. En asm-tree.jar, nos enfocamos principalmente en las clases de herramientas de análisis ClassNode, MethodNode, FieldNode, que pueden ayudarnos a obtener rápidamente la estructura de Clase, modificar campos o modificar códigos de operación con el conjunto de herramientas de Introducción.

下图是一个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:将常量压入操作数栈

No es necesario memorizar, consulte juejin.cn/post/684490 cuando lo use...

Tres: combate real de ASM

1. Método estadístico AOP que requiere mucho tiempo.

AOP es la función de expansión horizontal del software sin afectar nuestro código comercial. Cuando usamos ASM para hacer una solución AOP, también debemos comprender los puntos de conocimiento de AOP:

Aspectos: Clases de interceptores, definición de puntos de corte y consejos.

Pointcut: un punto comercial específico para ser interceptado.

Notificación: un método en un aspecto que declara la ubicación de ejecución del método de notificación en la capa empresarial de destino.

Como se muestra en la figura a continuación, el aspecto puede entenderse como nuestro CostClassVisitor. El punto de entrada es que no pertenece a las clases abstractas e interfaz, y la descripción del método tiene información de CostTime. La notificación agrega código Asm antes y después del método . , realiza la instrumentación e imprime información que requiere mucho tiempo .

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. Verificación de cumplimiento de permisos ASM

Use ClassNode para analizar el archivo .class, analice el método, descubra si contiene el método de getNetworkType, si lo encuentra, inserte el código para imprimir la información.

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)
    }
}

Cuatro: Referencia ASM

blog.51cto.com/lsieun/2924… Aprendizaje básico de ASM

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

bytebuddy.net/ bytebuddy

blog.csdn.net/qq_27512671… Guía de salto en boxes de ASM

Supongo que te gusta

Origin juejin.im/post/7116442617218334728
Recomendado
Clasificación