字节码插桩(三): ASM 字节码插桩(1)

简介: 字节码插桩(三): ASM 字节码插桩

2023-04-15 (7).png

阅读本篇可能需要的预备知识 《ASM4 使用手册(中文版)》,本文涉及代码已经上传Github,欢迎star一波~

《ASM 字节码插桩》大纲

2023-04-15 (5).png

背景和疑问

  在 Android 中,你可能经常听某位中台大佬说 无痕埋点 , Hook ,apm监控,编译器动态修改代码等名词,小伙伴通常都知道 AspectJ 可以通过切面织入相关代码,但殊不知 就连小小的 Lambada 语法在自定义 Plugin 都无法实现。


  更何况其他兼容问题,有没有一个相对完美的选择,实现全埋点呢?有没有最优质的技术选择向应用程序中插入调试或性能监视代码同时保证应用程序的运行速度。


  好吧,不拐外抹角啦,今天就带大家详细聊聊一款轻量级AOP设计ASM。

1.0 关键技术

  了解 ASM 之前 首先得了解 APP 的打包流程,这里推荐大家看一下邓凡平的 《深入理解Android虚拟机》,Android应用打包流程大概分为以下7个阶段:

2023-04-15 (9).png

  1. aapt 打包资源文件 阶段
  2. aidl 转 java 文件 阶段
  3. Java 编译(Compilers)生成.class文件 阶段
  4. dex(生成dex文件)阶段
  5. apkbuilder(生成未签名apk)阶段
  6. Jarsigner(签名)阶段
  7. zipalign(对齐) 阶段

  具体每个阶段做了哪些工作,可以参考浅谈Android打包流程一文,我们只需要关注第四阶段 生成.dex之前的工作即可,因为在这个阶段我们能拦截到所有的.class文件,然后借助插件,就可以遍历.class文件的所有方法,再根据一定的条件找到需要的目标方法,最后修改并保存,就可以插入我们需要的代码了。


  那么我们所说的插件到底是什么呢?在Google从 Android Gradle 1.5 开始就提供了 Transform Api,通过Transform Api,允许三方 Plugin形式,在应用程序打包生成 .dex 前编译过程中操作 .class 文件。


  我们要做的工作就是 自定义 Transform, 迭代 .class 文件所有的方法,然后修改在特定的listener中插入埋点代码,最后对源文件替换,达到织入代码的目的,那么Transform到是什么呢?

1.1 Gradle Transform

  回到什么一个问题,什么是Transform?Google官方文档是这么翻译的

A transform receives input as a collection TransformInput, which is composed of JarInputs and DirectoryInputs. Both provide information about the QualifiedContent.Scopes and QualifiedContent.ContentTypes associated with their particular content.The output is handled by TransformOutputProvider which allows creating new self-contained content, each associated with their own Scopes and Content Types. The content handled by TransformInput/Output is managed by the transform system, and their location is not configurable.It is best practice to write into as many outputs as Jar/Folder Inputs have been received by the transform. Combining all the inputs into a single output prevents downstream transform from processing limited scopes.

  简单理解就是: 用来修改 .class 文件的一套标准 API,目的是把 .class 文件 转换成目标 字节码文件,其实你可以简单的把它和文件输入流和输出流类比起来,只不过IO体系里面操作的是流对象而Transform操作的是文件对象。


  记住两个核心的API:TransformInput 和 TransformOutputProvider;TransformInput代表的是输入文件抽象接口,它有两个比较重要的方法

2023-04-15 (10).png

 获取DirectoryInput集合和获取JarInput集合,其中: DirectoryInput是以源码方式参与项目编译所有目录结构及其目录下的源文件;而JarInput是以jar包方式参与项目编译的所有jar包和远程包。


  TransformOutputProvider代表的是输出文件抽象接口,里面有一个核心接口方法getContentLocation主要是获取输出路径信息的

2023-04-15 (11).png

 getContentLocation方法有几个比较重要的参数name,type,scopes和format,其中name代表该 Transform 对应的 Task 的名称。

2023-04-15 (12).png

