ASM插桩(java)

ASM插桩基础使用(java)以及知识点记录


常见功能

  • 日志埋点
  • 性能监控
  • 登录校验

Tips

  • 需要对jar包进行处理
  • 对android版本和gradle版本要求比较苛刻,java版建议在低gradle版本下进行
  • gradle4.2.2使用ASM Bytecode Viewer Support Kotlin,ASM Bytecode Viewer无效
  • java文件夹内的文件必须建立相应的路径,比如:com.stew.asmlife.xxxxxx.java

使用步骤

1.创建GP(Gradle Plugin 即 gradle插件)

1)创建library类型的module,除build.gradle和main文件夹,其余都删除
2)main中创建groovy和java文件夹
在groovy中创建XXX.groovy文件,也可以添加文件夹路径再创建XXX.groovy文件,区别在于直接创建不需要写package

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.gradle.api.Plugin;
import org.gradle.api.Project;
public class AsmLifePlugin implements Plugin<Project>{
void apply(Project project) {
System.out.println("#------AsmLifePlugin-------#")
}
}

package asm.life.plugin
import org.gradle.api.Plugin;
import org.gradle.api.Project;
public class AsmLifePlugin implements Plugin<Project>{
void apply(Project project) {
System.out.println("@------AsmLifePlugin-------@")
}
}

3)main中创建resources/META-INF/gradle-plugins/ABC.properties,路径必须这样,ABC可以随意,即插件名字,文件内容如下(区别和上面对应):
//implementation-class=asm.life.plugin.AsmLifePlugin
//implementation-class=AsmLifePlugin
4)build.gralle如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apply plugin: 'groovy'
apply plugin: 'maven'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation gradleApi()
implementation localGroovy()
implementation 'com.android.tools.build:gradle:4.2.2'
}
group='lifeplugin'
version='1.0.0'
uploadArchives {
repositories {
mavenDeployer {
//本地的Maven地址设置
repository(url: uri('../asm_life_repo'))
}
}
}

5)执行右侧gradle中的uploadArchives
6)app module的buildgradle如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
apply plugin: 'ABC'
buildscript {
repositories {
google()
jcenter()
//自定义插件maven地址
maven { url '../asm_life_repo' }
}
dependencies {
//加载自定义插件 group + module + version
classpath 'lifeplugin:asm_life_plugin:1.0.0'
}
}

如需改变插件配置,需要注释6)中的代码
7)执行右侧gradle中的assemble,结果如下:
Configure project :app
//——AsmLifePlugin——-

2.使用自定义Transform找到所有class文件

插件配置完之后需要使用Transform的api来找到class文件

Transform 可以被看作是 Gradle 在编译项目时的一个 task,在 .class 文件转换成 .dex 的流程中会执行这些 task,对所有的 .class 文件(可包括第三方库的 .class)进行转换,转换的逻辑定义在 Transform 的 transform 方法中。实际上平时我们在 build.gradle 中常用的功能都是通过 Transform 实现的,比如混淆(proguard)、分包(multi-dex)

