Android Dalvik虚拟机执行指令格式与字节码规范

Dalvik虚拟机,是Google开发的Android移动设备平台的核心组成部分之一。它可以支持已转换为.dex(即“Dalvik Executable”)格式的Java应用程序的运行。.dex格式是专为Dalvik设计的一种压缩格式,适合内存和处理器速度有限的系统。在Android4.4后为了解决Android性能问题,Google对Android虚拟机进行了修改,将Dalvik虚拟机AOT编译的ART(Android RunTime)虚拟机,虽然使用了ART虚拟机代替Dalvik但是ART在设计之初就完整地提供了对Dalvik格式以及字节码规范的支持。从Android 5.0版起,Android Runtime(ART)取代Dalvik成为系统内默认虚拟机。

Dalvik虚拟机和JVM虚拟机

大多数虚拟机包括JVM都是一种堆栈机器,而Dalvik虚拟机则是基于寄存器。两种架构各有优劣,一般而言,基于堆栈的机器需要更多指令,而基于寄存器的机器指令更长。

基于寄存器虚拟架构的Dalvik虚拟机

Dalvik虚拟机由Google为Android操作系统实现,并作为在Android设备上运行的Java代码解释器。它是一个进程虚拟机,Android OS的底层Linux内核为每个进程生成一个新的Dalvik VM实例。Android中的每个进程都有自己的Dalvik VM实例。如果一个Dalvik VM崩溃,这可以减少多应用程序失败的可能性。Dalvik实现了寄存器机器模型,与标准Java字节码(在基于JVM的堆栈上执行8位堆栈指令)不同,它使用16位指令集。寄存器在Dalvik中实现为4位字段。

Dalvik虚拟机和JVM的区别

  • 运行的字节码不同:
    JVM运行的是Java字节码,Dalvik虚拟机运行的是Dalvik字节码,JVM通过对编译后的class文件解码来运行程序,Dalvik虚拟机的Dalvik字节码由Java字节码转换而来并打包到DEX文件中来解释运行。
  • Dalvik虚拟机执行文件的体积更小效率更高
    传统的Java字节码class文件中包含了很多冗余的数据,Dalvik对冗余的数据进行了精简压缩,从而减小体积。
  • 虚拟机架构不同

Dalvik字节码和Java字节码的区别

使用下列程序进行编译class文件和DEX文件,分别打开两个文件对比字节码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Demo{

private int verifyAccount(int a,int b){
int c = (a+b)*(a-b);
return c;
}

public static void main(String args[]){
Demo d = new Demo();
int res = d.verifyAccount(10,6);
System.out.println(res);

}

}

使用javac编译java class文件

1
javac Demo.java

使用javap反编译Demo.class

1
javap -c Demo.class

通过反编译可以看到如下JAVA字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Compiled from "Demo.java"
class Demo {
Demo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public static void main(java.lang.String[]);
Code:
0: new #2 // class Demo
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: bipush 10
11: bipush 6
13: invokespecial #4 // Method verifyAccount:(II)I
16: istore_2
17: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
20: iload_2
21: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
24: return
}

使用dx命令将java class转换为dex格式
注:dx命令位于Android SDK的build-tools/$version/目录下,读者可以配置环境变量进行使用

1
dx --dex --output=Demo.dex Demo.class

执行完dx命令后会在当前目录生成Demo.dex文件,接下来使用dexdump(该命令位Android SDK的build-tools/$version/目录下,部分SDK位于platform-tools目录)查看字节码

1
dexdump -d Demo.dex

因数据量相对较多,如阅读不便读者可以使用 “>”(dexdump -d Demo.dex > bytecode.txt)将输出重定向到文本文件中方便查阅。以下是dexdump命令输出的Dalvik字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
Processing 'Demo.dex'...
Opened 'Demo.dex', DEX version '035'
Class #0 -
Class descriptor : 'LDemo;'
Access flags : 0x0000 () #类访问标识
Superclass : 'Ljava/lang/Object;' #父类类型
Interfaces -
Static fields - #静态字段
Instance fields - #实例字段
Direct methods -
#0 : (in LDemo;)
name : '<init>' #Demo构造函数
type : '()V' #方法声明类型:由返回类型和参数列表组成,并且返回类型在参数列表前面
access : 0x10000 (CONSTRUCTOR)
code -
registers : 1 #声明该方法所需要的寄存器数量
ins : 1 #该方法的参数个数
outs : 1 #调用其他方法时使用的寄存器个数
insns size : 4 16-bit code units #指令集的个数,十六位
00014c: |[00014c] Demo.<init>:()V
00015c: 7010 0400 0000 |0000: invoke-direct {v0}, Ljava/lang/Object;.<init>:()V // method@0004
000162: 0e00 |0003: return-void
catches : (none)
positions :
0x0000 line=1
locals :
0x0000 - 0x0004 reg=0 this LDemo;

