ASM的tree api对匿名线程的hook操作详解

目录
  • 背景
  • ASM介绍
    • class文件
    • fields
    • methods
    • InsnList
    • Signature
  • 实战部分
    • 解决“匿名”Thread
  • 最后

背景

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

ASM介绍

ASM其实就是一个可以编译字节码的工具,比如说我们日常开发会引入很多的类库对不对,又或者说我们的项目太大了,想修改某个点的时候,统一修改容易出错(比如隐私合规问题等),这个时候如果能有一个工具对生成后的class文件进行编辑的话,就非常方便我们进行后续的工作了。

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

class文件

我们常说的class文件,其实从二进制的角度出发,无非是分成以下几个部分:

可以看到,一个class文件其实就是由上图中的多个部分组成,而ASM,就是把这些结构进行了更进一步的抽象,对于class文件,其实就是抽象成asm中的class node类

对于一个class文件来说,通过以下就可以进行唯一性识别,分别是:version(版本),access(作用域,比如private等修饰符),name(名称),signature(泛型签名),superName(父类),interfaces(实现的接口),fields(当前的属性),methoss(当前的方法)。 所以如果想要修改一个class,我们修改对应的classNode即可

fields

属性,也是类非常重要的一部分,在字节码中,是如此定义的

对于一个属性,ASM将其抽象为FieldNode

对于一个属性field来说,通过以下就可以进行唯一性识别:access(作用域,跟class结构一样,比如private修饰),name(属性名称),desc(签名),signature(泛型签名),value(当前对应的数值)

methods

相比于属性,我们的方法结构更为复杂

相比于属性的单一,一个方法可能由多条指令组成而,一个方法的成功执行,也涉及到局部变量表跟操作数栈的配合。ASM中把方法抽象成这样一个定义 方法 = 方法头+方法体

方法头:即标识一个方法的基本属性,包括:access(作用域),name(方法名),desc(方法签名),signature(泛型签名),exceptions(方法可以抛出的异常)

方法体:相比于方法头,方法体的概念其实就比较简单了,其实方法体就是方法的各条指令的集合,主要包括instrutions(方法的指令集),tryCatchBlocks(异常的节点集),maxStack(操作数栈的最大深度),maxLocals(本地变量表的最大长度)

可以看到,方法其中的InsnList对象,是特指方法的指令集的抽象,这里继续讲解

InsnList

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

可以看到,主要的对象就是firstInsn,与lastInsn,代表着方法指令集的头指令与尾指令,每一个指令其实都被抽象成了AbstractInsnNode的子类,AbstractInsnNode定义了一条指令最基础的信息,我们可以看看这个类的子类

这里我们再看看我们最常用的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()字节码

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

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

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

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

当然,我们插入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相关用法与实战了,希望能有所帮助!

以上就是ASM的tree api对匿名线程的hook操作详解的详细内容,更多关于ASM tree api匿名线程hook的资料请关注我们其它相关文章!

(0)

