Después de aprender ASM Tree api, ya no tendrás miedo de los ganchos

antecedentes

Después de leer este capítulo, aprenderá a usar la API de árbol de ASM para realizar operaciones de enganche en subprocesos anónimos, ¡y también podrá aprender sobre operaciones relacionadas con ASM y conocimientos previos! Para la instrumentación ASM, muchas personas pueden estar familiarizadas con él, pero la mayoría de ellos pueden permanecer en la API central.Para algunas bibliotecas de instrumentación en el mercado, muchas de ellas están escritas en la API del árbol, porque las características simples y claras de la API del árbol son cada vez más convirtiéndose en la elección de muchas bibliotecas de código abierto. (ASM tiene dos conjuntos de tipos de API, núcleo y árbol)

imagen.png

Introducción a la MAPE

ASM es en realidad una herramienta que puede compilar código de bytes. Por ejemplo, introduciremos muchas bibliotecas de clases en nuestro desarrollo diario, ¿no? O nuestro proyecto es demasiado grande. Cuando queremos modificar un punto determinado, es fácil cometer errores. en la modificación unificada (como cuestiones de cumplimiento de Privacidad, etc.), en este momento, si existe una herramienta para editar el archivo de clase generado, nos será muy conveniente para realizar el trabajo de seguimiento.

Este capítulo presenta principalmente la API de árbol. El ASM que se menciona a continuación se refiere al funcionamiento de la API de árbol . Para la introducción de la API principal, puede consultar el artículo Spider escrito por el autor .

archivo de clase

El archivo de clase que solemos decir en realidad está dividido en las siguientes partes desde un punto de vista binario: Como imagen.pngpuede ver, un archivo de clase en realidad está compuesto de varias partes en la figura anterior, y ASM debe llevar a cabo estas estructuras. , para el archivo de clase, en realidad se abstrae en la clase de nodo de clase en asm

imagen.pngPara un archivo de clase, se puede identificar de forma única por lo siguiente: versión (versión), acceso (ámbito, como modificadores como privado), nombre (nombre), firma (firma genérica), superNombre (clase principal), interfaces ( interfaces implementadas), campos (propiedades actuales), métodos (métodos actuales) . Entonces, si queremos modificar una clase, podemos modificar el classNode correspondiente

campos

Las propiedades, también una parte muy importante de la clase, se definen en el código de bytes como tal

imagen.pngPara una propiedad, ASM la abstrae como FieldNode

imagen.pngPara un campo de atributo, se puede identificar de forma única por lo siguiente: acceso (alcance, igual que la estructura de clase, como modificación privada), nombre (nombre de atributo), desc (firma), firma (firma genérica), valor (el valor correspondiente)

métodos

En comparación con los atributos, la estructura de nuestro método es más compleja imagen.png. En comparación con el atributo único, un método puede estar compuesto por varias instrucciones. La ejecución exitosa de un método también implica la cooperación de la tabla de variables locales y la pila de operandos. En ASM, el método se abstrae en una definición de este tipo: método = encabezado del método + cuerpo del método

  • Encabezado del método: los atributos básicos que identifican un método, incluidos: acceso (ámbito), nombre (nombre del método), desc (firma del método), firma (firma genérica), excepciones (excepciones que puede generar el método)

imagen.png

  • Cuerpo del método: en comparación con el encabezado del método, el concepto del cuerpo del método es relativamente simple. De hecho, el cuerpo del método es una colección de varias instrucciones del método, que incluye principalmente instrucciones (el conjunto de instrucciones del método), tryCatchBlocks ( el conjunto de nodos anormales), maxStack (profundidad máxima de la pila de operandos), maxLocals (longitud máxima de la tabla de variables locales)

imagen.pngSe puede ver que el objeto InsnList en el método es una abstracción del conjunto de instrucciones del método específico, que se explica aquí.

InsnList