QualifiedContent.ContentType代表的是Transform需要处理的数据类型;里面有两个默认枚举参数: CLASSES 和 PESOURCES。

2023-04-15 (13).png

CLASSES 代表 需要处理编译后的字节码,可能是jar 也可能是 目录。PESOURCES代表处理标准的 java 资源,scopes 也是一个比较有意思的枚举类,

2023-04-15 (14).png

用来指定 Transform 的作用域,其中有七个枚举对象:PROJECT,SUB_PROJECTS,PROJECT_LOCALDEPS,SUB_PROJECTS_LOCAL_OEPS,EXTERNAL_LIBRARIES,PROVIDED_ONLY和TESTED_CODE。


  PROJECT只处理当前项目,SUB_PROJECTS,只处理子项目,PROJECT_LOCALDEPS只处理当前项目的本地依赖,例如: jar , arr。SUB_PROJECTS_LOCAL_OEPS只处理子项目的本地依赖。例如: jar , arr,EXTERNAL_LIBRARIES只处理外包的依赖库,PROVIDED_ONLY只处理本地或远程以 provided 形式引入的依赖库。而TESTED_CODE指的是测试代码。format 是用来格式化内容的。


  至此TransformOutputProvider就介绍完了。纸上得来终觉浅,绝知此事要躬行。我们简单实现一下Transform吧

第一步: 新建一个 Project

里面自动生成一个主 moudle,即: app

2023-04-15 (15).png

第二步: 新建一个命名为 plugin 的 moudle

2023-04-15 (16).png

第三步: 清空 plugin/build.gradle 文件的内容,然后修改里面的内容

2023-04-15 (17).png

第四步: 删除 plugin.src/main 目录下所有的文件

2023-04-15 (18).png

第五步: 新建 groovy 目录

2023-04-15 (19).png

第六步: 创建 Transform 类

2023-04-15 (20).png2023-04-15 (21).png

第七步: 新建 Plugin,创建 .properties

2023-04-15 (22).png

第八步: 执行 plugin 的 uploadArchives 任务构建 plugin

2023-04-15 (23).png

第九步: 修改项目根目录下的 buid.gradle 文件,添加对插件的依赖

2023-04-15 (24).png

第十步: 在 app/build.gradle 文件声明使用插件

2023-04-15 (25).png

第十一步: 构建应用程序

  至此,一个简单的 Gradle Transform 的实例 就已经完成了

1.2 ASM 基础知识

如果说Transform只是前菜,那么今天的主菜应该是ASM,什么是ASM呢?官方是这么解释的。

ASM is an all purpose Java bytecode manipulation and analysis framework. It can be used to modify existing classes or to dynamically generate classes, directly in binary form. ASM provides some common bytecode transformations and analysis algorithms from which custom complex transformations and code analysis tools can be built. ASM offers similar functionality as other Java bytecode frameworks, but is focused on performance. Because it was designed and implemented to be as small and as fast as possible, it is well suited for use in dynamic systems (but can of course be used in a static way too, e.g. in compilers).

  ASM其实是一个功能比较齐全的 Java 字节码操作和分析框架,通过ASM,我们可以动态生成类或增强类的既有类的功能,ASM可以直接生成二进制的.class文件,也可以被类在加载入 java 虚拟机


  之前动态改变现有类的行为,Java 的二进制被存储在严格格式定义的 .class 文件里,这些字节码拥有足够的元数据信息用来表示类的所有元素,包括类的名称,方法,属性以及Java  字节码指令。


  ASM 从字节码文件读入这些信息后能改变 类的行为,分析类的信息,甚至根据具体要求生成新的类。ASM涉及了五个比较核心的类: ClassReader,ClassWriter,MethodVisitor,ClassVistor和AdiviveAdapter。


  ClassReader 主要是 用来解析编译过的 .class  字节码文件,ClassWriter 主要是 用来重新构建编译后的类,比如修改类名,属性以及方法,甚至可以生产新的类名字节码。


  MethodVisitor主要是方法访问类,有几个重要的方法:onMethodEnter,visitEnd和visitAnnotation。onMethodEnter主要是进入方法时插入字节码,visitEnd主要是退出方法前可以退出字节码,visitAnnotation可以在这里通过注解的方式操作字节码。


  ClassVistor负责 "拜访" 类成员信息 其中包括 在类的注解,类的构造方法,类的字段,类的方法,静态代码块

  ClassVistor 有几个比较重要的方法,重点需要了解的有: visit和visitMehtod;

