早期Android是Dalvik虚拟机,Android4.4以后,引入ART(Android Runtime)虚拟机
JIT(Just-In-Time)即时编译,AOT(Ahead-Of-Time)预先编译
ART提供对Dalvik的可执行格式与字节码的支持
专有DEX(Dalvik Executable)可执行文件格式
常量池32位索引
基于寄存器架构,完整指令系统
Java源文件 -> Java字节码保存在class文件中 -> Java虚拟机执行
Java源文件 -> Java字节码保存在class文件中 -> Dalvik字节码保存在DEX可执行文件 -> Dalvik虚拟机执行
dx重新排列Java类文件,消除冗余信息,dx将java文件转换为DEX文件
压缩常量池,重复字符串和常量只会出现一次
Java虚拟机基于栈,Dalvik虚拟机基于寄存器
实例
public class Hello {
public int foo(int a, int b) {
return (a + b) * (a - b);
}
public static void main(String[] args) {
Hello hello = new Hello();
System.out.println(hello.foo(5, 3));
}
}
编译文件,指定版本
javac -source 1.7 -target 1.7 Hello.java
生成DEX文件
dx --dex --output=Hello.dex Hello.class
javap反编译Hello.class,查看foo函数的Java字节码
javap -c -classpath . Hello
得到
public int foo(int, int);
Code:
0: iload_1
1: iload_2
2: iadd
3: iload_1
4: iload_2
5: isub
6: imul
7: ireturn
java字节码共占8字节,每条指令没有参数
Java虚拟机指令集为零地址形式的指令集,源参数和目标参数都是隐含的,Java虚拟机提供求值栈的数据结构。
Java程序每一个线程在执行时都有一个PC计数器和一个Java栈
PC计数器以字节为单位记录当前运行位置与方法开头的偏移量,PC计数器只对当前方法有效,Java虚拟机通过PC值来取指令执行。
Java栈记录Java方法调用的活动记录,以帧frame为单位保存线程的运行状态,每调用一个新方法分配新栈帧压栈,方法返回则弹出撤出对应栈帧。
每个栈帧包括:局部变量区、操作数栈、其他
以iload_1
为例分析
i
是前缀,可以是f
float,l
long,d
double
load
是指令,表示将局部变量存入java栈
_1
数字表示对哪个局部变量进行操作,从0开始计数
使用dexdump,查看foo的Dalvik字节码
dexdump -d Hello.dex
得到
Virtual methods -
#0 : (in LHello;)
name : 'foo'
type : '(II)I'
access : 0x0001 (PUBLIC)
code -
registers : 5
ins : 3
outs : 0
insns size : 6 16-bit code units
000198: |[000198] Hello.foo:(II)I
0001a8: 9000 0304 |0000: add-int v0, v3, v4
0001ac: 9101 0304 |0002: sub-int v1, v3, v4
0001b0: b210 |0004: mul-int/2addr v0, v1
0001b2: 0f00 |0005: return v0
catches : (none)
positions :
0x0000 line=3
locals :
0x0000 - 0x0006 reg=2 this LHello;
Dalvik指令简洁,只用了4条
Dalvik虚拟机运行也为每一个线程维护一个PC和一个调用栈,不同的是,该调用栈维护一个寄存器列表,寄存器的数量在方法结构体的registers
字段给出,根据这个值创建一个虚拟寄存器列表
Android系统从下到上为Linux内核,函数库,Android运行时环境,框架,应用
Android系统启动后,立即执行init进程,完成设备初始化工作,再读取init.rc文件启动系统中重要外部程序Zygote(我查字典后才知道Zygote是受精卵的意思…还真的挺贴切的)
Zygote是Android系统中所有进程的孵化器进程。
Zygote启动后,初始化Dalvik虚拟机,再启动system_server进程进入Zygote模式,通过socket等待命令下达。
执行一个Android应用程序的时候,system_server进程通过Binder IPC方式将命令发送给Zygote,收到命令后,通过fork自身创建一个Dalvik虚拟机实例来执行应用的入口函数,完成程序自启动。
Zygote三种创建进程的方法
fork()
创建一个Zygote进程,这种方法实际上不会被调用forkAndSpecialize()
创建一个非Zygote进程forkSystemServer()
创建一个系统服务进程系统服务进程终止后子进程也终止。Zygote进程可以再分为其他进程,非Zygote进程不能再分。
进程fork后,执行交给Dalvik虚拟机
先通过loadClassFromDex()
函数装载类,每个类成功解析后,都会获得运行时环境中的一个ClassObject
类型的数据结构存储,虚拟机使用gDvm.loadedClasses
全局散列表来存储和查询所有装载进来的类。接下来字节码验证器使用dvmVerifyCodeFlow()
函数对装入的代码进行校验,虚拟机调用FindClass()
函数查找并装载main方法类,最后虚拟机调用dvmInterpret()
函数来初始化解释器并执行字节码流
即时编译JIT,又叫动态编译,在运行时将字节码翻译为机器码使程序执行速度加快。
主流JIT包括两种字节码编译方式
method
方式:以函数方法为单位编译trace
方式:以trace为单位进行编译trace方式:在函数中,只有少数代码是顺序执行的,多数代码有好几条执行路径,其中一些路径很少执行,称为冷路径,执行频繁的称为热路径,使用trace编译能快速获取热路径的代码。
Dalvik虚拟机默认采用trace方式编译代码,同时支持JIT
Dalvik汇编语言
基于寄存器的设计,方法在内存创建以后拥有固定大小的栈帧,栈帧空间取决于方法中寄存器数目,运行时数据和代码在DEX文件中
指令流以16位无符号整型为存储单元
指令的位描述和指令格式标识
位描述
A|G|op BBBB F|E|D|C
为例,空格将其分为3个部分,每个部分16位
第一个部分A|G|op
,高8位由A和G组成,低字节op。
第二个部分BBBB
表示一个16位的偏移量
第三个部分F|E|D|C
分别表示寄存器参数
指令格式标识
目前主流DEX文件反汇编工具:Android官方dexdump
,第三方baksmali
两种反编译代码结构大致相同,寄存器命名有不同,dexdump
使用的是以v开头的寄存器,baksmali
使用的是以v和p开头的寄存器。
基于寄存器架构,设计之初为ARM架构,Dalvik将部分寄存器映射到ARM寄存器上,还有一部分通过调用栈进行模拟。
Dalvik使用的寄存器都是32位的,64位类型用相邻两个寄存器表示
语法格式为op vAAAA, vBBBB
,则最大值为216-1,即65535,从0开始,取值为v0-v65535
每个函数在头部用.registers
指令指定所使用的寄存器数目,当虚拟机执行到这个函数的时候,会根据寄存器数目分配适当的栈空间。
虚拟机通过处理字节码对寄存器进行的读写操作实际上都是对栈空间进行读写操作。
fp为ARM寄存器栈帧寄存器,取idx的寄存器值
两种寄存器表示方法:v命名法和p命名法
假设一个函数使用M个寄存器,函数有N个参数。则参数使用最后的N个寄存器,局部变量用从v0开始的M-N个寄存器。
以foo为例子,一共使用5个寄存器,2个显式的整型参数。
该函数是Hello类的非静态方法,调用时会传入一个隐式的Hello对象引用。所以实际上有三个参数,局部变量使用前两个寄存器,参数使用后三个寄存器。
# virtual methods
.method public foo(II)I
.registers 5
.prologue
.line 3
add-int v0, p1, p2
sub-int v1, p1, p2
mul-int/2addr v0, v1
return v0
.end method
v命名法是所有的寄存器都是v+数字。
p命名法是局部变量使用的寄存器为v+数字,参数使用的寄存器为p+数字。可以通过前缀判断寄存器是局部变量寄存器还是参数寄存器。
基本类型和引用类型,这两种类型表示Java语音的全部类型。
Java中的对象和数组都属于引用对象,其他Java类型为基本类型。
Dalvik字节码类型描述符
语法 | 含义 |
---|---|
v | void |
Z | boolean |
B | byte |
S | short |
C | char |
I | int |
J | long |
F | float |
D | double |
L | Java类类型 |
[ | 数组类型 |
Dalvik寄存器每个都是32位的,对于长度小于等于32位的类型,只用一个寄存器就可以存放该类型的值,对J
、D
等64位类型,使用相邻两个寄存器存储。
L
表示Java类型中的任何类,Java代码中表示为package.name.ObhectName
,在Dalvik汇编代码中以Lpackage/name/ObjectName;
表示
[
表示基本类型数组,多个表示多维数组,例如[[I
。最大维数为255
L
和[
可以同时使用
Dalvik用方法名,类型参数,返回值描述一个方法。例如
Lpackage/name/ObjectName;->MethodName(III)Z
括号(III)
为方法的参数,这里意思为三个int类型,Z
表示返回值类型,这里为boolean类型
一个更复杂的例子
method(I[[IILjava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
转换为Java代码为
String method(int, int[][], int, String, Object[]);
baksmali生成的方法代码以.method
指令开始,以.end method
结束
#
为注释
字段的格式
Lpackage/name/ObjectName;->FiledName:Ljava/lang/String;
类型Lpackage/name/ObjectName;
,字段名FiledName
,字段类型Ljava/lang/String;
。
baksmali生成字段代码以.filed
开头
Android4.4以前,可以在dalvik/libdex/DexOpcodes.h
中找到完整Dalvik指令集
ART主导系统,可以在art/runtime/dexinstructionlist.h
中找到系统支持的完整的指令集定义。
Dalvik指令集使用单字节的指令助记符
参数采用从dest到src的方式
为一些字节码添加名称后缀消除歧义
-wide
后缀-string
,-class
,-object
例子:move-wide/from16 vAA, vBBBB
move
为基础字节码,-wide
为名称后缀,表示指令操作的数据宽度为64位,from16
为字节码后缀,表示源为一个16位寄存器引用变量,vAA
为dest寄存器,vBBBB
为src寄存器
nop
,值为00,通常用于对齐代码,无操作
原型为move dest, src
,有不同后缀
指函数运行结束时运行的最后一条指令,基础字节码为return
定义程序用到的常量,字符串,类等类型,基础字节码为const
多线程程序对同一对象的操作中
monitor-enter vAA
,为指定对象获取锁
monitor-exit vAA
,释放指定对象的锁
包括类型转换,检查,创建等
check-cast vAA, type@BBBB
,用于将vAA寄存器中对象引用转换为指定的类型,失败抛出ClassCastException异常。
instance-of vA, vB, type@CCCC
,用于判断vB寄存器中的对象引用是否可以转换为指定类型,如果可以vA赋值1,否则赋值0
new-instance vAA, type@BBBB
,用于构造一个指定类型对象的新实例,对象引用赋值给vAA寄存器,type指定类型不能是数组
获取数组长度,新建数组,数组赋值,数组元素取值与赋值
array-length vA, vB
,获取给定vB中数组长度,值赋给vA
new-array vA, vB, type@CCCC
,用于构造指定类型type@CCCC
和大小vB的数组,值赋给vA
filled-new-array {vC, vD, vE, vF, vG}, type@BBBB
,用于构造指定type类型和vA大小的数组并填充数组内容,vA为隐含使用的
filled-new-array/range {vCCCC, ..., vNNNN}, type@BBBB
,该指令与上一条功能相同,使用range指定取值范围
arrayop vAA, vBB, vCC
,用于对vBB寄存器指定的数组元素进行取值和赋值,vCC中为索引,vAA为取得值或赋的值,,读取用aget
,赋值用aput
throw vAA
,抛出vAA中指定类型的异常
三种跳转:无条件跳转goto
,分支跳转switch
,条件跳转if
packed-switch vAA, +BBBBBBBB
,vAA为switch分支判断的值,BBBBBBBB
指向一个packed-switch-payload
格式偏移表。
if-test vA, vB, +CCCC
条件跳转,比较vA和vB的值,比较结果满足则跳转到CCCC指定的偏移处,test
有好几种类型
eq:==, ne:!=, lt:<, ge:>=, gt:>, le:<=
eqz:==0, nez:!=0, ltz:<0, gez:>=0, gtz:>0, lez:<=0
对两个寄存器的值,浮点或者长整型进行比较,格式为cmpkind vAA, vBB, vCC
,其中vBB和vCC是比较的两个寄存器,比较的结果放在vAA中。共有5条比较指令
对对象实例的字段进行读写 普通字段: 静态字段: 后面加上字段类型后缀,例如 调用类实例的方法,基础指令为 方法类型不同,有5种 返回值必须用 将一种类型的数值转换为另一种类型的数值,格式 算术运算指令和逻辑运算指令,加减乘除模移位,与或非异或 数据运算指令有4类 运算+数据类型 以上就是所以支持指令,Android4.0以后扩充了一部分指令,助记符后添加 新建 添加指令,在 会得到 启动Android运行环境,cmd执行 将文件输入sdcard目录中 会看到输出Hello Worldcmpl-float
比较单精度浮点数,vBB>vCC则vAA=-1,vBB=vCC则vAA=0,vBBcmpg-float
cmpl-double
cmpg-double
cmp-long
字段操作指令
iinstanceop vA, vB, fileld@CCCC
,前缀为i
sstaticop vAA, field@BBBB
,前缀为s
iget-byte
,sget-wide
方法调用指令
invoke
invoke-kind {vC, vD, vE, vF, vG}, meth@BBBB
invoke-kind/range {vCCCC,...,vNNN}, meth@BBBB
invoke-virtual
实例的虚方法invoke-super
实例的父类方法invoke-direct
实例的直接方法invoke-static
实例的静态方法invoke-interface
实例的接口方法move-result*
指令获取数据转换指令
unop vA, vB
,vB中放需要转换的数据,转换结果放在vA中。neg-[type]
求补not-[type]
求反XXX-to-XXX
一个类型到另一个类型数据运算指令
binop vAA, vBB, vCC
将寄存器vBB和vCC进行运算,结果放在vAAbinop/2addr vA, vB
将vA与vB进行运算,结果在vAbinop/lit16 vA, vB, #+CCCC
vB与常量CCCC进行运算,结果在vAbinop/lit8 vAA, vBB, #+CC
同上add, sub, mul, div, rem(就是mod), and, or, xor, shl, shr(>>), ushr(>>>)
jumbo
Dalvik指令练习
编写smali文件
HelloWorld.smali
文件,模版框架如下.class public LHelloWorld;
.super Ljava/lang/Object;
.method public static main([Ljava/lang/String;)V
.registers 4
.prologue
return-void
.end method
.prologue
后添加.class public LHelloWorld;
.super Ljava/lang/Object;
.method public static main([Ljava/lang/String;)V
.registers 4
.prologue
nop
nop
nop
nop
#数据定义
const/16 v0, 0x8
const/4 v1, 0x5
const/4 v2, 0x3
#数组操作
new-array v0, v0, [I
array-length v1, v0
#实例操作
new-instance v1, Ljava/lang/StringBuilder;
#方法调用
invoke-direct {v1}, Ljava/lang/StringBuilder;->
编译smali文件
java -jar smali.jar a HelloWorld.smali
out.dex
文件测试运行
adb push out.dex /sdcard/
adb shell dalvikvm -cp /sdcard/out.dex HelloWorld