第 3 章 进入Android Dalvik虚拟机

第 3 章 进入Android Dalvik虚拟机

虽然Android 平台使用Java语言来开发应用程序,但Android程序却不是运行在标准Java虚拟机上的。可能是为了解决移动设备上软件运行效率的问题,也可能是为了规避与Oracle公司的版权纠纷。Google为Android平台专门设计了一套虚拟机来运行Android程序,它就是Dalvik Virtual Machine(Dalvik虚拟机)。本章将讨论Dalvik虚拟机的特性及基于Dalvik字节码的汇编语言知识。

3.1 Dalvik虚拟机的特点——掌握Android程序的运行原理

本节主要介绍Dalvik的基本特性以及工作原理,这对掌握Android程序运行原理尤为重要。

3.1.1 Dalvik虚拟机概述

Google于2007年底正式发布了Android SDK,Dalvik虚拟机也第一次进入了人们的视野。它的作者是丹·伯恩斯坦(Dan Bornstein),名字来源于他的祖先曾经居住过的名叫Dalvik的小渔村。Dalvik虚拟机作为Android平台的核心组件,拥有如下几个特点:

  • 体积小,占用内存空间小;
  • 专有的DEX可执行文件格式,体积更小,执行速度更快;
  • 常量池采用32位索引值,寻址类方法名、字段名、常量更快;
  • 基于寄存器架构,并拥有一套完整的指令系统;
  • 提供了对象生命周期管理、堆栈管理、线程管理、安全和异常管理以及垃圾回收等重要功能;
  • 所有的Android程序都运行在Android系统进程里,每个进程对应着一个Dalvik虚拟机实例。

3.1.2 Dalvik虚拟机与Java虚拟机的区别

Dalvik虚拟机与传统的Java虚拟机有着许多不同点,两者并不兼容,它们显著的不同点主要表现在以下几个方面:

  1. Java虚拟机运行的是Java字节码,Dalvik虚拟机运行的是Dalvik字节码。

    传统的Java程序经过编译,生成Java字节码保存在class文件中,Java虚拟机通过解码class文件中的内容来运行程序。而Dalvik虚拟机运行的是Dalvik字节码,所有的Dalvik字节码由Java字节码转换而来,并被打包到一个DEX(Dalvik Executable)可执行文件中。Dalvik虚拟机通过解释DEX文件来执行这些字节码。

  2. Dalvik可执行文件体积更小。

    Android SDK中有一个叫dx的工具负责将Java字节码转换为Dalvik字节码。dx工具对Java类文件重新排列,消除在类文件中出现的所有冗余信息,避免虚拟机在初始化时出现反复的文件加载与解析过程。一般情况下,Java类文件中包含多个不同的方法签名,如果其他的类文件引用该类文件中的方法,方法签名也会被复制到其类文件中,也就是说,多个不同的类会同时包含相同的方法签名,同样地,大量的字符串常量在多个类文件中也被重复使用。这些冗余信息会直接增加文件的体积,同时也会严重影响虚拟机解析文件的效率。dx工具针对这个问题专门做了处理,它将所有的Java类文件中的常量池进行分解,消除其中的冗余信息,重新组合形成一个常量池,所有的类文件共享同一个常量池。dx工具的转换过程如图3-1所示。由于dx工具对常量池的压缩,使得相同的字符串、常量在DEX文件中只出现一次,从而减小了文件的体积。

    图3-1 Java文件转换为DEX文件

  3. Java虚拟机与Dalvik虚拟机架构不同。

    Java虚拟机基于栈架构。程序在运行时虚拟机需要频繁的从栈上读取或写入数据,这个过程需要更多的指令分派与内存访问次数,会耗费不少CPU时间,对于像手机设备资源有限的设备来说,这是相当大的一笔开销。

    Dalvik虚拟机基于寄存器架构。数据的访问通过寄存器间直接传递,这样的访问方式比基于栈方式要快很多。下面通过一个实例来对比一下Java字节码与Dalvik字节码的区别。测试代码如下。

    public class Hello {
        public int foo(int a, int b) {
            return (a + b) * (a - b);
        }
    
        public static void main(String[] argc) {
            Hello hello = new Hello();
            System.out.println(hello.foo(5, 3));
        }
    }

    将以上内容保存为Hello.java。打开命令提示符,执行命令“javac Hello.java”编译生成Hello.class文件。然后执行命令“dx --dex --output=Hello.dex Hello.class”生成dex文件。

    接下来使用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

    使用dexdump.exe(位于Android SDK的platform-tools目录中)查看foo() 函数的Dalvik字节码,执行以下命令。

    dexdump.exe -d Hello.dex

    命令执行后整理输出结果,可以得到如下代码。

    Hello.foo:(II)I
    0000: add-int v0, v3, v4
    0002: sub-int v1, v3, v4
    0004: mul-int/2addr v0, v1
    0005: return v0

    注意 如果使用JDK1.7编译Hello.java,生成的Hello.class默认的版本会比较低,使用dx生成dex文件会提示class文件无效。解决方法是强制指定class文件的版本,执行如下命令重新编译。

    javac -source 1.6 -target 1.6 Hello.java

    使用dx工具编译Hello.class时,如果提示无法找到Hello.class文件,可以将Hello.class文件与dx放同一目录后重新编译。

    查看上面的Java字节码,发现foo() 函数一共占用了8个字节,代码中每条指令占用1个字节,并且这些指令都没有参数。那么这些指令是如何存取数据的呢?Java虚拟机的指令集被称为零地址形式的指令集,所谓零地址形式,是指指令的源参数与目标参数都是隐含的,它通过Java虚拟机中提供的一种数据结构“求值栈”来传递的。

    对于Java程序来说,每个线程在执行时都有一个PC计数器与一个Java栈。PC计数器以字节为单位记录当前运行位置距离方法开头的偏移量,它的作用类似于ARM架构CPU的PC寄存器与x86架构CPU的IP寄存器,不同的是PC计数器只对当前方法有效,Java虚拟机通过它的值来取指令执行。Java栈用于记录Java方法调用的“活动记录”(activation record),Java栈以帧(frame)为单位保存线程的运行状态,每调用一个方法就会分配一个新的栈帧压入Java栈上,每从一个方法返回则弹出并撤销相应的栈帧。每个栈帧包括局部变量区、求值栈(JVM规范中将其称为“操作数栈”)和其他一些信息。局部变量区用于存储方法的参数与局部变量,其中参数按源码中从左到右顺序保存在局部变量区开头的几个slot中。求值栈用于保存求值的中间结果和调用别的方法的参数等,JVM运行时它的状态结构如图3-2所示。

    图3-2 JVM运行状态

    结合代码来理解上面的理论知识。由于每条指令占用一个字节空间,foo() 函数Java字节码左边的偏移量就是程序执行到每一行代码时PC的值,并且Java虚拟机最多只支持0xff条指令。第1条指令iload_1可分成两部分:第一部分为下划线左边的iload,它属于JVM(Java虚拟机)指令集中load系列中的一条,i是指令前缀,表示操作类型为int类型,load表示将局部变量存入Java栈,与之类似的有lload、fload、dload分别表示将long、float、double类型的数据进栈;第二部分为下划线右边的数字,表示要操作具体哪个局部变量,索引值从0开始计数,iload_1表示将第二个int类型的局部变量进栈,这里第二个局部变量是存放在局部变量区foo() 函数的第二参数。同理,第2条指令iload_2取第三个参数。第3条指令iadd从栈顶弹出两个int类型值,将值相加,然后把结果压回栈顶。第4、5条指令分别再次压入第二个参数与第三个参数。第6条指令isub从栈顶弹出两个int类型值,将值相减,然后把结果压回栈顶。这时求值栈上有两个int值了。第7条指令imul从栈顶弹出两个int类型值,将值相乘,然后把结果压回栈顶。第8条指令ireturn函数返回一个int值。到这里,foo() 函数就执行完了。关于Java虚拟机字节码的其它内容,笔者在此就不展开了,读者可以在以下网址找到一份完整的Java字节码指令列表:http://en.wikipedia.org/wiki/Java_bytecode_instruction_listings

    比起Java虚拟机字节码,上面的Dalvik字节码显得简洁很多,只有4条指令就完成了上面的操作。第一条指令add-int将v3与v4寄存器的值相加,然后保存到v0寄存器,整个指令的操作中使用到了三个参数 ,v3与v4分别代表foo() 函数的第一个参数与第二个参数,它们是Dalvik字节码参数表示法之一v命名法,另一种是p命名法,本章3.2小节会详细介绍Dalvik汇编语言。第二条指令sub-int将v3减去v4的值保存到v1寄存器。第三条指令mul-int/2addr将v0乘以v1的值保存到v0寄存器。第四条指令返回v0的值。

    Dalvik虚拟机运行时同样为每个线程维护一个PC计数器与调用栈,与Java虚拟机不同的是,这个调用栈维护一份寄存器列表,寄存器的数量在方法结构体的registers字段中给出,Dalvik虚拟机会根据这个值来创建一份虚拟的寄存器列表。Dalvik虚拟机运行时的状态如图3-3所示。

    图3-3 Dalvik VM运行状态

    通过上面的分析可以发现,基于寄存器架构的Dalvik虚拟机与基于栈架构的Java虚拟机相比,由于生成的代码指令减少了,程序执行速度会更快一些。