public class InsnList implements Iterable<AbstractInsnNode> {
    private int size;
    private AbstractInsnNode firstInsn;
    private AbstractInsnNode lastInsn;
    AbstractInsnNode[] cache;
    ...

Se puede ver que los objetos principales son firstInsn y lastInsn, que representan las instrucciones iniciales y finales del conjunto de instrucciones del método. Cada instrucción se abstrae en una subclase de AbstractInsnNode. AbstractInsnNode define la información más básica de una instrucción, nosotros puede mirar las subclases de esta clase

imagen.pngAquí echamos un vistazo a nuestro método InsnNode más utilizado.

public class MethodInsnNode extends AbstractInsnNode {

  /**
   * The internal name of the method's owner class (see {@link
   * org.objectweb.asm.Type#getInternalName()}).
   *
   * <p>For methods of arrays, e.g., {@code clone()}, the array type descriptor.
   */
  public String owner;

  /** The method's name. */
  public String name;

  /** The method's descriptor (see {@link org.objectweb.asm.Type}). */
  public String desc;

  /** Whether the method's owner class if an interface. */
  public boolean itf;

这个就是一个普通方法指令最根本的定义了,owner(方法调用者),name(方法名称),desc(方法签名)等等,他们都有着相似的结构,这个也是我们接下来会实战的重点。

Signature

嗯!我们最后介绍一下这个神奇的东西!不知道大家在看介绍的时候,有没有一脸疑惑,这个我解释为泛型签名,这个跟desc(函数签名)参数有什么区别呢?当然,这个不仅仅在函数上有出现,在属性,类的结构上都有出现!是不是非常神奇!

其实Signature属性是在JDK 1.5发布后增加到了Class文件规范之中,它是一个可选的定长属性, 可以出现于类、属性表和方法表结构的属性表中。我们想想看,jdk1.5究竟是发生什么了!其实就是对泛型的支持,那么1.5版本之前的sdk怎么办,是不是也要进行兼容了!所以java标准组就想到了一个折中的方法,就是泛型擦除,泛型信息编译(类型变量、参数化类型)之后 都通通被擦除掉,以此来进行对前者的兼容。那么这又导致了一个问题,擦除的泛型信息有时候正是我们所需要的,所以Signature就出现了,把这些泛型信息存储在这里,以提供运行时反射等类型信息的获取!实际上可以看到,我们大部分的方法或者属性这个值都为null,只有存在泛型定义的时候,泛型的信息才会被存储在Signature里面

实战部分

好啦!有了理论基础,我们也该去实战一下,才不是口水文!以我们线程优化为例子,在工作项目中,或者在老项目中,可能存在大多数不规范的线程创建操作,比如直接new Thread等等,这样生成的线程名就会被赋予默认的名字,我们这里先把这类线程叫做“匿名线程”!当然!并不是说这个线程没有名字,而是线程名一般是“Thread -1 ”这种没有额外信息含量的名字,这样对我们后期的线程维护会带来很大的干扰,时间长了,可能就存在大多数这种匿名线程,有可能带来线程创建的oom crash!所以我们的目标是,给这些线程赋予“名字”,即调用者的名字

解决“匿名”Thread

为了达到这个目的,我们需要对thread的构造有一个了解,当然Thread的构造函数有很多,我们举几个例子

public Thread(String name) {
    init(null, null, name, 0);
}
public Thread(ThreadGroup group, String name) {
    init(group, null, name, 0);
}

可以看到,我们Thread的多个构造函数,最后一个参数都是name,即Thread的名称,所以我们的hook点是,能不能在Thread的构造过程,调用到有name的构造函数是不是就可以实现我们的目的了!我们再看一下普通的new Thread()字节码

imagen.png 那么我们怎么才能把new Thread()的方式变成 new Thread(name)的方式呢?很简单!只需要我们把init的这条指令变成有参的方式就可以了,怎么改变呢?其实就是改变desc!方法签名即可,因为一个方法的调用,就是依据方法签名进行匹配的。我们在函数后面添加一个string的参数即可

node是methidInsnNode
def desc =
        "${node.desc.substring(0, r)}Ljava/lang/String;${node.desc.substring(r)}"
node.desc = desc

那么这样我们就可以完成了吗,非也非也,我们只是给方法签名对加了一个参数,但是这并不代表我们函数就是这么运行的!因为方法参数的参数列表中的string参数我们还没放入操作数栈呢!那么我们就可以构造一个string参数放入操作数栈中,这个指令就是ldc指令啦!asm为我们提供了一个类是LdcInsnNode,我们可以创建一个该类对象即可,构造参数需要传入一个字符串,那么这个就可以把当前方法的owner(解释如上,调用者名称)放进去了,是不是就达到我们想要的目的了!好啦!东西我们又了,我们要在哪里插入呢?

imagen.png 所以我们的目标很明确,就是在init指令调用前插入即可,asm也提供了insertBefore方法,提供在某个指令前插入的便捷操作。

method.instructions.insertBefore(
        node,
        new LdcInsnNode(klass.name)
)

我们看看最后插入后的字节码

imagen.pngPor supuesto, generalmente insertamos código asm en la etapa Transform que nos proporciona Android (la nueva versión de agp ha cambiado, pero el flujo de trabajo general es el mismo), por lo que para evitar una interferencia excesiva con la clase en transfrom, también necesidad de poner innecesarios El escenario se elimina temprano! Por ejemplo, si solo operamos en un hilo nuevo, podemos filtrar operaciones que no sean Opcodes.INVOKESPECIAL. También hay una etapa que no es de inicio (es decir, una etapa que no es de constructor) o, si el propietario no es una clase Thread, se puede filtrar por adelantado sin participar en el cambio.

Luego vemos el código completo (el código que debe ejecutarse en Transform)

static void transform(ClassNode klass) {
    println("ThreadTransformUtils")
    // 这里只处理Thread
    klass.methods?.forEach { methodNode ->
        methodNode.instructions.each {
            // 如果是构造函数才继续进行
            if (it.opcode == Opcodes.INVOKESPECIAL) {
                transformInvokeSpecial((MethodInsnNode) it, klass, methodNode)
            }
        }
    }

}


private static void transformInvokeSpecial(MethodInsnNode node, ClassNode klass, MethodNode method) {
    // 如果不是构造函数,就直接退出
    if (node.name != "<init>" || node.owner != THREAD) {
        return
    }
    println("transformInvokeSpecial")
    transformThreadInvokeSpecial(node, klass, method)

}

private static void transformThreadInvokeSpecial(
        MethodInsnNode node,
        ClassNode klass,
        MethodNode method
) {
    switch (node.desc) {
    // Thread()
        case "()V":
            // Thread(Runnable)
        case "(Ljava/lang/Runnable;)V":
            method.instructions.insertBefore(
                    node,
                    new LdcInsnNode(klass.name)
            )
            def r = node.desc.lastIndexOf(')')
            def desc =
                    "${node.desc.substring(0, r)}Ljava/lang/String;${node.desc.substring(r)}"
            // println(" + $SHADOW_THREAD.makeThreadName(Ljava/lang/String;Ljava/lang/String;) => ${this.owner}.${this.name}${this.desc}: ${klass.name}.${method.name}${method.desc}")
            println(" * ${node.owner}.${node.name}${node.desc} => ${node.owner}.${node.name}$desc: ${klass.name}.${method.name}${method.desc}")
            node.desc = desc
            break
    }


}

Al final

Al ver esto, debería poder comprender el uso relacionado y el combate real de la api del árbol asm, ¡espero que pueda ser útil!

Estoy participando en el reclutamiento del programa de firma de creadores de la Comunidad Tecnológica de Nuggets, haga clic en el enlace para registrarse y enviar .

Supongo que te gusta

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