ASM字节码插桩初体验

2,400 阅读4分钟

一直觉得字节码插桩是不可高攀的知识,最近工作不太忙,于是下决心去学习一下字节码插桩;打开我收藏了很久的文章开始学习,下面几个是我之前收藏的文章(收藏==学会)

文章中的AMS应该是ASM, 我搞错了,写文章的时候没注意,跟AMS(ActivityManagerService)搞混了一直打成AMS;在开头纠错下;

下面开始正题,就简单的实现一下在Test.java类中的test()方法中插入一些代码;由于是初体验,就不讲很难的,怎么简单怎么来。小白看完上面文章还是有点懵逼,刚开始我也是很懵逼,实践是最好的方式去掌握新的技能知识的方式,于是我就找着博客踩坑自己体验了一次ASM字节码插桩。下面我就一步一步的从0开始新建一个项目来展示字节码插桩,test方法执行前后插入一些代码。

项目源码

1.新建工程,新建一个插件module

微信截图_20220129155221.png

pluginams module只保留main文件夹,不需要资源目录,额外需要新建一个resource目录,后面会讲干嘛用的。

2.插件的build

apply plugin: 'kotlin'
//下面两个很重要
apply plugin: 'groovy'
apply plugin: 'maven'

repositories {
    mavenCentral()
}
dependencies {
    //gradle sdk
    implementation gradleApi()
    //groovy sdk
    implementation localGroovy()
    //引入asm
    implementation 'org.ow2.asm:asm:7.2'
    implementation 'org.ow2.asm:asm-commons:7.2'
    implementation 'org.ow2.asm:asm-analysis:7.2'
    implementation 'org.ow2.asm:asm-util:7.2'
    implementation 'org.ow2.asm:asm-tree:7.2'
    implementation 'com.android.tools.build:gradle:4.1.2', {
        exclude group:'org.ow2.asm'
    }
}

//group和version在后面引用自定义插件的时候会用到
group='com.cb.test.amsplugin'
version='1.0.0'

//上传到本地maven残酷
uploadArchives {
    repositories {
        mavenDeployer {
            //本地的Maven地址:当前工程下
            repository(url: uri('./ams_plugin'))
        }
    }
}

3.编写一个gradle插件类

插件主要作用就是遍历整个项目的文件,包含jar包

```
package com.cb.test.pluginams

import com.android.build.api.transform.Format
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformInvocation
import com.android.build.gradle.AppExtension
import com.android.build.gradle.internal.pipeline.TransformManager
import com.android.utils.FileUtils
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import java.io.FileOutputStream
```
class TestMethodPlugin : Transform(), Plugin<Project> {
    
    override fun apply(target: Project) {
        val appExtension = target.extensions.getByType(AppExtension::class.java)
        appExtension.registerTransform(this)
    }

    override fun getName(): String {
        return "TestMethodPlugin"
    }

    override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
        return TransformManager.CONTENT_CLASS
    }

    override fun isIncremental(): Boolean {
        return false
    }

    override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    override fun transform(transformInvocation: TransformInvocation?) {
        val inputs = transformInvocation?.inputs
        val out = transformInvocation?.outputProvider
        inputs?.forEach { transformInput ->
            //项目目录  遍历项目目录
            transformInput.directoryInputs.forEach { directoryInput ->
                if (directoryInput.file.isDirectory) {
                    FileUtils.getAllFiles(directoryInput.file).forEach {
                        val file = it
                        val name = file.name
                        //去掉不需要的.class文件
                        if (name.endsWith(".class") && name != "R.class" 
                        && !name.startsWith("R$") && name != "BuildConfig.class") {
                            //找到需要的。class文件,进行一系列读写操作
                            val classPath = file.absolutePath
                            val cr = ClassReader(file.readBytes())
                            val cw = ClassWriter(cr, ClassWriter.COMPUTE_MAXS)
                            val visitor = TestClassVisitor(cw)
                            cr.accept(visitor, ClassReader.SKIP_FRAMES)

                            val byte = cw.toByteArray();
                            val fos = FileOutputStream(classPath)
                            fos.write(byte)
                            fos.close()
                        }
                    }
                }
                val dest = out?.getContentLocation(
                    directoryInput.name,
                    directoryInput.contentTypes,
                    directoryInput.scopes,
                    Format.DIRECTORY
                )
                FileUtils.copyDirectoryToDirectory(directoryInput.file, dest)
            }
            //jar包
            transformInput.jarInputs.forEach {
                val dest = out?.getContentLocation(
                    it.name,
                    it.contentTypes,
                    it.scopes,
                    Format.JAR
                )
                FileUtils.copyFile(it.file, dest)
            }

        }

    }
}

TestClassVisitor后面会将

4.给插件命名

之前不是新建了一个resource文件夹吗,在里面在创建一个META-INF文件夹然后在新建一个gradle-plugins文件夹,然后新建一个 properties 文件 如 com.cb.test.amsplugin.properties

然后在文件中指定下TestMethodPlugin插件plugin的绝对路径

implementation-class=com.cb.test.pluginams.TestMethodPlugin

5.自定义一个MethodVisitor

package com.cb.test.pluginams

import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.commons.AdviceAdapter