3.1.3 Dalvik虚拟机是如何执行程序的

Android系统的架构采用分层思想,这样的好处是拥有减少各层之间的依赖性、便于独立分发、容易收敛问题和错误等优点。Android系统由Linux内核、函数库、Android运行时环境、应用程序框架以及应用程序组成。如图3-4的Android系统架构所示,Dalvik虚拟机属于Android运行时环境,它与一些核心库共同承担Android应用程序的运行工作。

{%}

图3-4 Android系统结构

Android系统启动加载完内核后,第一个执行的是init进程,init进程首先要做的是设备的初始化工作,然后读取init.rc文件并启动系统中的重要外部程序Zygote。Zygote进程是Android所有进程的孵化器进程,它启动后会首先初始化Dalvik虚拟机,然后启动system_server并进入Zygote模式,通过socket等候命令。当执行一个Android应用程序时,system_server进程通过socket方式发送命令给Zygote,Zygote收到命令后通过fork自身创建一个Dalvik虚拟机的实例来执行应用程序的入口函数,这样一个程序就启动完成了。整个流程如图3-5所示。

图3-5 Zygote启动进程

Zygote提供了三种创建进程的方法:

  • fork(),创建一个Zygote进程;
  • forkAndSpecialize(),创建一个非Zygote进程;
  • forkSystemServer(),创建一个系统服务进程。

其中,Zygote进程可以再fork() 出其他进程,非Zygote进程则不能fork 其他进程,而系统服务进程在终止后它的子进程也必须终止。

当进程fork成功后,执行的工作就交给了Dalvik虚拟机。Dalvik虚拟机首先通过loadClassFromDex()函数完成类的装载工作,每个类被成功解析后都会拥有一个ClassObject类型的数据结构存储在运行时环境中,虚拟机使用gDvm.loadedClasses全局哈希表来存储与查询所有装载进来的类,随后,字节码验证器使用dvmVerifyCodeFlow() 函数对装入的代码进行校验,接着虚拟机调用FindClass() 函数查找并装载main方法类,随后调用dvmInterpret() 函数初始化解释器并执行字节码流。整个过程如图3-6所示。

图3-6 Dalvik虚拟机执行程序流程

3.1.4 关于Dalvik虚拟机JIT(即时编译)

JIT(Just-in-time Compilation,即时编译),又称为动态编译,是一种通过在运行时将字节码翻译为机器码的技术,使得程序的执行速度更快。Android 2.2版本系统的Dalvik虚拟机引入了JIT技术,官方宣称新版的Dalvik虚拟机比以往执行速度快3~6倍。

主流的JIT包含两种字节码编译方式:

  • method方式:以函数或方法为单位进行编译。
  • trace方式:以trace为单位进行编译。

method方式很好理解,那什么是trace方式呢?在函数中一般很少是顺序执行代码的,多数的代码都分成了好几条执行路径,其中函数的有些路径在实际运行过程中是很少被执行的,这部分路径被称为“冷路径”,而执行比较频繁的路径被称为“热路径”。采用传统的method方式会编译整个方法的代码,这会使得在“冷路径”上浪费很多编译时间,并且耗费更多的内存;trace方式编译则能够快速地获取“热路径”代码,使用更短的时间与更少的内存来编译代码。

目前,Dalvik虚拟机默认采用trace方式编译代码,同时也支持采用method方式来编译。关于JIT的详细内容本书不做深入的探讨,有兴趣的读者可以参看其它相关资料。

3.2 Dalvik汇编语言基础为分析Android程序做准备

Dalvik虚拟机为自己专门设计了一套指令集,并且制定了自己的指令格式与调用规范。我们将Dalvik指令集组成的代码称为Dalvik汇编代码,将这种代码表示的语言称为Dalvik汇编语言(Dalvik汇编语言并不是正式的语言,只是本书描述Dalvik指令集代码的一种称呼)。本节主要介绍Dalvik汇编语言的相关基础知识。

3.2.1 Dalvik指令格式