2023-04-15 (26).png

visit有6个比较重要的参数: version,access,name,signature,signatureName和interfaces。version代表的是JDK的版本,版本对应表格如下:

2023-04-15 (27).png

access代表类的修饰符,具体修饰符含义如下:

2023-04-15 (28).png

name代表的是类的名称;signature是泛型信息;signatureName是当前类所继承的父类;interfaces是类所实现的接口列表,在java中,一个类是可以实现多个不同的接口,因此该参数是一个数组类型。下面我们看一下ClassVistor的样例代码:

class MkAnalyticsClassVisitor extends ClassVisitor implements Opcodes {
   private final static String SDK_API_CLASS = "com/github/microkibaco/asm_sdk/SensorsDataAutoTrackHelper"
   private String[] mInterfaces
   private ClassVisitor classVisitor
   private HashMap<String, MkAnalyticsMethodCell> mLambdaMethodCells = new HashMap<>()
   MkAnalyticsClassVisitor(final ClassVisitor classVisitor) {
       super(Opcodes.ASM6, classVisitor)
       this.classVisitor = classVisitor
   }
   private
   static void visitMethodWithLoadedParams(MethodVisitor methodVisitor, int opcode, String owner, String methodName, String methodDesc, int start, int count, List<Integer> paramOpcodes) {
       for (int i = start; i < start + count; i++) {
           methodVisitor.visitVarInsn(paramOpcodes[i - start], i)
       }
       methodVisitor.visitMethodInsn(opcode, owner, methodName, methodDesc, false)
   }
   /**
    *  visit 可以拿到关于 .class 的所有信息,比如: 当前类所实现的接口列表
    * @param version JDK的版本
    * @param access 类的修饰符
    * @param name 类的名称
    * @param signature 当前类所继承的父类
    * @param superName
    * @param interfaces 类所实现的接口列表,在java中,一个类是可以实现多个不同的接口,因此该参数是一个数组类型
    */
   @Override
   void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
       super.visit(version, access, name, signature, superName, interfaces)
       mInterfaces = interfaces
   }
   /**
    * 可以拿到关于 method所有的信息,比如: 方法名 方法的参数描述
    * @param access 类的修饰符
    * @param name 类的名称
    * @param desc 方法签名
    * @param signature 类签名
    * @param exceptions 异常信息
    * @return MethodVisitor
    */
   @Override
   MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
       MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions)
       return methodVisitor
   }
   /**
    * 获取方法参数下标为 index 的对应 ASM index
    * @param types 方法参数类型数组
    * @param index 方法中参数下标,从 0 开始
    * @param isStaticMethod 该方法是否为静态方法
    * @return 访问该方法的 index 位参数的 ASM index
    */
   int getVisitPosition(Type[] types, int index, boolean isStaticMethod) {
       if (types == null || index < 0 || index >= types.length) {
           throw new Error("getVisitPosition error")
       }
       if (index == 0) {
           return isStaticMethod ? 0 : 1
       } else {
           return getVisitPosition(types, index - 1, isStaticMethod) + types[index - 1].getSize()
       }
   }
}

上面的 visitMethod中,可以对特定的方法镜像修改,修改方法的时候需要用到"拜访"方法所以信息 MethodVistitor

methodVisitor = new MkAnalyticsDefaultMethodVisitor(methodVisitor, access, name, desc) {
           boolean isSensorsDataTrackViewOnClickAnnotation = false
           /**
            * 退出方法前可以退出字节码
            */
           @Override
           void visitEnd() {
               super.visitEnd()
               if (mLambdaMethodCells.containsKey(nameDesc)) {
                   mLambdaMethodCells.remove(nameDesc)
               }
           }
           /**
            * 进入方法时插入字节码
            */
           @Override
           protected void onMethodEnter() {
               super.onMethodEnter()
               }
/**
* 可以在这里通过注解的方式操作字节码
* @param s 访问的注解名
* @param b 是否方法
* @return  methodVisitor
*/
           @Override
           groovyjarjarasm.asm.AnnotationVisitor visitAnnotation(String s, boolean b) {
               if (s == 'Lcom/sensorsdata/analytics/android/sdk/SensorsDataTrackViewOnClick;') {
                   isSensorsDataTrackViewOnClickAnnotation = true
               }
               return super.visitAnnotation(s, b)
           }
       }