#1 : (in LDemo;)
name : 'main' #main函数
type : '([Ljava/lang/String;)V' #方法声明类型:返回类型V(Voide),参数列表String数组类型
access : 0x0009 (PUBLIC STATIC)
code -
registers : 4 #该函数所需要的寄存器数量
ins : 1
outs : 3
insns size : 18 16-bit code units #000164~000196为指令集
000164: |[000164] Demo.main:([Ljava/lang/String;)V
000174: 2200 0100 |0000: new-instance v0, LDemo; // type@0001
000178: 7010 0000 0000 |0002: invoke-direct {v0}, LDemo;.<init>:()V // method@0000
00017e: 1301 0a00 |0005: const/16 v1, #int 10 // #a
000182: 1262 |0007: const/4 v2, #int 6 // #6
000184: 7030 0200 1002 |0008: invoke-direct {v0, v1, v2}, LDemo;.verifyAccount:(II)I // method@0002
00018a: 0a00 |000b: move-result v0
00018c: 6201 0000 |000c: sget-object v1, Ljava/lang/System;.out:Ljava/io/PrintStream; // field@0000
000190: 6e20 0300 0100 |000e: invoke-virtual {v1, v0}, Ljava/io/PrintStream;.println:(I)V // method@0003
000196: 0e00 |0011: return-void
catches : (none)
positions :
0x0000 line=9
0x0005 line=10
0x000c line=11
0x0011 line=13
locals :

#2 : (in LDemo;)
name : 'verifyAccount' #verifyAccount函数
type : '(II)I' #方法声明类型:返回类型I(Int),参数列表II标识有两个Int类型参数
access : 0x0002 (PRIVATE)
code -
registers : 5
ins : 3
outs : 0
insns size : 6 16-bit code units
000198: |[000198] Demo.verifyAccount:(II)I
0001a8: 9000 0304 |0000: add-int v0, v3, v4 #将V3和V4寄存器的值进行相加然后放入V0寄存器中
0001ac: 9101 0304 |0002: sub-int v1, v3, v4 #将V3和V4寄存器的值进行相减然后放入V1寄存器中
0001b0: b210 |0004: mul-int/2addr v0, v1 #将V0和V1寄存器的值进行乘法运算然后放入V0寄存器中
0001b2: 0f00 |0005: return v0 #将V0寄存器的值返回
catches : (none)
positions :
0x0000 line=4
0x0005 line=5
locals :
0x0000 - 0x0006 reg=2 this LDemo;

Virtual methods -
source_file_idx : 1 (Demo.java)

Dalvik虚拟机在Android系统中的实例化流程

Android中Dalvik和进程的关系是怎样的?Dalvik是如何被系统实例化的?使用下图来展示Android OS内核启动过程:

系统启动时,引导加载程序会将内核加载到内存中并初始化系统参数,在此之后:

  • 内核运行Init程序,该程序是系统中所有进程的父进程。
  • Init程序启动系统守护进程和非常重要的“Zygote”服务。
  • Zygote进程创建一个Dalvik实例,该实例将成为系统中所有Dalvik VM实例的父Dalvik进程。
  • Zygote进程还设置了一个BSD读取套接字并监听传入的请求。
  • 当收到对Dalvik VM实例的新请求时,Zygote进程会分叉父Dalvik VM进程并将子进程发送到请求的应用程序

Dalvik 可执行指令格式

Dalvik虚拟机有专门的指令集以及指令格式和调用规范,其设计准则如下:

  • 基于寄存器,帧栈的大小在创建时就已经确定切固定不变。每一帧由特定数量的寄存器以及执行该方法所需的所有数据构成,例如程序计数器和对包含该方法的.dex文件的引用。
  • 当用于整数和浮点数时,寄存器会被视为32位。如果值是64位,则使用两个相邻的寄存器。对于寄存器对,没有边界要求。
  • 当用于对象引用时,寄存器的大小必须能容纳目标引用类型。
  • 不同的数据类型按胃表示
  • 如果一个方法有N个参数,则在该方法的调用帧的最后N个寄存器中按顺序传递参数。对于wide类型使用响铃的两个寄存器来组合传递。对实例方法,传递一个this引用作为其第一个参数。

Dalvik指令格式

Dalvik指令由位描述和指令标识符决定,位描述如下:

  • 每16位用空格表示。
  • 每个字母表示4位,每个字母按顺序从高字节到低字节排序,每4位之间使用竖线“|”分割以方便阅读
  • 顺序采用英文大小写字母A-Z表示4位的操作码。op表示8位的操作码
  • 带斜划的零(“Ø”)用于表示所有在指示位置的位必须为零。

例如“B|A|op CCCC”格式表示其包含两个 16 位代码单元。第一个字由低8位(op)中的操作码和高8位(BA)中的两个四位值组成;第二个字由单个16位(CCCC)值组成。遵循Dalvik位描述规则每16位使用空格表示。

单独使用位描述是无法确定一条指令的,必须通过指令格式标识符来指定格式编码。
大多指令格式标识符包含三个字符:前两个是十进制数,最后一个是字母。第一个十进制数表示格式中16位代码单元的数量。第二个十进制数表示格式包含的最大寄存器数量。第三个字母为类型码,标识指令所使用的额外数据的类型,如下图。

以指令格式符23x为例,第一个数字2表示指令由2个十六位代码单元的数量,第二个数字3表示该指令需要的寄存器数量为三个寄存器,第三个字母x表示该指令没有额外数据。

Dalvik寄存器

指令集与字节码对照表

点击查看字节码与指令集对照表

随意分享,您的支持将鼓励我继续创作!