一段Dalvik汇编代码由一系列Dalvik指令组成,指令语法由指令的位描述与指令格式标识来决定。位描述约定如下:

  • 每16位的字采用空格分隔开来。
  • 每个字母表示四位,每个字母按顺序从高字节开始,排列到低字节。每四位之间可能使用竖线“|”来表示不同的内容。
  • 顺序采用A~Z的单个大写字母作为一个4位的操作码,op表示一个8位的操作码。
  • “∅”来表示这字段所有位为0值。

以指令格式“A|G|op BBBB F|E|D|C”为例:

指令中间有两个空格,每个分开的部分大小为16位,所以这条指令由三个16位的字组成。第一个16位是“A|G|op”,高8位由A与G组成,低字节由操作码op组成。第二个16位由BBBB组成,它表示一个16位的偏移值。第三个16位分别由F、E、D、C共四个4字节组成,在这里它们表示寄存器参数。

单独使用位标识还无法确定一条指令,必须通过指令格式标识来指定指令的格式编码。它的约定如下:

  • 指令格式标识大多由三个字符组成,前两个是数字,最后一个是字母。
  • 第一个数字是表示指令有多少个16位的字组成。
  • 第二个数字是表示指令最多使用寄存器的个数。特殊标记“r”标识使用一定范围内的寄存器。
  • 第三个字母为类型码,表示指令用到的额外数据的类型。取值见表3-1。

还有一种特殊的情况是末尾可能会多出另一个字母,如果是字母s表示指令采用静态链接,如果是字母i表示指令应该被内联处理。

表3-1 指令格式标识的类型码

助记符

位大小

说明

b

8

8位有符号立即数

c

16,32

常量池索引

f

16

接口常量(仅对静态链接格式有效)

h

16

有符号立即数(32位或64位数的高值位,低值位为0)

i

32

立即数,有符号整数或32位浮点数

l

64

立即数,有符号整数或64位双精度浮点数

m

16

方法常量(仅对静态链接格式有效)

n

4

4位的立即数

s

16

短整型立即数

t

8,16,32

跳转、分支

x

0

无额外数据

以指令格式标识22x为例:

第一个数字2表示指令有两个16位字组成,第二个数字2表示指令使用到2个寄存器,第三个字母x表示没有使用到额外的数据。

另外,Dalvik指令对语法做了一些说明,它约定如下:

  • 每条指令从操作码开始,后面紧跟参数,参数个数不定,每个参数之间采用逗号分开。
  • 每条指令的参数从指令第一部分开始,op位于低8位,高8位可以是一个8位的参数,也可以是两个4位的参数,还可以为空,如果指令超过16位,则后面部分依次作为参数。
  • 如果参数采用“vX”的方式表示,表明它是一个寄存器,如v0、v1等。这里采用v而不用r是为了避免与基于该虚拟机架构本身的寄存器命名产生冲突,如ARM架构寄存器命名采用r开头。
  • 如果参数采用“#+X”的方式表示,表明它是一个常量数字。
  • 如果参数采用“+X”的方式表示,表明它是一个相对指令的地址偏移。
  • 如果参数采用“kind@X”的方式表示,表明它是一个常量池索引值。其中kind表示常量池类型,它可以是“string”(字符串常量池索引)、“type”(类型常量池索引)、“field”(字段常量池索引)或者“meth”(方法常量池索引)。

以指令“op vAA, string@BBBB”为例:

指令用到了1个寄存器参数vAA,并且还附加了一个字符串常量池索引string@BBBB,其实这条指令格式代表着const-string指令。

在Android 4.0源码Dalvik/docs目录下提供了一份文档instruction-formats.html,里面详细列举了Dalvik指令的所有格式。读者可以通过它了解Dalvik指令更加完整的信息。

注意 在Android 4.1源码Dalvik/docs目录中,instruction-formats.html已经被移除了。

3.2.2 DEX文件反汇编工具

目前DEX可执行文件主流的反汇编工具有BakSmali与Dedexer。两者的反汇编效果都不错,在语法上也有着很多的相似处。下面通过代码对比两者的语法差异,测试代码采用上一节的Hello.java,首先使用dx工具生成Hello.dex文件,然后在命令提示符下输入以下命令使用baksmali.jar反汇编Hello.dex:

java -jar baksmali.jar -o baksmaliout Hello.dex

命令成功执行会在baksmaliout目录下生成Hello.smali文件,使用文本编辑器打开它,foo() 函数代码如下。

# virtual methods
.method public foo(II)I
    .registers 5
    .parameter
    .parameter
    .prologue
    .line 3
    add-int v0, p1, p2
    sub-int v1, p1, p2
    mul-int/2addr v0, v1
    return v0
.end method

执行以下命令使用ddx.jar(Dedexer的jar文件)反汇编Hello.dex:

java -jar ddx.jar -d ddxout Hello.dex

命令成功执行后,会在ddxout目录下生成Hello.ddx文件,使用文本编辑器打开它,foo()函数代码如下。

.method public foo(II)I
.limit registers 5
; this: v2 (LHello;)
; parameter[0] : v3 (I)
; parameter[1] : v4 (I)
.line 3
    add-int  v0,v3,v4
    sub-int  v1,v3,v4
    mul-int/2addr    v0,v1
    return   v0
.end method

两种反汇编代码大体的结构组织是一样的,在方法名、字段类型与代码指令序列上它们保持一致,具体的差异表现在一些语法细节上。对比之下,可以发现如下不同点:

  • 前者使用.registers指令指定函数用到的寄存器数目,后者在.registers指令前加了limit前缀。
  • 前者使用寄存器p0作为this引用,后者使用寄存器v2作为this引用。
  • 前者使用一条.parameter指令指定函数一个参数,后者则使用parameter数组指定参数寄存器。
  • 前者使用.prologue指令指定函数代码起始处,后者却没有。
  • 两者寄存器表示法不同,前者使用p命名法,后者使用v命名法。(具体差异下一节进行讲解)

BakSmali提供反汇编功能的同时,还支持使用Smali工具打包反汇编代码重新生成dex文件,这个功能被广泛应用于apk文件的修改、补丁、破解等场合,因而更加受到开发人员的青睐,本书Dalvik指令的语法将采用Smali语法格式。

3.2.3 了解Dalvik寄存器

Dalvik虚拟机基于寄存器架构,在代码中大量地使用到了寄存器。Dalvik虚拟机是作用于特定架构的CPU上运行的,在设计之初采用了ARM架构,ARM架构的CPU本身集成了多个寄存器,Dalvik将部分寄存器映射到了ARM寄存器上,还有一部分则通过调用栈进行模拟。注意:Dalvik中用到的寄存器都是32位的,支持任何类型,64位类型用2个相邻寄存器表示。

