Andoird Apps Reverse Engineering III: Dalvik Virtual Machine

来源:互联网 发布:淘宝宣传片双十一 编辑:程序博客网 时间:2024/05/22 05:12

继续上篇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_11: iload_22: iadd3: iload_14: iload_25: isub6: imul7: ireturn
左边的序号即PC值。load 表示把局部变量存入求值stack,i是指令前缀,表示int。_1表示局部变量区的第二个int。iadd将栈顶的两个int弹出并把值相加再压入求值栈中。后面过程以此类推。
 我们再来看Dalvik bytecode。用Android SDK自带的dexdump反编译.dex所得

0000: add-int v0, v3, v40002: sub-int v1, v3, v40004 mul-int/2addr v0, v10005: 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 startupnopnopnopnop# def dataconst/16 v0, 0x8const/4 v1, 0x5const/4 v2, 0x3#move datamove v1, v2# array opsnew-array v0, v0, [Iarray-length v1, v0# instances opnew-instance v1, Ljava/lang/StringBuilder;#method callinvoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V# branchesif-nez v0, :cond_0goto :goto_0:cond_0# data conversionint-to-float v2, v2# data opsadd-float v2, v2, v2# cmpcmpl-float v0, v2, v2# field opssget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;const-string v1, "Hello World"# method callinvoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V# returns:goto_0return-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字样。

0 0
原创粉丝点击