Après avoir appris l'api ASM Tree, vous n'aurez plus peur des hooks

Contexte

Après avoir lu ce chapitre, vous apprendrez à utiliser l'api arborescente d'ASM pour effectuer des opérations de hook sur des threads anonymes, et vous pourrez également en apprendre davantage sur les opérations liées à asm et les connaissances de base ! Pour l'instrumentation ASM, de nombreuses personnes peuvent la connaître, mais la plupart d'entre elles peuvent rester sur l'api principale.Pour certaines bibliothèques d'instrumentation sur le marché, beaucoup d'entre elles sont écrites en api arborescente, car les fonctionnalités simples et claires de l'api arborescente sont de plus en plus devenir le choix de nombreuses bibliothèques open source. (ASM a deux ensembles de types d'API, noyau et arbre)

image.png

Présentation de l'ASM

ASM est en fait un outil capable de compiler du bytecode. Par exemple, nous allons introduire beaucoup de bibliothèques de classes dans notre développement quotidien, n'est-ce pas ? Ou notre projet est trop gros. Lorsque nous voulons modifier un certain point, il est facile de faire des erreurs en modification unifiée (telles que les problèmes de conformité à la confidentialité, etc.), à ce stade, s'il existe un outil pour modifier le fichier de classe généré, il nous sera très pratique d'effectuer le travail de suivi.

Ce chapitre présente principalement l'api tree. L'ASM mentionné ci-dessous fait référence au fonctionnement de l'api tree . Pour l'introduction de l'api core, vous pouvez consulter l'article Spider écrit par l'auteur .

fichier de classe

Le fichier de classe que nous disons souvent est en fait divisé en les parties suivantes d'un point de vue binaire : comme image.pngvous pouvez le voir, un fichier de classe est en fait composé de plusieurs parties dans la figure ci-dessus, et ASM doit réaliser ces structures. , pour le fichier de classe, il est en fait abstrait dans la classe de nœud de classe dans asm

image.pngPour un fichier de classe, il peut être identifié de manière unique par les éléments suivants : version (version), accès (portée, tels que des modificateurs tels que privé), nom (nom), signature (signature générique), superNom (classe parent), interfaces ( interfaces implémentées), champs (propriétés courantes), méthodes (méthodes courantes) . Donc, si nous voulons modifier une classe, nous pouvons modifier le classNode correspondant

des champs

Les propriétés, également une partie très importante de la classe, sont définies dans le bytecode en tant que telles

image.pngPour une propriété, ASM l'abstrait en tant que FieldNode

image.pngPour un champ d'attribut, il peut être identifié de manière unique par les éléments suivants : accès (portée, identique à la structure de classe, telle que modification privée), nom (nom d'attribut), desc (signature), signature (signature générique), valeur (le valeur correspondante)

méthodes

Par rapport aux attributs, notre structure de méthode est plus complexe image.png. Par rapport à l'attribut unique, une méthode peut être composée de plusieurs instructions. L'exécution réussie d'une méthode implique également la coopération de la table de variables locales et de la pile d'opérandes. Dans ASM, la méthode est abstraite dans une telle définition method = method header + method body

  • En-tête de méthode : Les attributs de base qui identifient une méthode, y compris : access (portée), name (nom de la méthode), desc (signature de la méthode), signature (signature générique), exceptions (exceptions que la méthode peut lever)

image.png

  • Corps de la méthode : par rapport à l'en-tête de la méthode, le concept du corps de la méthode est en fait relativement simple. En fait, le corps de la méthode est une collection de diverses instructions de la méthode, comprenant principalement des instructions (le jeu d'instructions de la méthode), tryCatchBlocks ( l'ensemble de nœuds anormal), maxStack (profondeur maximale de la pile d'opérandes), maxLocals (longueur maximale de la table de variables locales)

image.pngOn peut voir que l'objet InsnList dans la méthode est une abstraction du jeu d'instructions de la méthode spécifique, qui est expliquée ici.

InsnListe

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

On peut voir que les objets principaux sont firstInsn et lastInsn, qui représentent les instructions de tête et de queue du jeu d'instructions de la méthode. Chaque instruction est en fait abstraite dans une sous-classe de AbstractInsnNode. AbstractInsnNode définit les informations les plus élémentaires d'une instruction, nous vous peut regarder les sous-classes de cette classe

image.pngIci, nous jetons un coup d'œil à notre méthode InsnNode la plus couramment utilisée

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()字节码

image.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(解释如上,调用者名称)放进去了,是不是就达到我们想要的目的了!好啦!东西我们又了,我们要在哪里插入呢?

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

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

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

image.pngBien sûr, nous insérons généralement du code asm dans l'étape Transform qui nous est fournie par Android (la nouvelle version d'agp a changé, mais le flux de travail général est le même), donc afin d'éviter une interférence excessive avec la classe dans transfrom, nous avons également besoin d'en mettre inutile L'étape est éliminée au début ! Par exemple, si nous opérons uniquement sur new Thread, nous pouvons filtrer les opérations qui ne sont pas Opcodes.INVOKESPECIAL. Il existe également une étape non init (c'est-à-dire une étape non constructeur) ou si le propriétaire n'est pas une classe Thread, il peut être filtré à l'avance sans participer au changement.

Ensuite, nous voyons le code complet (le code qui doit être exécuté dans 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
    }


}

enfin

En voyant cela, vous devriez être en mesure de comprendre l'utilisation connexe et le combat réel de l'api asm tree, j'espère que cela pourra être utile!

Je participe au recrutement du programme de signature de créateurs de la communauté technologique Nuggets, cliquez sur le lien pour vous inscrire et soumettre .

Je suppose que tu aimes

Origine juejin.im/post/7121643784638562317
conseillé
Classement