Dalvik虚拟机支持多少个虚拟寄存器呢?通过查看Dalvik指令格式表,可以发现类似“∅∅|op AAAA BBBB”的指令,它的语法为“op vAAAA, vBBBB”,其中每个大写字母代表4位,AAAA、BBBB最大值是2的16次方减1,即65536,寄存器采用v0作起始值,因此,它的取值范围是v0~v65535。

Dalvik虚拟机又是如何虚拟地使用寄存器的呢?这个还得从上面章节讲到的Dalvik虚拟机的调用栈说起,Dalvik虚拟机为每个进程维护一个调用栈,这个调用栈其中一个作用就是用来“虚拟”寄存器,由上一节我们知道,每个函数都在函数头部使用.registers指令指定函数用到的寄存器数目,当虚拟机执行到这个函数时,会根据寄存器的数目分配适当的栈空间,这些空间就是用来存放寄存器实际的值的!虚拟机通过处理字节码,对寄存器进入读与写的操作,其实都是在写栈空间。Android SDK中有一个名为dalvik.bytecode.Opcodes的接口,它定义了一份完整的Dalvik字节码列表。处理这些字节码的函数为一个宏HANDLE_OPCODE(),这份Dalvik字节码列表中每个字节码的处理过程可以在Android源码的dalvik\vm\mterp\c目录中找到,拿OP_MOVE来举例,OP_MOVE.cpp内容如下:

HANDLE_OPCODE($opcode /*vA, vB*/)
    vdst = INST_A(inst);
    vsrc1 = INST_B(inst);
    ILOGV("|move%s v%d,v%d %s(v%d=0x%08x)",
        (INST_INST(inst) == OP_MOVE) ? "" : "-object", vdst, vsrc1,
        kSpacing, vdst, GET_REGISTER(vsrc1));
    SET_REGISTER(vdst, GET_REGISTER(vsrc1));
    FINISH(1);
OP_END

INST_A是用来获取vA寄存器地址的宏,右边的A表示寄存器的“名称”,可以是其他的字母或长度,如INST_AA、INST_B等分别是获取vAA与vB的地址。在OP_MOVE.cpp文件同目录的header.cpp文件中,INST_A与INST_B的声明如下:

#define INST_A(_inst)       (((_inst) >> 8) & 0x0f)
#define INST_B(_inst)       ((_inst) >> 12)

这里的_inst为一个16位的指令,INST_A将_inst右移8位然后与0x0f相与,也就是获取了_inst高8位的低4位作为vdst的值,而INST_B将_inst右移12位,也就是获取_inst的最高4位作为vsrc1的值。

ILOGV用来输出调试信息。

SET_REGISTER用来设置寄存器的值,GET_REGISTER用来获取寄存器的值。另外,操作的寄存器可以是其它的大小与类型,如WIDE、FLOAT,相关的宏函数则是GET_REGISTER_WIDE、GET_REGISTER_FLOAT。在header.cpp文件中,GET_REGISTER与SET_REGISTER的声明如下:

# define GET_REGISTER(_idx)    (fp[(_idx)])
# define SET_REGISTER(_idx, _val)    (fp[(_idx)] = (_val))

fp为ARM栈帧寄存器,在虚拟机运行到某个函数时它指向函数的局部变量区,其中就维护着一份寄存器值的列表,GET_REGISTER 宏以_idx为索引返回一个“寄存器”的值,而SET_REGISTER则是以_idx为索引,设置相应寄存器的值。如果Dalvik虚拟机开启了寄存器数目验证,即#ifdef CHECK_REGISTER_INDICES为真时,在进行寄存器读写操作时,虚拟机会首先判断_idx是否小于curMethod->registersSize,如果条件不成立则说明寄存器超出引用范围,此时虚拟机会通过assert(!"bad reg")抛出异常。

最后,由FINISH宏来完成一条指令的执行。FINISH的功能由ADJUST_PC宏来完成,主要是计算当前指令占用的长度,然后将PC寄存器加上计算出的偏移,这样一条指令执行完成后,PC计数器会指向下一条将要执行的指令。

3.2.4 两种不同的寄存器表示方法——v命名法与p命名法

前面曾多次提到v命名法与p命名法,它们是Dalvik字节码中两种不同的寄存器表示方法。下面我们来看看,它们在表现上有一些什么样的区别。

假设一个函数使用到M个寄存器,并且该函数有N个参数,根据Dalvik虚拟机参数传递方式中的规定:参数使用最后的N个寄存器中,局部变量使用从v0开始的前M-N个寄存器。如前面的小节中,foo() 函数使用到了5个寄存器,2个显式的整型参数,其中foo() 函数是Hello类的非静态方法,函数被调用时会传入一个隐式的Hello对象引用,因此,实际传入的参数数量是3个。根据传参规则,局部变量将使用前2个寄存器,参数会使用后3个寄存器。

v命名法采用以小写字母“v”开头的方式表示函数中用到的局部变量与参数,所有的寄存器命名从v0开始,依次递增。对于foo() 函数,v命名法会用到v0、v1、v2、v3、v4等五个寄存器,v0与v1用来表示函数的局部变量寄存器,v2表示被传入的Hello对象的引用,v3与v4分别表示两个传入的整型参数。

p命名法对函数的局部变量寄存器命名没有影响,它的命名规则是:函数中引入的参数命名从p0开始,依次递增。对于foo() 函数,p命名法会用到v0、v1、p0、p1、p2等五个寄存器,v0与v1同样用来表示函数的局部变量寄存器,p0表示被传入的Hello对象的引用,p1与p2分别表示两个传入的整型参数。

对于有M个寄存器及N个参数的函数foo() 来说,v命名法与p命名法的表现形式如表3-2所示。通过观察可以发现,使用p命名法表示的Dalvik汇编代码,通过寄存器的前缀更容易判断寄存器到底是局部变量寄存器还是参数寄存器,在Dalvik汇编代码较长、使用寄存器较多的情况下,这种优势将更加明显。

表3-2 v命名法与p命名法

v命名法

p命名法

寄存器含义

v0

v0

第一个局部变量寄存器

v1

v1

第二个局部变量寄存器

中间的局部变量寄存器依次递增且名称相同

vM-N

p0

第一个参数寄存器

中间的参数寄存器分别依次递增

vM-1

pN-1

第N个参数寄存器

3.2.5 Dalvik字节码的类型、方法与字段表示方法