visitMehtod 方法也有几个比较重要的参数: access,name,desc,signature和exceptions。access代表的是方法的修饰符,具体修饰符含义如下:

2023-04-15 (28).png

name表示方法名;desc表示方法签名,具体符号类型如下:

2023-04-15 (29).png

方法参数列表对应的方法签名如下:

2023-04-15 (31).png

signature表示泛型相关信息;exceptions表示将会抛出异常,如果方法不会抛出异常,该参数为空。最后我们看一下AdiviveAdapter,它实现 MethodVistor 接口,主要负责 "拜访" 方法的信息,用来进行具体方法

class MkAnalyticsDefaultMethodVisitor extends AdviceAdapter {
   MkAnalyticsDefaultMethodVisitor(MethodVisitor mv, int access, String name, String desc) {
       super(Opcodes.ASM6, mv, access, name, desc)
   }
   /**
    * 表示 ASM 开始扫描这个方法
    */
   @Override
   void visitCode() {
       super.visitCode()
   }
   @Override
   void visitMethodInsn(int opcode, String owner, String name, String desc) {
       super.visitMethodInsn(opcode, owner, name, desc)
   }
   @Override
   void visitAttribute(Attribute attribute) {
       super.visitAttribute(attribute)
   }
   /**
    * 表示方法输出完毕
    */
   @Override
   void visitEnd() {
       super.visitEnd()
   }
   @Override
   void visitFieldInsn(int opcode, String owner, String name, String desc) {
       super.visitFieldInsn(opcode, owner, name, desc)
   }
   @Override
   void visitIincInsn(int var, int increment) {
       super.visitIincInsn(var, increment)
   }
   @Override
   void visitIntInsn(int i, int i1) {
       super.visitIntInsn(i, i1)
   }
   /**
    * 该方法是 visitEnd 之前调用的方法,可以反复调用。用以确定类方法在执行时候的堆栈大小。
    * @param maxStack
    * @param maxLocals
    */
   @Override
   void visitMaxs(int maxStack, int maxLocals) {
       super.visitMaxs(maxStack, maxLocals)
   }
   @Override
   void visitVarInsn(int opcode, int var) {
       super.visitVarInsn(opcode, var)
   }
   @Override
   void visitJumpInsn(int opcode, Label label) {
       super.visitJumpInsn(opcode, label)
   }
   @Override
   void visitLookupSwitchInsn(Label label, int[] ints, Label[] labels) {
       super.visitLookupSwitchInsn(label, ints, labels)
   }
   @Override
   void visitMultiANewArrayInsn(String s, int i) {
       super.visitMultiANewArrayInsn(s, i)
   }
   @Override
   void visitTableSwitchInsn(int i, int i1, Label label, Label[] labels) {
       super.visitTableSwitchInsn(i, i1, label, labels)
   }
   @Override
   void visitTryCatchBlock(Label label, Label label1, Label label2, String s) {
       super.visitTryCatchBlock(label, label1, label2, s)
   }
   @Override
   void visitTypeInsn(int opcode, String s) {
       super.visitTypeInsn(opcode, s)
   }
   @Override
   void visitLocalVariable(String s, String s1, String s2, Label label, Label label1, int i) {
       super.visitLocalVariable(s, s1, s2, label, label1, i)
   }
   @Override
   void visitInsn(int opcode) {
       super.visitInsn(opcode)
   }
   @Override
   AnnotationVisitor visitAnnotation(String s, boolean b) {
       return super.visitAnnotation(s, b)
   }
   @Override
   protected void onMethodEnter() {
       super.onMethodEnter()
   }
   /**
    * 使用 onMethodExit 这样就不会影响到应用程序原有点击事件的响应速度
    * @param opcode
    */
   @Override
   protected void onMethodExit(int opcode) {
       super.onMethodExit(opcode)
   }
}

