背景

看完本章,你将会学习到用ASM的tree api进行对匿名线程的hook操作,一起也能够了解到asm相关的操作和背景常识介绍!关于ASM插桩来说,或许许多人都不陌生了,但是大多数或许都停留在core api上,关于现在市面上的一些插桩库,其实许多都用tree api进行编写了,由于tree api的简单与明了的特性,也越来越成为许多开源库的挑选。(ASM有两套api类型,分别是core 和 tree)

学完ASM Tree api,再也不怕hook了

ASM介绍

ASM其实便是一个能够编译字节码的东西,比方说咱们日常开发会引进许多的类库对不对,又或许说咱们的项目太大了,想修正某个点的时分,统一修正容易出错(比方隐私合规问题等),这个时分假如能有一个东西对生成后的class文件进行编辑的话,就非常便利咱们进行后续的工作了。

本章主要介绍tree api,下文所说的ASM都是指tree api的操作哦,关于core api的介绍能够查看笔者曾经写过的文章Spider。

class文件

咱们常说的class文件,其实从二进制的视点动身,无非是分成以下几个部分:

学完ASM Tree api,再也不怕hook了
能够看到,一个class文件其实便是由上图中的多个部分组成,而ASM,便是把这些结构进行了更进一步的抽象,关于class文件,其实便是抽象成asm中的class node类

学完ASM Tree api,再也不怕hook了
关于一个class文件来说,经过以下就能够进行唯一性辨认,分别是:version(版别),access(效果域,比方private等润饰符),name(称号),signature(泛型签名),superName(父类),interfaces(完成的接口),fields(当时的特点),methoss(当时的办法)。 所以假如想要修正一个class,咱们修正对应的classNode即可

fields

特点,也是类非常重要的一部分,在字节码中,是如此界说的

学完ASM Tree api,再也不怕hook了
关于一个特点,ASM将其抽象为FieldNode

学完ASM Tree api,再也不怕hook了
关于一个特点field来说,经过以下就能够进行唯一性辨认:access(效果域,跟class结构相同,比方private润饰),name(特点称号),desc(签名),signature(泛型签名),value(当时对应的数值)

methods

比较于特点,咱们的办法结构更为复杂

学完ASM Tree api,再也不怕hook了
比较于特点的单一,一个办法或许由多条指令组成而,一个办法的成功执行,也涉及到局部变量表跟操作数栈的配合。ASM中把办法抽象成这样一个界说 办法 = 办法头+办法体

  • 办法头:即标识一个办法的根本特点,包含:access(效果域),name(办法名),desc(办法签名),signature(泛型签名),exceptions(办法能够抛出的反常)
    学完ASM Tree api,再也不怕hook了
  • 办法体:比较于办法头,办法体的概念其实就比较简单了,其实办法体便是办法的各条指令的调集,主要包含instrutions(办法的指令集),tryCatchBlocks(反常的节点集),maxStack(操作数栈的最大深度),maxLocals(本地变量表的最大长度)
    学完ASM Tree api,再也不怕hook了
    能够看到,办法其间的InsnList目标,是特指办法的指令集的抽象,这儿持续解说

InsnList

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

能够看到,主要的目标便是firstInsn,与lastInsn,代表着办法指令集的头指令与尾指令,每一个指令其实都被抽象成了AbstractInsnNode的子类,AbstractInsnNode界说了一条指令最基础的信息,咱们能够看看这个类的子类

学完ASM Tree api,再也不怕hook了
这儿咱们再看看咱们最常用的methodInsnNode

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

学完ASM Tree api,再也不怕hook了
那么咱们怎么才能把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(解释如上,调用者称号)放进去了,是不是就到达咱们想要的意图了!好啦!东西咱们又了,咱们要在哪里刺进呢?

学完ASM Tree api,再也不怕hook了
所以咱们的目标很明确,便是在init指令调用前刺进即可,asm也提供了insertBefore办法,提供在某个指令前刺进的快捷操作。

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

咱们看看最终刺进后的字节码

学完ASM Tree api,再也不怕hook了
当然,咱们刺进asm代码一般是在android提供给咱们的Transform阶段进行的(agp新版有改动,但是大体工作流程一致),所以咱们在transfrom中为了防止对类的过度搅扰,咱们还需求把不必要的阶段提早除掉!比方咱们只在new Thread操作,那么就把非Opcodes.INVOKESPECIAL的操作过滤即可。还有便是非init阶段(即非结构函数阶段)或许owner不为Thread类就能够提早过滤,不参与更改即可。

那咱们看到完好的代码(需求在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
    }
}

最终

看到这儿,应该能够了解到asm tree api相关用法与实战了,希望能有所帮助!

我正在参与技能社区创作者签约方案招募活动,点击链接报名投稿。