引
在odex格式的解析文章中提到过,它的dex文件部分和原dex相比发生了变更,参见:
这里的变更共有3部分,分别是class_def段,code段和header中的checksum。
而且变更后的dex部分和原文件大小一致,变更的内容也不是很多。
到底发生了什么呢,我们还是从源码中找下,dexopt过程中到底对原dex文件做了什么。
class_def段
dexopt过程包含内容比较多,我们直接跳到核心优化的代码点吧。。。
http://androidxref.com/4.4_r1/xref/dalvik/vm/analysis/DexPrepare.cpp
dvmContinueOptimization
上面的函数中,巴拉巴拉写了一堆的校验和重写(形成odex必要的一些数据),我们都忽略了,只关注dex部分的流程,如下:
重写dex文件
其中rewriteDex又调用了下述函数:
校验和优化所有的类
这里的verifyAndOptimizeClasses就是我们要找的函数了。
它遍历了class_def的列表,获取到了每个类的信息,并且对每个类都执行优化。
校验和优化所有类
对于单个类的校验和优化的过程如下:
校验和优化单个类
打住打住,这个函数对DexClassDef的accessFlags做了变更,分别在校验和优化后加入两个标志。
特殊类型
两个标志位的异或结果
这个结果不就是0x00030000吗? class_def段的对比如下,这个03终于找到了。。。
class_def_diff
code段
继续阅读源码,看类是怎么优化的。
http://androidxref.com/4.4_r1/xref/dalvik/vm/analysis/Optimize.cpp
优化单个类
上面这个函数遍历了它的直接方法和虚方法列表,对每个函数做优化。
优化单个函数
上面这个函数就是最最核心的方法了,它对指令字节码部分做转换,转换后的指令码列表如下:
快速标记指令
过程可以划分为针对instance_field,static_field, invoke_init, return_void, invoke_virtual, inline的几个部分。
指令转换
转换instance_field
实例属性
从上图可以看出,除了指令码更改为quick类型外,还会将原指令中的field_idx转换为byte_offset,这个byte_offset是偏移量可以直接定位。
这里还涉及到volatile类型,如果它是volatile类型的变量那么会把volatile_opc写入field_idx部分而不是byte_offset
转换指令码
上图是如何转换指令,以为指令码只占用一个字节,所以通过异或来保留第二个字节的原有信息。 至于opcode >= 256基本是没用的,因为从上面的quick列表看,都在256以内。
转换过程还涉及到如何查找field,如下图所示。
属性域转换
这个过程需要先查找类。
这个部分很难,说它难不是它在虚拟机中的加载难,而是没有任何依赖的情况下,如何加载并定位它,可是这是不可能的。。。 因为很多类,是rom自己编译好的,只能从rom对应的framework.odex, core.odex等系统类信息中去查询。
所以当我看到下面的referrer->classLoader,忽然就觉得,『有你真好!』
寻找类
接着是一个递归查询的方法,它沿着类和它的父类一直往上查询,直到查询到field为止。
寻找属性
转换static_field
同instance_field的转换类似。
注意一下,非volatile类型的static属性,是不做任何处理了。
看下面的逻辑,static_field被索引到后,只是判断了它的volatile类型信息。
write_static
转换invoke_init
这是比较独特的一个转换,就是把构造函数的单独拎出来了,转换前后的数字很容易识别。
invoke init
转换return_void
这个转换只涉及指令码转换
return
这个转换过程是有条件的,从下面代码可以看出,如果函数所属的类是final类型或者它的属性中至少有一个是final类型的。
转换return的条件
转换invoke_virtual
过程和instance_field类似,除了转换指令码,还会通过method_id找到对应的method方法,将它的method_index替换掉method_id。
virtual
转换inline
同invoke_virtual的转换类似,不同的是,把method_id转换为对应的内联方法的索引。
inline
从上面的转换过程来看,主要是把属性获取和设置,方法调用相关的指令字节码优化掉,这样虚拟机在加载odex后,在执行指令字节码的时候能更加快速的获取到相关信息。
对比结果分析
下面是code段对比的一个差异图,我们按照上面的转换过程一一对比下。
code段对比
先看下这段的反汇编信息:
(关于反汇编,参看:用python一步步解剖dex文件(四)--- 反汇编框架)
数据对应的反汇编信息
这里共有5个指令字节码单元,我们一个个分析。
第一段: 70 10 11 00 00 00
它是执行了方法,所以对应上面的『转换invoke_init』部分,按照逻辑分支又属于invoke_direct的分支,所以转换结果是 0xf0 0x100 = 0x1f0, 字节表示就是右侧的 f0 01。 其它不变化。
第二段:5b 01 0b 00
它是执行iput-object指令,对应上面的『转换instance_field』部分。
这个部分更改两处,一个是指令码变更为0xf7,另一个就是转换field_idx。
field_idx是字节的0b 00部分,也就是0x000b = 11,在field_id_list中的索引号为11。
经过虚拟机的类属性查询,它转换后的数值为08.
第三段:5b 02 0c 00
同第二段,指令码转换为0xf7,而field_idx经过转换后数值不变。
第四段: 0e
这个是return-void类型,因为原函数的所属类,没有final信息,所以不转换。
第五段: 00
nop类型,啥也不动。
这样,code段的变化就都解释清楚了。
checksum
最后的最后,odex对dex部分做checksum校验并重写。
这里有做checksum计算的示例:
https://github.com/callmejacob/dexfactory/blob/master/dexinfo.py
请勿转载,谢谢!
欢迎关注
简书主页:https://www.jianshu.com/u/d2a08403ea7f
微信公众号:一叶谷
领取专属 10元无门槛券
私享最新 技术干货