继续上篇blog。上次粗略的引入了通过apktool得到的smali code。这次来谈谈详细谈谈Dalvik bytecode。
Android 的apps 虽然基于Java, 但并非跑在传统的Java 虚拟机,而是跑在由Android 开发团队自己定制的Dalvik虚拟机上。Dalvik bytecode也即与跑在Java virtual machine 的java bytecode相对应的一种类似于汇编语言的字节码。与传统的C/C++程序不同,每次当java 程序运行时,实际上是虚拟机动态的把bytecode翻译成机器码,而不是直接跑在binary code上(不过在Android 4.3后引入的ART runtime 把动态翻译过程换成了静态,也就是在安装apk时一次性把bytecode翻译成机器码,以后再每次运行app时就是基于机器码的了,从而提升了运行效率。但翻译后的机器码要比字节码占的空间多,也是一种空间换时间策略吧)。注意这里谈到的虚拟机是针对编程语言,并不是我们日常看到使用的virtual box之类产生的『虚拟计算机』。虚拟机都是为了抽象而生,只是Java的虚拟机用general bytecode抽象了底层实现的binary code (为了portable和garbage collection),而虚拟计算机用虚拟的操作系统和硬件抽象了底层(宿主的)的硬件和操作系统。
言归正传,那Dalvik 虚拟机与Java 虚拟机有蛇呢不同呢?
1. 先看流程
Java: Java source code -compilation-> Java bytecode (.class) -feed-> Java Virtual Machine
Dalvik: Java source code -compilation-> Java bytecode -dx tool-> Dalvik bytecode (.dex) -feed-> Dalvik Virtual Machine
我们注意到在Dalvik中有一步把Java bytecode通过dx工具转成Dalvik bytecode。那dx究竟做了什么了?其实即消除Java bytecode里的冗余信息
1)将所有的Java各个类文件中的constant pool分解,消除冗余并重新合成新的单一的constant pool。(图片来自非虫大大的书)
2. Java 虚拟机基于栈,而Dalvik基于寄存器。
我们知道对于一般program运行所在的runtime 包含调用栈和heap两部分。调用栈用来存储method 的context (运行状态),heap 用来存储new/malloc 出来的动态instance们。
Java VM里调用栈的PC只对当前method有效(从0000开始),用来取指令。调用栈用于记录Java method『调用记录』(activation record),每个stack frame保存线程运行状态 (1. 局部变量区-局部变量和参数 2. 求值栈 -栈中栈: 中间结果,调用别的方法的参数等),每调用一个method 即分配一个frame并把frame压入stack中。当mthod执行结束返回时则pop出相应的frame。
Java bytecode每条指令占用1 byte, 这也是Java bytecode名字的来源。
我们用一下程序作为例子
public int foot(int a, int b) {
return (a + b) * (a - b);
}
把它写成完整的带main的java 程序后,通过javac 编译成.class文件,再使用dx生成dex文件。
用javap反编译.class函数所看到的java字节码为
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
左边的序号即PC值。load 表示把局部变量存入求值stack,i是指令前缀,表示int。_1表示局部变量区的第二个int。iadd将栈顶的两个int弹出并把值相加再压入求值栈中。后面过程以此类推。
我们再来看Dalvik bytecode。用Android SDK自带的dexdump反编译.dex所得
0000: add-int v0, v3, v4
0002: sub-int v1, v3, v4
0004 mul-int/2addr v0, v1
0005: return v0
Dalvik bytecode要比Java的简洁多了,执行开销也更小,也更与X86/ARM等现代assembly code类似。add-int中v3 v4是操作数,v0存放结果。
Dalvik也不能免俗,同样使用调用栈。但与Java不同的是,Dalvik不用栈中栈,而是维护一份寄存器列表。架构如下图所示
所需寄存器竖线在method中的registers字段中给出,
下面来说说Dalvik VM是如何运行program的。
先放上灰常有名的Andorid 架构图:
Android Architecture 是分层的。和所有用分层的东东好处一样,这样做结构清晰,抽象出的每层只要管好自己就行了,避免了出现牵一发而动全身的情况,更方便升级和debug。
Android系统在运行起来后首先加载kernel, 然后执行init进程来使设备初始化。进而读取inic.rc文件并启动系统中的重要外部程序Zygote。Zygote进程用于孵化所有其它Andoid进程。它会首先初始化Dalvik虚拟机,启动system_server并进入Zygote模式,通过socket等候命令。当一个Android app要执行时,system_server进程通过socket方式发送命令给Zygote, Zygote收到命令后fork自身创建一个Dalvik VM的instance来执行app的入口函数,这样app就算启动完成了。流程如下:
Zygote有三种创建进程的方法 1. fork() 2. forkAndSpecialize() 创建一个非Zygote进程 3. forkSystemServer()。 其中fork()和forkSys()后的进程自身可以继续fork(), 而非Zygote进程不可。forkSys()在父进程终止后所有子进程也都会被终止。
进程fork成功后,就交给Dalvik VM来执行了。Dalvik VM 通过loadClassFromDex()装载类,每个类被成功解析后会拥有一个ClassObject类型的数据结构。VM使用gDv.loadedClasses全局hash table来存储与查询所有加载进来的类。dvmVerifyCodeFlow()对装入的代码进行校验,接着V调FindClass()查找并装载main mthod,随后调用·dvmInterpret()函数初始化解释器并执行字节码flow。
JIT (Just-in-time Compilation)
动态编译,一种通过运行时将bytecode翻译为机器码的技术。包含两种翻译方式 1. method方式,以method为单位进行翻译 2. trace方式: 以trace为单位。trace和branch prediction有些关系,对method中很少被执行的路径先搁置,只翻译"热路径"。
Dalvik (汇编)语言基础
格式: 1每16bits 为一个字,字与字之间用空格分隔 2. 每个字母占4 bits; 以单个大写字母作为操作码即占4 bit 3. op表示占8 bits的操作码
e.g. A|G|op BBBB F|E|D|C
此指令包含两个空格,分开三个16 bits的字。A|G|op高8位为A|G。BBBB表示16位的offset, 第三个表示寄存器参数。
指令格式标识: 1. 由三个字符组成,前两个是数字,最后为字母 2. 第一个数字表示指令占多少字 3. 第二个数字表示指令最多使用寄存器个数 3. 表示类型码,介绍如下
每条指令从操作码开始,后面紧跟参数,参数之间用逗号隔开。参数采用vX表示寄存器,"#+X"表示里技术,"+X"表示指令offset, "kind@X"表示常量池索引,kind可以是"string","type", "field"或"meth"。
最终这些bytecode会以16进制编码形式存储。我们可以把它想成机器码(或者类似ASCII码),不过它不是真正的机器码,因为它还要再被JIT翻译一次才能真正成为二进制机器可读的代码。我们不可能直接对着16进制的文件来分析,这时候就需要一些反编译工具把16进制的bytecode还原成用字符和数字的人类可读的形式。类似的工具有baksmaliout和dex,它们还原16 进制bytecode回人类可读bytecode的同时还加入了一些方便理解的"解释"(加了注释),最终成为一种名为smali的code。我们可以把Dalvik 和Smali等同,毕竟smali只是在Dalvik上多点东西。实例如下:
smali文件可以无损还原成16进制dex文件。
Dalvik的寄存器映射到了ARM上,还有一部分则通过调用栈模拟。寄存器为32位,最大支持v0~vBBBB即2 ^ 16 = 65536个。
v表示局部变量寄存器,p表示函数参数寄存器。在上面的例子中, 使用到v0, v1, p0, p1, p2五个寄存器。其中v0和v1表示int a和b, p0表示this (对所在instance的引用), p1和p2存放两个int 参数值。
Dalvik bytecode 只有两种类型,即基本类型和引用类型(对象与数组)。
L类型可以表示Java中的任何类。Java中的package.name.ObjectName到了Dalvik中以Lpackage/name/ObjectName;。如Ljava/lang/String相当于java.lang.String。
[I表示int[], [[I表示int [][],维数不得超过255。[Ljava/lang/lang/String表示字符串数组。
Dalvik中用method name, 参数类型与返回值来表示一个method。如Lpackage/name/ObjectName;->MethodName(III)Z
Lpackage/name/ObjectName;-理解为一个类型,MethodName为具体method name,(III)Z为签名部分,III表示三个整形参数,Z表示返回类型为boolean。
* #virtual method当从父类中继承的时候,虚函数和被继承的函数具有相同的签名。但是在运行过程中,运行系统将根据对象的型別,自动地选择适当的具体实现运行。虚函数是面向对象编程实现多型的基本手段。当从父类中继承的时候,虚函数和被继承的函数具有相同的签名。但是在运行过程中,运行系统将根据对象的型別,自动地选择适当的具体实现运行。即重载。
字段/field和method类似,只是没有签名域,而是字段的类型。e.g. Lpackage/name/ObjectName;->FieldName:Ljava/lang/String;
由所在类(Lpackage/name/ObjectName;) 、字段名与字段类型组成。
* 字段即类中声明的成员变量(数据)
操作指令包括空之类,数据操作(move)指令,返回(return)指令,数据定义(const)指令,锁(多线程,monitor)指令,instance操作指令,数组操作指令,异常(throw)指令,跳转指令,比较指令,字段操作指令,method调用指令(invoke),数据转换指令,数据运算指令。
具体就不在这里敷述了。
把书上的helloworld贴出来
.class public LTest; # define the class name
.super Ljava/lang/Object; # define the parent class
.method public static main([Ljava/lang/String;)V
.registers 4
# .parameter
.prologue # the startup
nop
nop
nop
nop
# def data
const/16 v0, 0x8
const/4 v1, 0x5
const/4 v2, 0x3
#move data
move v1, v2
# array ops
new-array v0, v0, [I
array-length v1, v0
# instances op
new-instance v1, Ljava/lang/StringBuilder;
#method call
invoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V
# branches
if-nez v0, :cond_0
goto :goto_0
:cond_0
# data conversion
int-to-float v2, v2
# data ops
add-float v2, v2, v2
# cmp
cmpl-float v0, v2, v2
# field ops
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;const-string v1, "Hello World"
# method call
invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
# returns
:goto_0
return-void
.end method
命名为HelloWorld.smali。用smail.jar把它翻译成.dex,再把它打包成zip。
用SDK里的adb把它推送到Andoird手机并运行
adb push HelloWorld.zip /data/local/
adb shell dalvikvm -cp /data/local/HelloWorld.zip HelloWorld
shell中应出现HelloWorld字样。