class CustomizeMethodVisitor(
    api: Int,
    methodVisitor: MethodVisitor,
    className: String?,
    access: Int,
    name: String?,
    descriptor: String?
) : AdviceAdapter(api, methodVisitor, access, name, descriptor) {

    private var mClassName: String? = className

    private val TAG = "${this.javaClass.simpleName}: "


    /**
     * 在方法调用之前插入
     */
    override fun onMethodEnter() {
        println("$TAG onMethodEnter")
        super.onMethodEnter()
        //this-方法接收的参数-方法内定义的局部变量
        //下面代码不需要自己写 有工具可以生成
        mv.visitLdcInsn("Test.class ")
        mv.visitLdcInsn("aaa start")
        mv.visitMethodInsn(
            INVOKESTATIC, "android/util/Log", "d",
            "(Ljava/lang/String;Ljava/lang/String;)I", false)
        mv.visitInsn(POP)
    }
    /**
     * 在方法调用之后插入,注意在 super.onMethodExit(opcode) 之前
     */
    override fun onMethodExit(opcode: Int) {
        mv.visitLdcInsn("Test.class ")
        mv.visitLdcInsn("aaa end")
        mv.visitMethodInsn(
            INVOKESTATIC, "android/util/Log", "d",
            "(Ljava/lang/String;Ljava/lang/String;)I", false)
        mv.visitInsn(POP)
        println("$TAG onMethodExit")
        super.onMethodExit(opcode)
    }
}

除了字节码部分其他的代码没什么好说的,都好理解,这部分代码也不需要自己写,可以在Android Stuido中搜索AMS bytecode Viewer插件。

6.使用ASM bytecode Viewer 生成相应的字节码

新建一个Test.java

public class Test {

    public static final String TAG = "Test.class ";

    void test() {
    }

}

右键代码区

微信截图_20220129162619.png

然后

image.png

复制下这里的代码,后面要用;

然后在Test.java中,编写想插入的代码

public class Test {

    public static final String TAG = "Test.class ";

    void test() {
        Log.d(TAG, "aaa start");
        
        Log.d(TAG, "aaa end");
    }

}

在重复ASM bytecode Viewer步骤,拿到字节码,复制保存下,使用diff对比工具找出两次字节码的区别,如图

diff对比工具

微信截图_20220129163342.png

找到不同之处,复制下来,

微信截图_20220129163542.png

从代码中可以看到多了两次Log 前四行对应 Log.d(TAG, "aaa start"); 后面四行对应 Log.d(TAG, "aaa end");

这就和第5步匹配上了。

7.编写ClassVisitor

package com.cb.test.pluginams

import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes

class TestClassVisitor(classVisitor: ClassVisitor) : ClassVisitor(Opcodes.ASM7, classVisitor) {

    private val TAG = "PluginAmsTag: "

    private var className: String? = null


    override fun visit(
        version: Int,
        access: Int,
        name: String?,
        signature: String?,
        superName: String?,
        interfaces: Array<out String>?
    ) {
        super.visit(version, access, name, signature, superName, interfaces)
        className = name


    }

    override fun visitMethod(
        methodAccess: Int,
        methodName: String?,
        methodDescriptor: String?,
        signature: String?,
        exceptions: Array<out String>?
    ): MethodVisitor {
        val methodVisitor = super.visitMethod(methodAccess, methodName, methodDescriptor, signature, exceptions)

        println("$TAG method = $methodName")
        println("$TAG className = $className")
        //这里写死了Test.java   读者可以自定义
        //注意这里是全路径 拷贝过来的是com.cb.test.amstest.Test.class 注意不能用.要换成/
        if (className == "com/cb/test/amstest/Test" && methodName == "test") {
            //返回我们自定义的MethodVisitor
            return CustomizeMethodVisitor(api, methodVisitor, className, methodAccess, methodName, methodDescriptor)
        }

        return methodVisitor
    }
}

至此插件代码就写完了

8.生成插件

微信截图_20220129164819.png

点击 uploadArchives 等待编译,编译完成会生成插件

微信截图_20220129165743.png

9.引入插件

在项目的build.gradle中引入生成的插件

buildscript {
    ext.kotlin_version = "1.5.0"
    repositories {
        google()
        mavenCentral()
        //这里
        maven {
            url uri('./pluginams/ams_plugin')
        }
    }
    dependencies {
        classpath "com.android.tools.build:gradle:4.1.2"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        //这里 
       classpath "com.cb.test.amsplugin:pluginams:1.0.0"

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

注意一点就是com.cb.test.amsplugin:pluginams:1.0.0 命名规则,就是查看生成插件中的 maven-metadata.xml文件中的 groupId+artifactId+version

<?xml version="1.0" encoding="UTF-8"?>
<metadata>
  <groupId>com.cb.test.amsplugin</groupId>
  <artifactId>pluginams</artifactId>
  <versioning>
    <release>1.0.0</release>
    <versions>
      <version>1.0.0</version>
    </versions>
    <lastUpdated>20220129085728</lastUpdated>
  </versioning>
</metadata>

然后在app里的build.gradle中引入插件

微信截图_20220129170741.png

这里的id命名规则就是第四步.properties之前的部分

微信截图_20220129170841.png

个人感觉这里命名规则有点恶心,命名不对编译就过不了,想要试试的同学一定要主要每个步骤的命名不是随便命名的。

10.运行验证

Test test = new Test();
//调用时将 之前获取字节码的代码删除,test是空方法
test.test();

查看logcat打印

2022-01-29 17:11:30.560 14149-14149/com.cb.test.amstest D/Test.class: aaa start
2022-01-29 17:11:30.560 14149-14149/com.cb.test.amstest D/Test.class: aaa end

完美,然后查看下Test.java生成的Test.class文件是否插入成功

微信截图_20220129171346.png

插入成功,至此简单的Asm字节码插桩就完成了。