Dalvik字节码有着一套自己的类型、方法与字段表示方法,这些方法与Dalvik虚拟机指令集一起组成了一条条的Dalvik汇编代码。

  1. 类型

    Dalvik字节码只有两种类型,基本类型与引用类型。Dalvik使用这两种类型来表示Java语言的全部类型,除了对象与数组属于引用对象外,其他的Java类型都是基本类型。BakSmali严格遵守了DEX文件格式中的类型描述符(DEX文件格式将在第四章进行介绍)定义。类型描述符对照如表3-3所示。

    表3-3 Dalvik字节码类型描述符

    语法含义
    Vvoid,只用于返回值类型
    Zboolean
    Bbyte
    Sshort
    Cchar
    Iint
    Jlong
    Ffloat
    Ddouble
    LJava类类型
    [数组类型

    每个Dalvik寄存器都是32位大小,对于小于或等于32位长度的类型来说,一个寄存器就可以存放该类型的值,而像J、D等64位的类型,它们的值是使用相邻两个寄存器来存储的,如v0与v1、v3与v4等。

    L类型可以表示Java类型中的任何类。这些类在Java代码中以package.name.ObjectName方式引用,到了Dalvik汇编代码中,它们以Lpackage/name/ObjectName;形式表示,注意最后有个分号,L表示后面跟着一个Java类,package/name/表示对象所在的包,ObjectName表示对象的名称,最后的分号表示对象名结束。例如:Ljava/lang/String;相当于java.lang.String。

    [类型可以表示所有基本类型的数组。[后面紧跟基本类型描述符,如[I表示一个整型一维数组,相当于Java中的int[]。多个[在一起时可用来表示多维数组,如[[I表示int[][],[[[I表示int[][][]。注意多维数组的维数最大为255个。

    L与[可以同时使用用来表示对象数组。如[Ljava/lang/String;就表示Java中的字符串数组。

  2. 方法

    方法的表现形式比类名要复杂一些,Dalvik使用方法名、类型参数与返回值来详细描述一个方法。这样做一方面有助于Dalvik虚拟机在运行时从方法表中快速地找到正确的方法,另一方面,Dalvik虚拟机也可以使用它们来做一些静态分析,比如Dalvik字节码的验证与优化。

    方法格式如下:

    Lpackage/name/ObjectName;->MethodName(III)Z

    在这个例子中,Lpackage/name/ObjectName;-应该理解为一个类型,MethodName为具体的方法名, (III)Z是方法的签名部分,其中括号内的III为方法的参数(在此为三个整型参数),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指令结束,根据方法类型的不同,在方法指令开始前可能会用井号“#”加以注释。如“# virtual methods”表示这是一个虚方法,“# direct methods”表示这是一个直接方法。

  3. 字段

    字段与方法很相似,只是字段没有方法签名域中的参数与返回值,取而代之的是字段的类型。同样,Dalvik虚拟机定位字段与字节码静态分析时会用到它。字段的格式如下:

    Lpackage/name/ObjectName;->FieldName:Ljava/lang/String;

    字段由类型(Lpackage/name/ObjectName;)、字段名(FieldName)与字段类型(Ljava/lang/String;)组成。其中字段名与字段类型中间用冒号“:”隔开。

    BakSmali生成的字段代码以.field指令开头,根据字段类型的不同,在字段指令的开始可能会用井号“#”加以注释,如“# instance fields”表示这是一个实例字段,“# static fields”表示这是一个静态字段。

3.3 Dalvik指令集

在Android 4.0源码Dalvik/docs目录下提供了一份指令集文档dalvik-bytecode.html,里面详细列举了Dalvik支持的所有指令,不过该文档在Android 4.1源码中已经去除。

3.3.1 指令特点

Dalvik指令在调用格式上模仿了C语言的调用约定。Dalvik指令的语法与助词符有如下特点:

  • 参数采用从目标(destination)到源(source)的方式。
  • 根据字节码的大小与类型不同,一些字节码添加了名称后缀以消除歧义。
    • 32位常规类型的字节码未添加任何后缀。
    • 64位常规类型的字节码添加 -wide后缀。
    • 特殊类型的字节码根据具体类型添加后缀。它们可以是 -boolean、-byte、-char、-short、-int、-long、-float、-double、-object、-string、-class、-void之一。
  • 根据字节码的布局与选项不同,一些字节码添加了字节码后缀以消除歧义。这些后缀通过在字节码主名称后添加斜杠“/”来分隔开。
  • 在指令集的描述中,宽度值中每个字母表示宽度为4位。

例如这条指令:“move-wide/from16 vAA, vBBBB”

move为基础字节码(base opcode)。标识这是基本操作。

wide为名称后缀(name suffix)。标识指令操作的数据宽度(64位)。

from16为字节码后缀(opcode suffix)。标识源为一个16位的寄存器引用变量。

vAA为目的寄存器。它始终在源的前面,取值范围为v0~v255。

vBBBB为源寄存器。取值范围为v0~v65535。

Dalvik指令集中大多数指令用到了寄存器作为目的操作数或源操作数,其中A/B/C/D/E/F/G/H代表一个4位的数值,可用来表示0~15的数值或v0~v15的寄存器,而AA/BB/CC/DD/EE/FF/GG/HH代表一个8位的数值,可用来表示0~255的数值或v0~v255的寄存器,AAAA/BBBB/CCCC/DDDD/EEEE/FFFF/GGGG/HHHH代表一个8位的数值,可用来表示0~65535的数值或v0~v65535的寄存器。

注意 Android官方指令文档描述寄存器时,对不同取值范围的寄存器以括号说明其大小,如A: destination register (4 bits)、A: destination register (16 bits)。本章后续小节在描述指令时,会采用4位、8位或16位等方式加以说明。请读者注意:Dalvik虚拟机中的每个寄存器都是32位的。描述指令时所说的位数表示的是寄存器数值的取值范围。

3.3.2 空操作指令

空操作指令的助记符为nop。它的值为00,通常nop指令被用来作对齐代码之用,无实际操作。

3.3.3 数据操作指令

数据操作指令为move。move指令的原型为move destination, source或move destination,move指令根据字节码的大小与类型不同,后面会跟上不同的后缀。

“move vA, vB”将vB寄存器的值赋给vA寄存器,源寄存器与目的寄存器都为4位。

“move/from16 vAA, vBBBB”将vBBBB寄存器的值赋给vAA寄存器,源寄存器为16位,目的寄存器为8位。

“move/16 vAAAA, vBBBB”将vBBBB寄存器的值赋给vAAAA寄存器,源寄存器与目的寄存器都为16位。

“move-wide vA, vB”为4位的寄存器对赋值。源寄存器与目的寄存器都为4位。

“move-wide/from16 vAA, vBBBB”与“move-wide/16 vAAAA, vBBBB”的实现与move-wide相同。

“move-object vA, vB”为对象赋值。源寄存器与目的寄存器都为4位。

“move-object/from16 vAA, vBBBB”为对象赋值。源寄存器为8位,目的寄存器为16位。

“move-object/16 vAAAA, vBBBB”为对象赋值。源寄存器与目的寄存器都为16位。

“move-result vAA”将上一个invoke类型指令操作的单字非对象结果赋给vAA寄存器。

“move-result-wide vAA”将上一个invoke类型指令操作的64位非对象结果赋给vAA寄存器。

“move-result-object vAA”将上一个invoke类型指令操作的对象结果赋给vAA寄存器。

“move-exception vAA”保存一个运行时发生的异常到vAA寄存器。这条指令必须是异常发生时的异常处理器的一条指令。否则的话,指令无效。

3.3.4 返回指令

返回指令指的是函数结尾时运行的最后一条指令。它的基础字节码为return,共有以下四条返回指令。

“return-void”表示函数从一个void方法返回。

“return vAA”表示函数返回一个32位非对象类型的值,返回值寄存器为8位的寄存器vAA。

“return-wide vAA”表示函数返回一个64位非对象类型的值。返回值为8位的寄存器对vAA。

“return-object vAA”表示函数返回一个对象类型的值。返回值为8位的寄存器vAA。

3.3.5 数据定义指令

数据定义指令用来定义程序中用到的常量、字符串、类等数据。它的基础字节码为const。

“const/4 vA, #+B”将数值符号扩展为32位后赋给寄存器vA。

“const/16 vAA, #+BBBB”将数值符号扩展为32位后赋给寄存器vAA。

“const vAA, #+BBBBBBBB”将数值赋给寄存器vAA。

“const/high16 vAA, #+BBBB0000”将数值右边零扩展为32位后赋给寄存器vAA。

“const-wide/16 vAA, #+BBBB”将数值符号扩展为64位后赋给寄存器对vAA。

“const-wide/32 vAA, #+BBBBBBBB”将数值符号扩展为64位后赋给寄存器对vAA。

“const-wide vAA, #+BBBBBBBBBBBBBBBB”将数值赋给寄存器对vAA。

“const-wide/high16 vAA, #+BBBB000000000000”将数值右边零扩展为64位后赋给寄存器对vAA。

“const-string vAA, string@BBBB”通过字符串索引构造一个字符串并赋给寄存器vAA。

“const-string/jumbo vAA, string@BBBBBBBB”通过字符串索引(较大)构造一个字符串并赋给寄存器vAA。

“const-class vAA, type@BBBB”通过类型索引获取一个类引用并赋给寄存器vAA。

“const-class/jumbo vAAAA, type@BBBBBBBB”通过给定的类型索引获取一个类引用并赋给寄存器vAAAA。这条指令占用两个字节,值为0x00ff(Android 4.0中新增的指令)。

3.3.6 锁指令

锁指令多用在多线程程序中对同一对象的操作。Dalvik指令集中有两条锁指令。

“monitor-enter vAA”为指定的对象获取锁。

“monitor-exit vAA”释放指定的对象的锁。

3.3.7 实例操作指令

与实例相关的操作包括实例的类型转换、检查及新建等。

“check-cast vAA, type@BBBB”将vAA寄存器中的对象引用转换成指定的类型,如果失败会抛出ClassCastException异常。如果类型B指定的是基本类型,对于非基本类型的A来说,运行时始终会失败。

“instance-of vA, vB, type@CCCC”判断vB寄存器中的对象引用是否可以转换成指定的类型,如果可以vA寄存器赋值为1,否则vA寄存器赋值为0。

“new-instance vAA, type@BBBB”构造一个指定类型对象的新实例,并将对象引用赋值给vAA寄存器,类型符type指定的类型不能是数组类。

“check-cast/jumbo vAAAA, type@BBBBBBBB”指令功能与“check-cast vAA, type@BBBB”相同,只是寄存器值与指令的索引取值范围更大(Android 4.0中新增的指令)。

“instance-of/jumbo vAAAA, vBBBB, type@CCCCCCCC”指令功能与“instance-of vA, vB, type@CCCC”相同,只是寄存器值与指令的索引取值范围更大(Android 4.0中新增的指令)。

“new-instance/jumbo vAAAA, type@BBBBBBBB”指令功能与“new-instance vAA, type@BBBB”相同,只是寄存器值与指令的索引取值范围更大(Android 4.0中新增的指令)。

3.3.8 数组操作指令

数组操作包括获取数组长度、新建数组、数组赋值、数组元素取值与赋值等操作。

“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@BBBB)与大小(vA)的数组并填充数组内容。vA寄存器是隐含使用的,除了指定数组的大小外还指定了参数的个数,vC~vG是使用到的参数寄存器序列。

“filled-new-array/range {vCCCC .. vNNNN}, type@BBBB”指令功能与“filled-new-array {vC, vD, vE, vF, vG}, type@BBBB”相同,只是参数寄存器使用range字节码后缀指定了取值范围,vC是第一个参数寄存器,N = A + C - 1。

“fill-array-data vAA, +BBBBBBBB”用指定的数据来填充数组,vAA寄存器为数组引用,引用必须为基础类型的数组,在指令后面会紧跟一个数据表。

“new-array/jumbo vAAAA, vBBBB, type@CCCCCCCC”指令功能与“new-array vA, vB, type@CCCC”相同,只是寄存器值与指令的索引取值范围更大(Android 4.0中新增的指令)。

“filled-new-array/jumbo{vCCCC ..vNNNN},type@BBBBBBBB”指令功能与“filled-new-array/range {vCCCC .. vNNNN}, type@BBBB”相同,只是索引取值范围更大(Android 4.0中新增的指令)。

“arrayop vAA, vBB, vCC”对vBB寄存器指定的数组元素进入取值与赋值。vCC寄存器指定数组元素索引,vAA寄存器用来存放读取的或需要设置的数组元素的值。读取元素使用aget类指令,元素赋值使用aput类指令,根据数组中存储的类型指令后面会紧跟不同的指令后缀,指令列表有aget、aget-wide、aget-object、aget-boolean、aget-byte、aget-char、aget-short、aput、aput-wide、aput-object、aput-boolean、aput-byte、aput-char、aput-short。

3.3.9 异常指令

Dalvik指令集中有一条指令用来抛出异常。

“throw vAA”抛出vAA寄存器中指定类型的异常。

3.3.10 跳转指令

跳转指令用于从当前地址跳转到指定的偏移处。Dalvik指令集中有三种跳转指令:无条件跳转(goto)、分支跳转(switch)与条件跳转(if)。

“goto +AA”无条件跳转到指定偏移处,偏移量AA不能为0。

“goto/16 +AAAA”无条件跳转到指定偏移处,偏移量AAAA不能为0。

“goto/32 +AAAAAAAA”无条件跳转到指定偏移处。

“packed-switch vAA, +BBBBBBBB”分支跳转指令。vAA寄存器为switch分支中需要判断的值,BBBBBBBB指向一个packed-switch-payload格式的偏移表,表中的值是有规律递增的。

“sparse-switch vAA, +BBBBBBBB”分支跳转指令。vAA寄存器为switch分支中需要判断的值,BBBBBBBB指向一个sparse-switch-payload格式的偏移表,表中的值是无规律的偏移量。关于分支跳转指令类型的代码会在第5章中详细介绍。

“if-test vA, vB, +CCCC”条件跳转指令。比较vA寄存器与vB寄存器的值,如果比较结果满足就跳转到CCCC指定的偏移处。偏移量CCCC不能为0。if-test类型的指令有以下几条:

  • “if-eq”如果vA等于vB则跳转。Java语法表示为“if (vA == vB)”
  • “if-ne”如果vA不等于vB则跳转。Java语法表示为“if (vA != vB)”
  • “if-lt”如果vA小于vB则跳转。Java语法表示为“if (vA < vB)”
  • “if-ge”如果vA大于等于vB则跳转。Java语法表示为“if (vA >= vB)”
  • “if-gt”如果vA大于vB则跳转。Java语法表示为“if (vA > vB)”
  • “if-le”如果vA小于等于vB则跳转。Java语法表示为“if (vA <= vB)”

“if-testz vAA, +BBBB”条件跳转指令。拿vAA寄存器与0比较,如果比较结果满足或值为0时就跳转到BBBB指定的偏移处。偏移量BBBB不能为0。if- testz类型的指令有以下几条:

  • “if-eq”如果vAA为0则跳转。Java语法表示为“if (!vAA)”
  • “if-ne”如果vAA不为0则跳转。Java语法表示为“if (vAA)”
  • “if-lt”如果vAA小于0则跳转。Java语法表示为“if (vAA < 0)”
  • “if-ge”如果vAA大于等于0则跳转。Java语法表示为“if (vAA >= 0)”
  • “if-gt”如果vAA大于0则跳转。Java语法表示为“if (vAA > 0)”
  • “if-le”如果vAA小于等于0则跳转。Java语法表示为“if (vAA <= 0)”

3.3.11 比较指令

比较指令用于对两个寄存器的值(浮点型或长整型)进行比较。它的格式为“cmpkind vAA, vBB, vCC”,其中vBB寄存器与vCC寄存器是需要比较的两个寄存器或两个寄存器对,比较的结果放到vAA寄存器。Dalvik指令集中共有5条比较指令。

“cmpl-float”比较两个单精度浮点数。如果vBB寄存器大于vCC寄存器,则结果为-1,相等则结果为0,小于的话结果为1。

“cmpg-float”比较两个单精度浮点数。如果vBB寄存器大于vCC寄存器,则结果为1,相等则结果为0,小于的话结果为-1。

“cmpl-double”比较两个双精度浮点数。如果vBB寄存器对大于vCC寄存器对,则结果为-1,相等则结果为0,小于的话结果为1。

“cmpg-double”比较两个双精度浮点数。如果vBB寄存器对大于vCC寄存器对,则结果为1,相等则结果为0,小于的话结果为-1。

“cmp-long”比较两个长整型数。如果vBB寄存器大于vCC寄存器,则结果为1,相等则结果为0,小于的话结果为-1。

3.3.12 字段操作指令

字段操作指令用来对对象实例的字段进入读写操作。字段的类型可以是Java中有效的数据类型。对普通字段与静态字段操作有两种指令集,分别是“iinstanceop vA, vB, field@CCCC”与“sstaticop vAA, field@BBBB”。

普通字段指令的指令前缀为i,如对普通字段读操作使用iget指令,写操作使用iput指令;静态字段的指令前缀为s,如对静态字段读操作使用sget指令,写操作使用sput指令。

根据访问的字段类型不同,字段操作指令后面会紧跟字段类型的后缀,如iget-byte指令表示读取实例字段的值类型为字节类型,iput-short指令表示设置实例字段的值类型为短整形。两类指令操作结果都是一样,只是指令前缀与操作的字段类型不同。

普通字段操作指令有:iget、iget-wide、iget-object、iget-boolean、iget-byte、iget-char、iget-short、iput、iput-wide、iput-object、iput-boolean、iput-byte、iput-char、iput-short。

静态字段操作指令有:sget、sget-wide、sget-object、sget-boolean、sget-byte、sget-char、sget-short、sput、sput-wide、sput-object、sput-boolean、sput-byte、sput-char、sput-short。

在Android 4.0系统中,Dalvik指令集中增加了“iinstanceop/jumbo vAAAA, vBBBB, field@CCCCCCCC”与“sstaticop/jumbo vAAAA, field@BBBBBBBB”两类指令,它们与上面介绍的两类指令作用相同,只是在指令中增加了jumbo字节码后缀,且寄存器值与指令的索引取值范围更大。

3.3.13 方法调用指令

方法调用指令负责调用类实例的方法。它的基础指令为invoke,方法调用指令有“invoke-kind {vC, vD, vE, vF, vG}, meth@BBBB”与“invoke-kind/range {vCCCC .. vNNNN}, meth@BBBB”两类,两类指令在作用上并无不同,只是后者在设置参数寄存器时使用了range来指定寄存器的范围。根据方法类型的不同,共有如下五条方法调用指令。

“invoke-virtual”或“invoke-virtual/range”调用实例的虚方法。

“invoke-super”或“invoke-super/range”调用实例的父类方法。

“invoke-direct”或“invoke-direct/range”调用实例的直接方法。

“invoke-static”或“invoke-static/range”调用实例的静态方法。

“invoke-interface”或“invoke-interface/range”调用实例的接口方法。

在Android 4.0系统中,Dalvik指令集中增加了“invoke-kind/jumbo {vCCCC .. vNNNN}, meth@BBBBBBBB”这类指令,它与上面介绍的两类指令作用相同,只是在指令中增加了jumbo字节码后缀,且寄存器值与指令的索引取值范围更大。

方法调用指令的返回值必须使用move-result* 指令来获取。如下面两条指令:

invoke-static {}, Landroid/os/Parcel;->obtain()Landroid/os/Parcel;
move-result-object v0

3.3.14 数据转换指令

数据转换指令用于将一种类型的数值转换成另一种类型。它的格式为“unop vA, vB”,vB寄存器或vB寄存器对存放需要转换的数据,转换后的结果保存在vA寄存器或vA寄存器对中。

“neg-int”对整型数求补。

“not-int”对整型数求反。

“neg-long”对长整型数求补。

“not-long”对长整型数求反。

“neg-float”对单精度浮点型数求补。

“neg-double”对双精度浮点型数求补。

“int-to-long”将整型数转换为长整型。

“int-to-float”将整型数转换为单精度浮点型。

“int-to-double”将整型数转换为双精度浮点型。

“long-to-int”将长整型数转换为整型。

“long-to-float”将长整型数转换为单精度浮点型。

“long-to-double”将长整型数转换为双精度浮点型。

“float-to-int”将单精度浮点型数转换为整型。

“float-to-long”将单精度浮点型数转换为长整型。

“float-to-double”将单精度浮点型数转换为双精度浮点型。

“double-to-int”将双精度浮点型数转换为整型。

“double-to-long”将双精度浮点型数转换为长整型。

“double-to-float”将双精度浮点型数转换为单精度浮点型。

“int-to-byte”将整型转换为字节型。

“int-to-char”将整形转换为字符串。

“int-to-short”将整型转换为短整型。

3.3.15 数据运算指令

数据运算指令包括算术运算指令与逻辑运算指令。算术运算指令主要进行数值间如加、减、乘、除、模、移位等运算,逻辑运算指令主要进行数值间与、或、非、异或等运算。数据运算指令有如下四类(数据运算时可能是在寄存器或寄存器对间进行,下面的指令作用讲解时使用寄存器来描述):

“binop vAA, vBB, vCC”将vBB寄存器与vCC寄存器进行运算,结果保存到vAA寄存器。

“binop/2addr vA, vB”将vA寄存器与vB寄存器进行运算,结果保存到vA寄存器。

“binop/lit16 vA, vB, #+CCCC”将vB寄存器与常量CCCC进行运算,结果保存到vA寄存器。

“binop/lit8 vAA, vBB, #+CC”将vBB寄存器与常量CC进行运算,结果保存到vAA寄存器。

后面3类指令比第1类指令分别多出了2addr、lit16、lit8等指令后缀。四类指令中基础字节码相同的指令执行的运算操作是类似的,第1类指令中,根据数据的类型不同会在基础字节码后面加上数据类型后缀,如-int或-long分别表示操作的数据类型为整型与长整型。第1类指令可归类如下:

“add-type”vBB寄存器与vCC寄存器值进行加法运算(vBB + vCC)。

“sub-type”vBB寄存器与vCC寄存器值进行减法运算(vBB - vCC)。

“mul-type”vBB寄存器与vCC寄存器值进行乘法运算(vBB x vCC)。

“div-type”vBB寄存器与vCC寄存器值进行除法运算(vBB / vCC)。

“rem-type”vBB寄存器与vCC寄存器值进行模运算(vBB % vCC)。

“and-type”vBB寄存器与vCC寄存器值进行与运算(vBB AND vCC)。

“or-type”vBB寄存器与vCC寄存器值进行或运算(vBB OR vCC)。

“xor-type”vBB寄存器与vCC寄存器值进行异或运算(vBB XOR vCC)。

“shl-type”vBB寄存器值(有符号数)左移vCC位(vBB << vCC)。

“shr-type”vBB寄存器值(有符号数)右移vCC位(vBB >> vCC)。

“ushr-type”vBB寄存器值(无符号数)右移vCC位(vBB >> vCC)。

其中基础字节码后面的-type可以是-int、-long、-float、-double。后面3类指令与之类似,此处不再列出。

至此,Dalvik虚拟机支持的所有指令就介绍完了。在Android 4.0系统以前,每个指令的字节码只占用一个字节,范围是0x0~-0x0ff。在Android 4.0系统中,又扩充了一部分指令,这些指令被称为扩展指令,主要是在指令助记符后添加了jumbo后缀,增加了寄存器与常量的取值范围。

3.4 Dalvik指令集练习——写一个Dalvik版的Hello World

本节主要对前面所学的知识进行简单的回顾,加深对Dalvik指令的理解,读者在练习过程中需要多看多写,认真打好Dalvik汇编基础,为后面的分析做准备。

3.4.1 编写smali文件

本节采用smali语法来编写一段Dalvik指令集代码来巩固上面所学到的知识。新建一个文本文件改名为HelloWorld.smali,然后写出HelloWorld类的程序框架如下:

.class public LHelloWorld;  #定义类名
.super Ljava/lang/Object;   #定义父类
.method public static main([Ljava/lang/String;)V   #声明静态main()方法
    .registers 4    #程序中使用v0、v1、v2寄存器与一个参数寄存器
    .parameter      #一个参数
    .prologue       #代码起始指令
    return-void     #返回空
.end method

这是一段HelloWorld的架构代码,定义了一个可编译运行的DEX文件的最小组成部分。下面在.prologue指令下面编写具体代码:

#空指令
nop
nop
nop
nop
#数据定义指令
const/16 v0, 0x8
const/4 v1, 0x5
const/4 v2, 0x3
#数据操作指令
move v1, v2
#数组操作指令
new-array v0, v0, [I
array-length v1, v0
#实例操作指令
new-instance v1, Ljava/lang/StringBuilder;
#方法调用指令
invoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V
#跳转指令
if-nez v0, :cond_0
goto :goto_0
:cond_0
#数据转换指令
int-to-float v2, v2
#数据运算指令
add-float v2, v2, v2
#比较指令
cmpl-float v0, v2, v2
#字段操作指令
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
const-string v1, "Hello World" #构造字符串
#方法调用指令
invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
#返回指令
:goto_0
return-void

代码中使用中了本节中讲到的大多数类型的指令,读者可以参照这段代码自己进行添加或修改。

3.4.2 编译smali文件

编译smali文件使用smali.jar。打开命令提示符窗口,执行以下命令进行编译。

java -jar smali.jar -o classes.dex HelloWorld.smali

如果没有错误,会在当前目录下生成classes.dex文件。使用压缩软件将classes.dex文件压缩成HelloWorld.zip文件。如果编译提示找不到文件,可以将smali.jar与HelloWorld.smali放到同一目录后再进行编译。

3.4.3 测试运行

启动Android运行环境,可以是Android模拟器或真实Android设备,在命令提示符窗口中执行以下命令。

adb push HelloWorld.zip /data/local/
adb shell dalvikvm -cp /data/local/HelloWorld.zip HelloWorld

如图3-7所示,如果没有错误,命令执行后会输出“Hello World”字符串。

{%}

图3-7 adb shell下执行HelloWorld.zip

3.5 本章小结

本章主要介绍了Android的运行环境Dalvik虚拟机,并对比了Dalvik虚拟机与Java虚拟机的差异,随后介绍了Dalvik的指令系统。Dalvik指令是DEX文件最主要的组成部分,读者必须熟练掌握这一部分的内容。另外,整个Dalvik指令集的数目不是很多,语法上面也比较好理解,读者可通过手动编写Dalvik汇编代码来熟悉所有的指令,为第5章的静态分析打好基础。

目录