创建自定义Transform,同样也是groovy文件
public class AsmLifeTrans extends Transform{}
需要实现抽象类 Transform 中的抽象方法,Transform 主要作用是检索项目编译过程中的所有文件。通过这几个方法,我们可以对自定义 Transform 设置一些遍历规则,具体如下:

  • getName:
    设置我们自定义的 Transform 对应的 Task 名称。Gradle 在编译的时候,会将这个名称显示在控制台上。比如:Task :app:transformClassesWithXXXForDebug。

  • getInputType:
    在项目中会有各种各样格式的文件,通过 getInputType 可以设置 AsmLifeTrans 接收的文件类型,此方法返回的类型是 Set<QualifiedContent.ContentType> 集合。ContentType 有以下 2 种取值,
    CLASSES:代表只检索 .class 文件;RESOURCES:代表检索 java 标准资源文件。

  • getScopes()
    这个方法规定自定义 Transform 检索的范围

  • isIncremental()
    表示当前 Transform 是否支持增量编译

  • transform()
    在 自定义Transform 中最重要的方法就是 transform()。在这个方法中,可以获取到两个数据的流向。
    inputs:inputs 中是传过来的输入流,其中有两种格式,一种是 jar 包格式,一种是 directory(目录格式)。
    outputProvider:outputProvider 获取到输出目录,最后将修改的文件复制到输出目录,这一步必须做,否则编译会报错。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    public class AsmLifeTrans extends Transform {
    @Override
    String getName() {
    return "AsmLifeTrans"
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
    return TransformManager.CONTENT_CLASS
    }

    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
    return TransformManager.PROJECT_ONLY
    }

    @Override
    boolean isIncremental() {
    return false
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
    transformInvocation.inputs.each {
    it.directoryInputs.each {
    System.out.println("find class: " + it.file.name)
    }
    }

    }
    }

    public class AsmLifePlugin implements Plugin<Project>{
    void apply(Project project) {
    System.out.println("###------AsmLifePlugin-------###")

    def android = project.extensions.getByType(AppExtension)
    AsmLifeTrans trans = new AsmLifeTrans()
    android.registerTransform(trans)
    }
    }

执行右侧gradle中的app内的build,可以看到所有class文件

3.遍历class文件,找到目标class,通过asm注入字节码

ASM 是一套开源框架,其中几个常用的 API 如下:
ClassReader:负责解析 .class 文件中的字节码,并将所有字节码传递给 ClassWriter。
ClassVisitor:负责访问 .class 文件中各个元素,当解析到某些特定结构时(比如类变量、方法),它会自动调用内部相应的 FieldVisitor 或者 MethodVisitor 的方法,进一步解析或者修改 .class 文件内容。
ClassWriter:继承自 ClassVisitor,它是生成字节码的工具类,负责将修改后的字节码输出为 byte 数组。

在 asm_life_plugin 的 build.gradle 中,添加对 ASM 的依赖
implementation 'org.ow2.asm:asm:9.1'
implementation 'org.ow2.asm:asm-commons:9.1'

在java文件夹内创建文件:

//处理class
public class AsmLifeClassVisitor extends ClassVisitor

//处理method
public class AsmLifeMethodVisitor extends MethodVisitor


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
visitInsn(int):访问一个零参数要求的字节码指令,如ACONST_NULL

visitIntInsn(int, int):访问一个需要零操作栈要求但需要有一个int参数的字节码指令,如BIPUSH

visitVarInsn(int, int):访问一个有关于局部变量的字节码指令,如ALOAD

visitTypeInsn(int, String):访问一个有关于类型的字节码指令,如CHECKCAST

visitFieldInsn(int, String, String, String):访问一个有关于字段的字节码,如PUTFIELD

visitMethodInsn(int, String, String, String, boolean):访问一个有关于方法调用的字节码,如INVOKESPECIAL

visitJumpInsn(int, Label):访问跳转字节码,如IFEQ

之后,是一些被包装好的字节码访问方法,这些方法都基于最基本的字节码指令,但是不需要我们自己用上面提到的那些方法直接调用字节码。

visitInvokeDynamicInsn(String, String, Handle, Object...):基于INVOKEDYNAMIC,动态方法调用,会在lambda表达式和方法引用里面说到

visitLdcInsn(Object):基于LDC、LDC_W和LDC2_W,将一个常量加载到操作栈用(详细见下文)

visitIincInsn(int, int):基于IINC、IINC_W,自增/减表达式

visitTableSwitchInsn(int, int, Label, Label...):基于TABLESWITCH,用于进行table-switch操作

visitLookupSwitchInsn(Label, int[], Label[]):基于LOOKUPSWITCH,用于进行lookup-switch操作

visitMultiANewArrayInsn(String, int):基于MULTIANEWARRAY,用于创建多重维度数组,如int[][]

详情可见:https://github.com/stewForAni/AndroidTest
参考文章:https://kaiwu.lagou.com/course/courseInfo.htm?courseId=67#/detail/pc?id=1858