相关推荐

  • android的got表HOOK实现代码

    概述 对于android的so文件的hook根据ELF文件特性分为:Got表hook.Sym表hook和inline hook等. 全局符号表(GOT表)hook,它是通过解析SO文件,将待hook函数在got表的地址替换为自己函数的入口地址,这样目标进程每次调用待hook函数时,实际上是执行了我们自己的函数. Androd so注入和函数Hook(基于got表)的步骤: 1.ptrace附加目标pid进程; 2.在目标pid进程中,查找内存空间(用于存放被注入的so文件的路径和so中被调用的函

  • Android开发AsmClassVisitorFactory使用详解

    目录 前言 AsmClassVisitorFactory 新的Extension 实战 ClassVisitor 实际代码分析 个人观点 前言 之前就和大家介绍过AGP(Android Gradle Plugin) 7.0.0版本之后Transform 已经过期即将废弃的事情.而且也简单的介绍了替换的方式是Transform Action,经过我这一阵子的学习和调研,发现只能说答对了一半吧.下面介绍个新东西AsmClassVisitorFactory. com.android.build.api

  • Android手机屏幕同步工具asm.jar

    有时候可能需要将手机上的一些操作投影出来,比如一些App Demo的展示等.其实,有专门的硬件设备能干这件事儿,但没必要专门为展示个Demo去花钱买硬件设备.正好,对于Android系统的手机,有一个开源的jar包能干这事儿:Android Screen Monitor(asm.jar) 步骤: 一 . 下载附件 asm.jar 官网 https://code.google.com/p/android-screen-monitor/ 二. 放到任意目录下, 这里放到D盘根目录; 将asm.jar

  • Android下hook点击事件的示例

    Hook是一种思想,也就是将原来的事件,替换到我们自己的事件,方便我们做一些切入处理.目的是不修改原来的代码,同时也避免遗漏的N多类里面处理. 最近需要在现有的app中设置统计埋点.去业务代码里埋的话似乎耦合度太高.所以决定使用hook的方法对事件进行埋点处理. 这里先记一下对点击事件hook的基本流程. 1.先建一个代理类实现View.OnClickListener,用来做点击后的后续处理. import android.view.View; /** * 实现点击监听 */ public cl

  • Android性能优化之plt hook与native线程监控详解

    目录 背景 native 线程创建 PLT PLT Hook xhook bhook plt hook总结 背景 我们在android超级优化-线程监控与线程统一可以知道,我们能够通过asm插桩的方式,进行了线程的监控与线程的统一,通过一系列的黑科技,我们能够将项目中的线程控制在一个非常可观的水平,但是这个只局限在java层线程的控制,如果我们项目中存在着native库,或者存在着很多其他so库,那么native层的线程我们就没办法通过ASM或者其他字节码手段去监控了,但是并不是就没有办法,还有

  • Android ASM插桩探索实战详情

    目录 前言 ASM的作用是什么? 如何使用ASM? 基本使用方式 自定义ClassVisitor ASM ByteCode Viewer 如何将ASM运用都我们的实际项目中来? 引入工程 Android Gradle Plugin 创建插件项目 配置插件 实现插件 发布插件 应用插件 Android Transform 方法节流 方法耗时日志 如何调试 发布线上的额外工作 插件项目的maven仓库 编译影响评估 Tips 总结 前言 我们都知道,在Android编译过程中,Java代码会被编译成

  • ASM的tree api对匿名线程的hook操作详解

    目录 背景 ASM介绍 class文件 fields methods InsnList Signature 实战部分 解决“匿名”Thread 最后 背景 看完本章,你将会学习到用ASM的tree api进行对匿名线程的hook操作,同时也能够了解到asm相关的操作和背景知识介绍!对于ASM插桩来说,可能很多人都不陌生了,但是大多数可能都停留在core api上,对于现在市面上的一些插桩库,其实很多都用tree api进行编写了,因为tree api的简单与明了的特性,也越来越成为许多开源库的选

  • JAVA线程sleep()和wait()详解及实例

    JAVA线程sleep()和wait()详解及实例 sleep 1.sleep是Thread的一个静态(static)方法.使得Runnable实现的线程也可以使用sleep方法.而且避免了线程之前相互调用sleep()方法,引发死锁. 2.sleep()执行时需要赋予一个沉睡时间.在沉睡期间(阻塞线程期间),CPU会放弃这个线程,执行其他任务.当沉睡时间到了之后,该线程会自动苏醒,不过此时线程不会立刻被执行,而是要等CPU分配资源,和其他线程进行竞争. 3.此外如果这个线程之前获取了一个机锁,

  • js匿名函数作为函数参数详解

    由衷的感叹,js真是烦. 学到现在,渐渐理解了什么是:语言都是通用的,没有好不好,只有擅长不擅长. 继承,多态,甚至指针,c能实现,c++,java有,javascript(和java是雷锋和雷峰塔的区别,名字上不知道坑了多少人)也能变通实现. 温故知新,今天又回味了一遍,匿名函数作为函数参数. 代码很短,五脏俱全. <!DOCTYPE html> <html lang="en"> <head> </head> <body>

  • java 打造阻塞式线程池的实例详解

    java 打造阻塞式线程池的实例详解 原来以为tiger已经自带了这种线程池,就是在任务数量超出时能够阻塞住投放任务的线程,主要想用在JMS消息监听. 开始做法: 在ThreadPoolExcecutor中代入new ArrayBlockingQueue(MAX_TASK). 在任务超出时报错:RejectedExecutionException. 后来不用execute方法加入任务,直接getQueue().add(task), 利用其阻塞特性.但是发现阻塞好用了,但是任务没有被处理.一看Qu

  • Java并发之传统线程同步通信技术代码详解

    本文研究的主要是Java并发之传统线程同步通信技术的相关代码示例,具体介绍如下. 先看一个问题: 有两个线程,子线程先执行10次,然后主线程执行5次,然后再切换到子线程执行10,再主线程执行5次--如此往返执行50次. 看完这个问题,很明显要用到线程间的通信了, 先分析一下思路:首先肯定要有两个线程,然后每个线程中肯定有个50次的循环,因为每个线程都要往返执行任务50次,主线程的任务是执行5次,子线程的任务是执行10次.线程间通信技术主要用到wait()方法和notify()方法.wait()方

  • Laravel5.5+ 使用API Resources快速输出自定义JSON方法详解

    从Laravel 5.5+开始,加入了API Resources这个概念. 我们先来看一下官网如何定义这个概念的: When building an API, you may need a transformation layer that sits between your Eloquent models and the JSON responses that are actually returned to your application's users. Laravel's resour

  • java中stringbuffer线程安全分析实例详解

    在对于一些类作用于线程时,安全系数高的线程更推荐大家使用,在尽可能的程度上降低程序出错的可能性.对于本篇所要提到的StringBuffer而言,在其缓冲区中有多个线程的存在,我们在查询其内部方法时发现了锁的存在.现在我们就StringBuffer线程.锁的应用.线程安全分析逐步带来介绍. 1.StringBuffer线程说明 Java.lang.StringBuffer线程安全的可变字符序列.一个类似于String的字符串缓冲区,但不能修改.虽然在任意时间点上它都包含某种特定的字符序列,但通过某

  • Java中内核线程理论及实例详解

    1.概念 内核线程是直接由操作系统内核控制的,内核通过调度器来完成内核线程的调度并负责将其映射到处理器上执行.内核态下的线程执行速度理论上是最高的,但是用户不会直接操作内核线程,而是通过内核线程的接口--轻量级进程来间接的使用内核线程.这种轻量级进程就是所谓的线程. 2.优点 由于内核线程的支持,每一个线程都是一个独立的单元,因此就算某一个线程挂掉了,也不会导致整个进程挂掉. 3.缺点 这种实现方式也存在局限性.由于是基于内核线程实现的,所以当涉及到线程的操作时(创建.运行.切换等)就涉及到系统

  • c# Thread类线程常用操作详解

    创建线程 线程是通过扩展 Thread 类创建的.扩展的 Thread 类调用 Start() 方法来开始子线程的执行. 下面的程序演示了这个概念: class ThreadCreationProgram { public static void CallToChildThread() { Console.WriteLine("Child thread starts"); } static void Main(string[] args) { ThreadStart childref

  • Java多线程之线程状态的迁移详解

    一.六种状态 java.lang.Thread 的状态分为以下 6 种,它们以枚举的形式,封装在了Thread类内部: NEW:表示线程刚刚创建出来,还未启动 RUNNABLE:可运行状态,该状态的线程可以是ready或running,唯一的决定因素是线程调度器 BLOCKED:阻塞,线程正在等待一个monitor锁以便进入一个同步代码块 WAITING:等待,一种挂起等待的状态.一个线程处于waiting是为了等待其他线程执行某个特定的动作. TIMED_WAITING:定时等待. TERMI

随机推荐