大家都知道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共四个字节组成,在这里他们表示寄存器的参数.
单独使用位标识还无法确定一条指令的意思,必须通过指令格式标识来指定指令的格式编码,约定如下
栗子
“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;
- “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条比较指令:
if(vBB == vCC){
vAA =0;
}else if(vBB>vCC){
vAA = -1;
}else if(vBB<vCC>){
vAA = 1;
}
if(vBB == vCC){
vAA =0;
}else if(vBB<vCC){
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类指令也差不多,就不列了,触类旁通;
== == == == == == == == == == == == == == == == == == == == == ==