安卓移动逆向(三)-Android Dalvik虚拟机
大家都知道 Java 程序是运行在 Java 虚拟机上, Android 程序呢
虽然 Android 平台使用 Java 语言来开发应用程序, 但是 Android 程序却不是运行在标准的 Java 虚拟机上的. Google 为 Android 平台专门设计了一套虚拟机来运行 Android 程序–Dalvik Virtual Machine, 也就是 Dalvik 虚拟机了
本篇作用:
- 扫盲 Dalvik 虚拟机
- 了解 Smail 的语法, 能读懂 Smail 文件
Dalvik 概述
Dalvik 的特点 (相对于 JVM)
- 体积小, 占用内存小;
- 专有的 DEX 可执行文件格式, 体积更小, 执行速度更快;
- 常量池采用 32 位索引值, 寻址类方法名, 字段名, 常亮更快;
- 基于寄存器架构, 并拥有一套完成的指令系统
- 提供了对象生命周期管理, 堆栈管理, 线程管理, 安全和异常管理以及垃圾回收等重要功能;
- 所有的 Android 程序都运行在 Android 系统进程里, 每个进程对应着一个 Dalvik 虚拟机实例;
Dalvik 虚拟机与 Java 虚拟机的区别
- Java 虚拟机运行的 Java 字节码, Dalvik 虚拟机运行的是 Dalvik 字节码
- Dalvik 可执行文件的体积更小
稍作解析:
SDK 中有一个叫做 dx 的工具负责将 Java 字节码转换为 Dalvik 字节码. dx 工具对 Java 类文件重新排列, 消除在类文件中出现的所有冗余信息, 避免虚拟机在初始化时出现重复的文件加载与解析过程.
举个栗子:
在 Java 中有大量的字符串常量在多个类文件中被重复使用, 这些荣誉信息会直接增加文件的体积, 同事也会严重影响虚拟机解析文件的效率. dx 工具针对这个问题做了专门的处理, 它将所有 Java 类文件中的常量池进行分解, 消除其中的冗余信息, 重新组合成一个常量池, 所有的类文件共享同一个常量池. dx 工具转换过程如图所示, 由于 dx 工具对常量池的压缩, 是的相同的字符串, 常量在 DEX 文件中只出现一次, 从而减小了文件的体积.
3. Java 虚拟机与 Dalvik 虚拟机架构不同
简单说一下: Java 虚拟机基于栈架构, Dalvik 基于寄存器架构;
Dalvik 指令格式
一般 Dalvik 汇编代码由一系列的 Dalvik 指令组成, 指令语法由指令的位描述与指令格式标识来决定. 位描述约定如下
- 每 16 位的字采用空格分割开来;
- 每个字母表示四位, 每个字母按顺序从高字节开始, 排列到低字节. 每四位之间可能用竖线 "|" 来表示不同的内容;
- 顺序采用 A-Z 的翻个大写字母作为一个 4 位的操作码, op 表示一个 8 位的操作码;
- "Ø" 来表示这个字段的所有位为 0 值;
栗子
"A|B|op BBBB F|E|D|C"
指令中间有两个空格, 每个分开的部分是 16 位, 共有 3 个 16 位组成这条指令;
第一个 16 位是 "A|B|op" 高 8 位由 A 和 B 组成, 低字节由操作码 op 组成;
第二个 16 位由 BBBB 组成, 他表示一个 16 位的偏移值;
第三个 16 位分别由 F,E,D,C 共四个字节组成, 在这里他们表示寄存器的参数.
单独使用位标识还无法确定一条指令的意思, 必须通过指令格式标识来指定指令的格式编码, 约定如下
- 指令格式标识大多由三个字符组成, 前两个是数字, 最后一个是字母;
- 第一个数字式标识指令由多少个 16 位的字组成;
- 第二个数字标识指令最多使用寄存器的个数, 特殊标记 "r" 标识使用一定范围内的寄存器;
- 第三个字母为类型码, 标识指令用到的额外数据的类型, 见下图:
- 还有一种特殊的情况 是末尾可能还会多出另一个字母, 如果是 "s" 表示指令采用静态链接, 如果是 "i" 表示指令应该被内联处理.
栗子
"22x" 有三条信息可以读出
- 指令由 2 个 16 位字组成
- 指令使用 2 个寄存器
- 没有使用到额外的数据
另外, Dalvik 指令对语法做了一些说明, 约定如下
- 每条指令从操作码开始, 后面紧跟参数, 参数个数不定, 每个参数之间采用逗号分开;
- 每条指令的参数从指令的第一部分开始, op 位于低 8 位, 高 8 位可以是一个 8 位的参数也可以是两个 4 位的参数, 还可以为空. 如果指令超过 16 位, 则后面的部分依次作为参数;
- 如果参数使用 "vX" 的方式标识, 表明它是一个寄存器, 如 v0,v1 等;
- 如果参数采用 "#+X" 的方式, 表明它是一个常量数字;
- 如果参数采用 "+X" 的方式, 表明它是一个相对指令的地址偏移;
- 如果参数采用 "kind@X" 的方式, 表明它是一个常量池索引值. 其中 kind 表示常量池类型, 例如 string@BBBB, 表示的就是字符串常量池索引 BBBB;
栗子
"op vAA string@BBBB"
高 8 位为空, 用到 1 个寄存器参数 vAA, 还用到一个字符串常量池索引 BBBB;
Dalvik 寄存器
扫盲结束了, 开始重点了
Dalvik 字节码的类型, 方法, 与字段表示方法
- 类型 Dalvik 字节码只有两种类型, 基本与引用, 话不多说, 看图;
每个 Dalvik 寄存器都是 32 位大小, 对于小鱼或者等于 32 位长度的类型来说, 一个寄存器就可以存放该类型的值, 而像 J(long),D(double) 等 64 位的类型, 它们的值是使用相邻量个寄存器来存储的, v0 和 v1 或者 vN 与 vN+1 等;L 就好理解了, 表示任何一个 Java 类, 在 Dalvik 汇编代码中, 它们以 "Lpackage/name/ObjectName;" 表示, 注意最后一个分号, 比如 "Ljava/lang/String;" 相当于 String;[类型就是所有的数组,[后面紧跟基本类型的描述符, 如 [I 表示一个整型一维数组,->int[],[[I 表示 int[][]<—> [Ljava/lang/String; 表示对象数组 String [];
- 方法 Dalvik 使用方法名, 类型参数与返回值来描述一个方法; 格式如下:Lpackage/name/ObjectName;->MethodName(III)Z 说明:Lpackage/name/ObjectName; 是一个类型;MethodName 方法名 (III) 参数, 三个 int 参数 Z 返回值 void
栗子 method(I[[IILjava/lang/String;[Ljava/lang/String;)Ljava/lang/String; 咳咳, 按照上面的知识, 将其转换为 Java 形式的代码为:String method(int ,int[][],String,String[])
- 字段 字段和方法很相似, 就是没有参数和返回值, 取而代之的是字段的类型, 格式如下 Lpackage/name/ObjectName;->FieldName:TYPE 说明:Lpackage/name/ObjectName; 是一个类型;FieldName 字段名 TYPE 字段类型 FieldName 与 TYPE 用冒号隔开
栗子 name:Ljava/lang/String; 转换:String name;
Dalvik 代码中的字段代码以. field 指令开头, 根据字段类型不同, 在字段指令的开始, 可能会用到井号 "#" 加以注释;
Dalvik 指令集
指令特点
Dalvik 指令在调用格式上模仿了 C 语言的调用约定. Dalvik 指令语法与助词符有如下特点:
- 参数采用从目标 (destination) 到源 (source) 的方式;
- 根据字节码的布局与选项的不同, 一些字节码添加了字节码后缀消除歧义, 这些后缀通过在字节码主名称后添加斜杠 "/" 来分隔开;
- 在指令集的描述中, 宽度值中的每个字母表示宽度为 4 位;
- 根据字节码的大小与类型的不同, 一些字节码添加了名称后缀以消除歧义:
- 32 位常规类型的字节码, 未添加任何后缀;
- 64 位常规类型的字节码以 - wide 后缀;
- 特殊类型的字节码根据具体类型添加后缀, 他们可以是 - boolean,-byte,-char,-short,-int,-long,-float,-double,-object,-string,-class,-void 之一;
栗子
"move-wide/from16 vAA,vBBBB"
move 为基础字节码. 标识这是基本操作;
wide 为名称后缀. 标识指令操作的数据宽度 (64 位);
from16 位字节码后缀. 标识源为一个 16 位的寄存器引用变量;
vAA 为目的寄存器, 它始终在源的前面 取值范围为 v0-v2^8-1(255);
vBBBB 为源寄存器, 取值范围为 v0-v2^16-1(65535)
空操作指令
空操作指令的助记符为 nop, 他的值是 00, 通常 nop 指令被用过对齐代码用途, 没啥大用;
数据操作指令
数据操作指令为 move.move 指令的原型为 move destination,source 或者 move destination,move 指令根据字节码的大小与类型不同, 后面会跟上不同的后缀.
栗子 (表示太多直接上图, 都差不多)
返回指令
返回指令指的是函数结尾时运行的最后一条指令. 他的基础字节码位 return, 共有以下四条返回指令
栗子
- "return-void" 返回一个 void
- "return vAA" 返回一个 32 位非对象类型的值, 返回值寄存器位 8 位的寄存器 vAA;
- "return-wide vAA" 返回一个 64 位非对象类型的值, 返回值寄存器位 8 位的寄存器 vAA;
- "return-object vAA" 返回一个对象类型的值, 返回值寄存器位 8 位的寄存器 vAA;
数据定义指令
数据定义指令用来定义程序中用到的变量, 字符串, 类等数据, 他的基础字节码为 const
栗子 (表示太多直接上图 -_-)
锁指令
锁指令多用在多线程程序中对同一对象的操作, Dalvik 指令集中有两条锁指令.
- "monitor-enter vAA" 为指定的对象获取锁
- "monitor-exit vAA" 为指定的对象释放锁
实例操作指令
与实例相关的操作包括实例的类型传换, 检查及新建等;
- "check-case vAA,type@BBBB" 将 vAA 中的对象引用强转为 BBBB 类型;
- (BBBB)vAA;
- "instance-of vA,vB,type@CCCC" 判断 vB 中的对象引用是否能转成 CCCC 类型, 能 vA=1, 不能 vA=0;
- if(vB.instanceof(type@CCCC)){
- vA =1;
- }else{
- vA =0;
- }
- "new-instance vAA,type@BBBB" 新建一个 BBBB 的对象 vAA,BBBB 不能为数组
- BBBB vAA = new BBBB();
- "check-cast/jumbo vAAAA,type@BBBBBBBB" 与 "check-case vAA,type@BBBB" 作用相同, 只是取值范围更大 (Android 4.0 新增)
- "instance-of/jumbo vAAAA,vBBBB,type@CCCCCCCC" 与 "instance-of vAA,vBB,type@CCCC" 作用相同, 只是取值范围更大 (Android 4.0 新增)
- "new-instance/jumbo vAAAA,type@BBBBBBBB" 与 "new-instance vAA,type@BBBB" 作用相同, 只是取值范围更大 (Android 4.0 新增)
数组操作指令
数组操作包括获取数组长度 (指的是数组的条目个数), 新建数组, 数组赋值, 数组元素取值与赋值等操作;
- vA = vB.length; // 将vB的长度赋值给vA
- "new-array vA,vB,type@CCCC"
- vA = CCCC[vB]; // 构建一个vB大的CCCC类型的数组赋值给vA
- 其余的附图
异常指令
Dalvik 指令集中有一条指令用于抛出异常
- "throw vAA" 抛出 vAA 寄存器中指定类型的异常
跳转指令
Dalvik 指令集中有三种跳转指令: 无条件跳转 (goto), 分支跳转 (switch), 条件跳转 (if)
- "goto +AA" 无条件跳转到指定偏移处, 偏移量 AA 不能为 0;
- "goto/16+AAAA" 无条件跳转到指定偏移处, 偏移量 AAAA 不能为 0;
- "goto/32+AAAAAAAA" 无条件跳转到指定偏移处;
- "packed-switch vAA,+BBBBBBBB" 分支跳转指令. vAA 寄存器为 switch 分支中需要判断的值即 (switch(vAA)),BBBBBBBB 指向一个 packed-switch-payload 格式的偏移表, 表中的值是规律递增的.(先这么记住就好, 感兴趣可以找百度..)
- "sparse-switch vAA,+BBBBBBBB" 分支跳转指令, vAA 寄存器为 switch 分支中需要判断的值即 (switch(vAA)),BBBBBBBB 指向一个 sparse-switch-payload 格式的偏移表, 表中的值是无规律的偏移量.
- "if-test vA,vB,+CCCC" 条件跳转指令, 比较 vA 与 vB 的值, 如果比较结果满足就跳转到 CCCC 指定的偏移处, 偏移量 CCCC 不能为 0,if-test 类型的指令有以下几条:
- "if-eq vA, vB, :cond_xx" 如果 vA 等于 vB 则跳转到: cond_xx
- "if-ne vA, vB, :cond_xx" 如果 vA 不等于 vB 则跳转到: cond_xx
- "if-lt vA, vB, :cond_xx" 如果 vA 小于 vB 则跳转到: cond_xx
- "if-ge vA, vB, :cond_xx" 如果 vA 大于等于 vB 则跳转到: cond_xx
- "if-gt vA, vB, :cond_xx" 如果 vA 大于 vB 则跳转到: cond_xx
- "if-le vA, vB, :cond_xx" 如果 vA 小于等于 vB 则跳转到: cond_xx
- "if-testz vAA,+BBBB" 条件跳转指令, 那 vAA 与 0 作比较, 满足结果或者不满足结果就跳转到 BBBB 的指定偏移处 BBBB 不能为 0, if-testz 类型的指令有以下几条:
- "if-eqz vA, :cond_xx" 如果 vA 等于 0 则跳转到: cond_xx
- "if-nez vA, :cond_xx" 如果 vA 不等于 0 则跳转到: cond_xx
- "if-ltz vA, :cond_xx" 如果 vA 小于 0 则跳转到: cond_xx
- "if-gez vA, :cond_xx" 如果 vA 大于等于 0 则跳转到: cond_xx
- "if-gtz vA, :cond_xx" 如果 vA 大于 0 则跳转到: cond_xx
- "if-lez vA, :cond_xx" 如果 vA 小于等于 0 则跳转到: cond_xx
比较指令
比较指令用于对两个寄存器的值 (浮点型或者长整型) 进行比较格式为:
"cmpkind vAA,vBB,vCC"
Dalvik 指令集中共有 5 条比较指令:
- "cmpl-float" 比较两个 float 值;
- if(vBB == vCC){
- vAA =0;
- }else if(vBB>vCC){
- vAA = -1;
- }else if(vBB){
- vAA =1;
- }
- "cmpg-float" 比较两个 float 的值
- if(vBB == vCC){
- vAA =0;
- }else if(vBB{
- vAA = -1;
- }else if(vBB>vCC>){
- vAA =1;
- }
当 cmpg 或者 cmp 时, B > C 时 A = 1, 反之 - 1; 当 cmpl 时, B > C 时 A = -1 反之 1;
- "cmpg-double" 比较两个 double 的值
- "cmpl-double" 比较两个 double 的值
- "cmp-long" 比较两个 long 的值
字段操作指令
字段操作指令用来对对象实例的字段进行读写操作.
字段的类型可以是 Java 中有效的数据类型, 对普通字段与静态字段操作有两种指令集, 分别是 "iinstanceop vA,vB,field@CCCC" 与 "sstaticop vAA,field@BBBB".
在 Android 4.0 系统中, 有 "iinstanceop /jumbovAAAA,vBBBB,field@CCCCCCCC" 与 "sstaticop/jumbo vAAAA,field@BBBBBBBB". 和上面的两种作用相同, 只是加了 jmpbo 后缀, 寄存器与指令索引取值范围更大 (后面的只会说有 / jumbo 指令后缀的指令集, 作用就不指明了)
普通字段指令的指令前缀为 i, 如. 对普通字段读操作使用 iget 指令, 写操作使用 iput 指令; 静态字段的指令前缀为 s, 如. 对静态字段的读操作为 sget, 写操作为 sput;
根据访问的字段类型不同, 字段操作指令后面会紧跟字段类型的后缀, 如 iget-byte 指令表示读取实例字段的值类型为 byte;
方法调用指令
方法调用指令负责调用类实例的方法, 它的基础指令为 invoke, 方法调用指令有 "invoke-kind{vC,vD,vE,vF,vG},meth@BBBB"与"invoke-kind/range{vCCCC…VNNNN},meth@BBBB" 两类, 这两类指令作用没啥不同, 后者在设置参数寄存器时使用了 range 来指定寄存器的范围, 根据方法类型的不同, 共有如下 5 条方法调用指令:
- "invoke-virtual" 调用实例的虚方法
- "invoke-super" 调用实例的父类方法
- "invoke-direct" 调用实例的直接方法
- "invoke-static" 调用实例的静态方法
- "invoke-interface" 调用实例的接口方法 Android 4.0 有 jumbo 的指令集;
方法调用指令的返回值必须使用 move-result * 指令来获取:
invoke-static{},Landroid/os/Parcel;->obtain()Landroid/os/Parcel;
move-result-object v0;
数据转换指令
数据转换指令用于将一种类型的数值转换为另一种类型, 他的格式为 "unop vA,vB" 把 vB 中的数据做一定运算 (转换) 放在 vA 中:(比较简单, 直接上图)
数据运算指令
数据运算指令包括算数运算指令与逻辑运算指令:
- 算数运算指令: 加, 减, 乘, 除, 模, 移位等
- 逻辑运算指令: 间与, 或, 非, 抑或等; 上个图吧
其中基础字节码后面的 - type 可以是 - int,-long,-float,-double, 后面 3 类指令也差不多, 就不列了, 触类旁通;
== == == == == == == == == == == == == == == == == == == == == ==
来源: http://blog.csdn.net/redwolfchao/article/details/70160413