Android-ASM字节码插桩技术跳坑指南

经验总是不停刨坑刨出来的,最近结合工作需要并熟悉学习android ASM 字节码插桩的环境下,开发了一个方法 hook 的插件,虽然在各方其他开源项目的参考下,还是刨了不少坑,下面就来记录下。

gradle 知识点记录

–dry-run 查看gradle task执行顺序,验证插件是否被执行时有用。
–stacktrace 查看详细堆栈,报错时可以看到插件代码错误的位置。

ASM开发相关注意

  1. 基本类型中的long 类型 和double 类型,占8个字节,在方法堆栈中占两个slot,不可以用ALOAD 接收,否则会数据不平衡,在编译期间如果有字节码分析过程就会报错,表现为无法转为dex,解决方法是用正确的参数接收,比如long 用 LLOAD

  2. 基本类型中的long 类型 和double 类型,占8个字节,在方法堆栈中占两个slot,在弹出的时候应该用DUP2 弹出,否则会栈不平衡。

  3. 基本类型在进入方法的时候,最好进行装箱操作。例如把int 改为 Integer.valueOf(num) , 否则在字节码检查的时候有时候会出问题。

  4. 很多旧版本的gradle 不支持 Opcodes.ASM6 以上的版本, 因此开发插件的时候如果没有特殊需求尽可能考虑 Opcodes.ASM5 比较好。

  5. visitMethod 方法会把抽象类里面的抽象方法也遍历到。因此应该进行空方法判断,def isUnImplMethod = (access & Opcodes.ACC_ABSTRACT) != 0

  6. 要注意Lambda 表达式的处理,可以关注 visitInvokeDynamicInsn 方法回调。
    可参考以下场景:

public class Java8Tester {
   public static void main(String args[]){
      Java8Tester tester = new Java8Tester();
        
      // 类型声明
      MathOperation addition = (int a, int b) -> a + b;
        
      // 不用类型声明
      MathOperation subtraction = (a, b) -> a - b;
        
      // 大括号中的返回语句
      MathOperation multiplication = (int a, int b) -> { return a * b; };
        
      // 没有大括号及返回语句
      MathOperation division = (int a, int b) -> a / b;
        
      System.out.println("10 + 5 = " + tester.operate(10, 5, addition));
      System.out.println("10 - 5 = " + tester.operate(10, 5, subtraction));
      System.out.println("10 x 5 = " + tester.operate(10, 5, multiplication));
      System.out.println("10 / 5 = " + tester.operate(10, 5, division));
        
      // 不用括号
      GreetingService greetService1 = message ->
      System.out.println("Hello " + message);
        
      // 用括号
      GreetingService greetService2 = (message) ->
      System.out.println("Hello " + message);
        
      greetService1.sayMessage("Runoob");
      greetService2.sayMessage("Google");
   }
    
   interface MathOperation {
      int operation(int a, int b);
   }
    
   interface GreetingService {
      void sayMessage(String message);
   }
    
   private int operate(int a, int b, MathOperation mathOperation){
      return mathOperation.operation(a, b);
   }
}
  1. 开发完后,对class文件字节码插桩总是成功的,有些混淆后的jar不成功,表现为插桩后无法转换为dex。原因是,该jar包在混淆的过程中有特殊字符,(暂时没定位到都有哪些特殊字符),正常使用是没问题的。

最后大家如果想学习ASM 字节码插桩开发的话,可以参考下我开发的这个工具,某些场景还是可以避坑的…希望能对你有帮助。

地址:https://github.com/miqt/MethodHookTool

它是一个 android 方法hook的插件,在方法进入和方法退出时,将当前运行的所有参数回调到固定的接口中,利用这一点,可以进行方法切片式开发,也可以进行一些耗时统计等性能优化相关的统计。

效果展示

原始代码:

@HookMethod
public int add(int num1, int num2) throws InterruptedException {
    int a = num1 + num2;
    Thread.sleep(a);
    return a;
}

实际编译插桩后代码:

public int add(int num1, int num2) throws InterruptedException {
    MethodHookHandler.enter(this,"com.miqt.plugindemo.Hello","add","[int, int]","int",i,i1);
    int a = num1 + num2;
    Thread.sleep(a);
    MethodHookHandler.exit(a,this,"com.miqt.plugindemo.Hello","add","[int, int]","int",i,i1);
    return a;
}

稍作开发就可以实现一个方法出入日志打印功能:

2020-05-08 16:16:31.385 25969-26027/com.miqt.plugindemo W/MethodHookHandler:  
╔======================================================================================
║[Thread]:Thread-3
║[Class]:com.miqt.plugindemo.Hello
║[Method]:add
║[This]:com.miqt.plugindemo.Hello@c65e5c0
║[ArgsType]:[int, int]
║[ArgsValue]:[100,200]
║[Return]:300
║[ReturnType]:int
║[Time]:301 ms
╚======================================================================================

可以看出,这样的话方法名,运行线程,当前对象,入/出参数和耗时情况就都一目了然啦。当然还可以做一些别的事情,例如hook点击事件等等。

使用方法

项目根目录:build.gradle 添加以下代码

dependencies {
    classpath 'me.miqt.plugin.tools:pluginSrc:0.2.2'
}

对应 module 中启用插件,可以是application也可以是library

apply plugin: 'miqt.plugin.tools'

methodhook {
    enable = true //是否启用
    //项目中的class true:全部插桩 false:只有注解插桩
    all = true

    // 下面是非必要配置,无特殊需求可直接删除

    //指定插桩那些外部引用的jar,默认空,表示只对项目中的class插桩
    jarRegexs = [".*androidx.*"]
    //指定插桩那些类文件,默认空
    classRegexs = [".*view.*"]
    //所有包含 on 的方法,所有构造方法
    methodRegexs = [".*on.*", ".*init.*"]
    
    //编译时是否打印log
    log = true
    //是否用插桩后的jar包替换项目中的jar包,慎用
    replaceJar = false
    //是否生成详细mapping文件
    mapping = true
    //自定义方法统计实现类,不指定默认使用自带实现方式
    //impl = "com.miqt.plugindemo.MyTimeP"
}

添加类库依赖:

dependencies {
    implementation 'me.miqt.plugin.tools:pluginlib:0.2.2'
}

这个插件是借鉴了很多大佬的代码,并结合自己的想法进行了一些调整,在此感谢他们付出的努力。

https://github.com/novoda/bintray-release
https://github.com/JeasonWong/CostTime
https://github.com/MegatronKing/StringFog

目前存在的已知问题

  1. 在对jar进行方法hook的时候,如果这个jar经历过混淆,则插入代码后会因为jar2dex转换失败而编译不通过。
  2. No such property: ASM6 for class: org.objectweb.asm.Opcodes 升级gradle版本可以解决。

你可能感兴趣的:(Android,java)