但是我在使用过程遇到了这个坑,具体原因还要审核一下Plugin是否导包有误

2023-04-15 (32).png


相关文章
|
6月前
|
安全 算法 Java
从零开发基于ASM字节码的Java代码混淆插件XHood
因在公司负责基础框架的开发设计,所以针对框架源代码的保护工作比较重视,之前也加入了一系列保护措施,例如自定义classloader加密保护,授权license保护等,但都是防君子不防小人,安全等级还比较低,经过调研各类加密混淆措施后,决定自研混淆插件,自主可控,能够贴合实际情况进行定制化,达到框架升级后使用零感知,零影响
73 1
从零开发基于ASM字节码的Java代码混淆插件XHood
|
10月前
|
开发框架 Java Maven
SpringBoot自定义maven-plugin插件整合asm代码插桩
公司开发框架增加了web系统license授权证书校验模块,实行一台机器一个授权证书,初步方案是增加拦截器针对全局请求进行拦截校验,评估后认为校验方式单一,应该增加重要工具类,业务service实现中每个方法的进行校验,因为涉及代码量较大硬编码工作困难,故选择通过自定义maven插件在编译期间进行动态代码插桩操作
96 1
|
监控 安全 Java
手把手带你实战 AGP 7.x ASM 字节码插桩
本文介绍了如何使用 AGP 7.0 推荐的 Transform Action API 来实现 ASM 插桩。
1024 0
手把手带你实战 AGP 7.x ASM 字节码插桩
|
存储 算法 Java
一起来学字节码插桩:ASM Tree API
`ASM`是一个通用的`Java字节码操作和分析框架`。它可用于`修改现有类`或`直接以二进制形式动态生成类`。`ASM`提供了一些常见的字节码转换和分析算法,可以根据这些算法构建定制的复杂转换和代码分析工具。
204 0
|
Java API 开发工具
字节码插桩(三): ASM 字节码插桩(2)
字节码插桩(三): ASM 字节码插桩(2)
111 0
|
存储 设计模式 Java
深入探索编译插桩(三,ASM揭秘以及实战)
最近在学习一些关于编译插桩方面的知识,说到编译插桩:大家可以想到的哪些关键字: `Gradle插件`,`ASM`,`AspectJ`,`AOP`,`JVM字节码`等。
|
Java fastjson 数据库连接
字节码操作框架介绍与实践(以ASM和Javassit为例)
ASM是java字节码操作领域公认的标准,被众多知名的开源框架使用,如cglib、mybatis,fastjson等。通过ASM提供的API,我们可以方便的修改类文件的字节码,并ASM会自动帮我们做很多事情,如维护常量池的索引、计算栈大小max_stack,局部变量表大小max_locals等。ASM提供了两种类型的API,基于事件触发的core api和基于对象的tree api,下面主要介绍基
967 0
字节码操作框架介绍与实践(以ASM和Javassit为例)
|
监控 Java Android开发
【字节码插桩】AOP 技术 ( “字节码插桩“ 技术简介 | AspectJ 插桩工具 | ASM 插桩工具 )
【字节码插桩】AOP 技术 ( “字节码插桩“ 技术简介 | AspectJ 插桩工具 | ASM 插桩工具 )
299 0
【字节码插桩】AOP 技术 ( “字节码插桩“ 技术简介 | AspectJ 插桩工具 | ASM 插桩工具 )
|
3月前
|
Oracle 关系型数据库
oracle asm 磁盘显示offline
oracle asm 磁盘显示offline
35 2
|
3月前
|
存储 Oracle 关系型数据库
【数据库数据恢复】Oracle数据库ASM磁盘组掉线的数据恢复案例
oracle数据库ASM磁盘组掉线,ASM实例不能挂载。数据库管理员尝试修复数据库,但是没有成功。
【数据库数据恢复】Oracle数据库ASM磁盘组掉线的数据恢复案例