深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上鸿蒙开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
需要这份系统化的资料的朋友,可以戳这里获取
redex 支持通过配置在方法前进行插桩,可以通过实现pass来完成自己的插桩功能。但是功能实现有限,使用起来比较复杂,而且在执行之后插入了一些fb自定义的代码,但Redex 提供了一套强大的字节码修改能力,后续的版本会基于redex的字节码修改能力进行完善。 github.com/facebook/re…
dexter
android.googlesource.com/platform/to…
dexter 工具是google开发的一个类似dexdump的工具,但其内部实现了对dex文件结构和字节码指令的一套完整的操作api,轻量简洁,对字节码的操作可以达到ASM的体验。
综合,选用dexter对dex进行操作。
需求
根据性能防劣化和流量统计的需求,都是在一个方法的方法体内部前后插入对其他方法的调用。以网络流量统计为例,需要在 的方法内部开头插入一个方法来获取request请求的详细数据。
Dex 插桩
基本流程
-
Dex文件分析
先要分析Dex文件格式,将其序列化成各种数据结构,Dex文件的结构可以参照官方文档
Dalvik 可执行文件格式
-
字节码解析
在code 段将二进制的字节码解析成可处理的数据结构
-
字节码构造
按照字节码规范构造字节码指令,并插入到现有字节码的序列中即可完成字节码的插入。
-
字节码序列化
将修改后的Dex结构重新计算Index,然后将各个数据Section序列化为Dex的文件格式。
功能需求
插桩支持两种能力,在一个方法的方法体前面和后面插入一个静态方法调用。
-
方法体前面插桩
如果被插入的方法为实例方法,则方法的第一参数为 ,随后的参数和被插入的方法一致 ,如果方法是静态方法则插入的方法定义需要和被插入的方法参数类型和个数一致,举例:
-
方法体后面插桩
需要注意的是返回值的处理,插入的方法的返回值需要和被插入方法的返回值类型一致。
参数的处理需要注意,插入的方法需要符合以下规则:
举例:
-
初始化插桩
一般用来插入一些需要提前初始化的代码,该功能会解析里 节点里定义的Application类。
根据配置在 或者 方法里插入代码。如果没有定义onCreate 和 attachbaseContext 方法,插桩工具会生成这两个方法。
由于Dex在格式和指令上的一些限制,在修改和插入字节码的过程中需要符合Dex 和 dalvik指令了一些规则,下面描述了直接操作Dex遇到的一些问题和解决方法。
方法数处理
当代码量增大后,由于Google早年设计缺陷,一个DEX文件只能容纳 65535个方法、方法引用等,插桩本身不可避免会引入新的方法以及方法引用。在某些时候会有如下情况,APP的某个dex文件非常迫近65k,导致无法再插入新的方法调用,这种情况在大多数app中常见。
一种方案是将Dex整体合并在一起,然后进行拆分,此种方法会破坏原有Dex的一些优化,并且需要实现类之间的应用关系计算,计算量比较大,这里采用一种轻量的解决方法。
Dex 拆包
解决方案1:
通过编译时增加 迫使编译器尽量不要塞满dex,但是这种方案可能不会生效,如果这个apk被类似redex的工具处理后,dex也有概率会被填满。
解决方案2:Dex 分拆逻辑
如果当前dex的方法数剩余量不满足插入新的方法则将现有dex拆出一部分类出来到一个额外的dex中。
以第一个dex的编译逻辑为例,在将maindex list和其引用的类都塞到dex后,一般方法数不会刚好到65535,如果超过了在编译的过程中就会出现 的错误。然后编译器会将一些引用关系比较小的类填入第一个dex中。这些类就是我们要拆分的目标。
主要找到这个dex里没有调用到的类就满足目标了,通过遍历所有、、的位置将所有类的引用过滤出来,可以将没有调用到的类过滤出来,拆分到其他的dex中。
主要逻辑:
- 判断该dex 的方法数是否可以继续插桩,如果无法进行插桩则需要进行dex分割逻辑
- 遍历每个类的每个方法的参数,记录类型
- 遍历每个类的属性,记录类型
- 遍历每个方法的字节码指令,通过方法调用,属性引用,类型强转的指令将引用的类型记录下来
source.android.com/devices/tec…
字节码格式ID为 的指令在最后的操作数都是一个类的方法或者属性的引用,就可以将这个方法使用的类获取到。
- 将所有 保留在原dex中
- 剩下的class 就是这个dex中没有使用到的class,可以将其拆分出去而对这个dex的执行不产生影响。
- 将没有用到的class 单独打包到一个额外的dex中,比如 现有dex有四个,则创建一个新的dex来保存。
这样被插入的dex 就会省出一部分方法空间可以继续插桩。
Dex 合并
-
分割dex合并
在Dex 分割完成后,dex分为两部分,我们需要将分割出来的dex合并成一个dex 附加到最后一个dex上面。
如上图,classes.split.dex、classes3.split.dex、… classes9.split.dex 会合并成同一个dex 为
-
插桩dex合并
插桩方法调用的代码一般不会打包到apk中,需要将代码merge到apk中。这里直接将需要插入的dex合并到最后一个dex上,如果最后一个dex无法合并则创建一个新的dex合并进去。
String Jumbo处理
在Dalvik字节码中从常量池中读取字符串到寄存器里有两个指令, 和 ,第一条指令只支持访问0-0xFFFF范围的字符串,由于我们插入了新的方法调用,会新增字符串(类名、方法名)进去,在很多情况下会导致字符串总量超过65535,由于Dex格式要求必须使用 UTF-16 代码点值按字符串内容进行排序,所以在插入新的字符串之后要进行重排序,重新排序之后会导致原先的字符串索引发生变化,引起原本使用 的指令访问到高于0xFFFF索引的字符串,引起虚拟机执行异常。
插桩工具对此做了处理,在插桩完成后会扫描所有 指令,如果访问的string index 超过65535 则强制将 修改为 指令。
混淆处理
目标方法被混淆
大部分情况下,目标方法都有比较大的概率会被混淆,所以我们在插桩的时候要基于mapping文件找到混淆后的目标函数然后进行插桩。
插入的dex使用了原APK中的类
很多情况下插桩方法调用到我们插入的dex都有可能使用到原来apk里提供的类,由于原apk里的类经历过混淆所以直接通过混淆前的名称调用会出现类、方法、属性无法找到的异常。
通过mapping文件将插入的dex里类名、方法名、属性名进行一次混淆,将调用方强行修改成混淆后的名称。
类被删除,方法内联/被删除
- 优先考虑在原apk编译的过程中增加混淆配置去解决。
- 如果调用的类和原apk逻辑关联不是很大,则建议将使用到的类包名重命名,然后一起打入到dex中,这样会表现为apk中存在相同的类,但是包名不一致,插入的dex只调用自己集成的类,这样就不用关心这个类的混淆问题。
- 很多情况下是需要使用到原apk的类,无法通过重命名包名来解决,比如通过参数传入的类,在调用这些类的方法的时候可能会出现这个方法被混淆器删除掉的情况,有可能是被内联或者没有其他位置使用到从而被删除,那么在调用过程中尽量避开调用方法。
- 有部分情况一些属性的get set方法会被内联成直接访问属性的情况
混淆前:
混淆后:
为了避免这种情况尽量在调用get set方法的时候直接使用属性访问。
比如:
如果这个get set方法没有被内联掉,那么会出现调用的属性是是private 和 protected 则导致fileld验证不通过,出现 错误,解决方法是强行把被调用的属性权限改成。需要提前指定要修改了哪些属性的访问权限。这些配置在一个配置文件里进行设置,后面会说明如何设置。
类重复问题处理
大部分情况下我们会遇到插入的Dex 与被插入的APK 存在相同类名的类的问题,比如调用了共同的第三方库,这里最常见遇到的是使用Kotlin编写的插入的dex,里面会存在kotlin 库。
这里有两种解决方法:
-
剔除插入的Dex里的重复类
在制作插入Dex的时候使用Dex插桩工具的按包名提取类的功能,将需要的类裁剪出来做成dex,这个时候就可以将一些与APK重复的类剔除出去,插入进去的Dex使用APK自身的库,这个时候需要将插入的Dex按照mapping进行混淆才能够正常进行调用。
-
重命名冲突的第三方库
将自身调用的重复类按照包名整体重名名调用。比如 包重命名成 ,这样自己的dex 调用的是 就与原apk 不冲突了。
字节码插桩
方法前插桩
在方法前面增加一条 的指令,将原方法的参数透传到 hook 方法中
方法后插桩
- 查找所有return 指令,在执行前面进行插桩
- 返回值处理
由于要将返回值通过参数传递给hook方法使用,所以需要申请一个寄存器保留返回值的结果然后传递过去。
除 指令以外,其他return指令都附带一个返回值,如下:
将p2 寄存器里的值保存到一个额外的寄存器里,然后获取hook方法的返回值,再返回回去
参数寄存器复用问题
在某些情况下,编译器在返回一个值的时候为了复用寄存器,会复用参数寄存器来作为通用寄存器,这就导致我们在方法后面获取参数的时候,发现这个参数寄存器被复用了,就无法正确获取到参数的值。
函数中引入的参数命名从p0开始,依次递增。举例一个方法会用到v0,v1,p0,p1,p2这五个寄存器,v0和v1表示局部变量寄存器,如果是实例方法的话,p0表示的是被传入的this对象的引用,p1和p2分别表示两个传入的参数。
比如下面,就复用了p1寄存器来保存返回值,导致我们插桩方法无法获取到正确的参数
解决方法:
在原有寄存器数量上面扩展对应参数数量的寄存器即可解决这个问题,比如