进入Android Dalvik虚拟机之Dalvik汇编语言基础

原创
2015/01/09 00:17
阅读数 5K

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

1. Dalvik指令格式

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

  • 每16位的字采用空格分隔开来

  • 每个字母表示4位,每个字母按顺序从高字节开始,排列到低字节。每4位之间可能使有竖线“|”来表示不同的内容

  • 顺序采用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”标识使用一定范围内的寄存器

  • 第三个字母为类型码,表示指令用到的额外数据的类型。取值见下表。

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

助记符                               
位大小                                      
说明
b
8
8位有符号立即数
c
16,32
常量池索引
f
16
接口常量(仅对静态链接格式有效)
h
16
有符号立即数(32位或64位数的高值位,低值位为0)
i
32
立即数,有符号整数或32位浮点数
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 指令。

2. DEX文件反汇编工具

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

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

命令成功执行会在baksmaliout目录下生成Hello.smali文件,使用文本编辑器打开它:

# 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文件的修改,补丁,破解等场合,因而更加受到开发人员的青睐。本系列blog默认都将采用Smali语法格式。

3. 了解Dalvik寄存器

Dalvik虚拟机基于寄存器架构,在代码中大量地使用到了寄存器。Dalvik虚拟机是作用于特定架构的CPU上运行的,在设计之初采用了ARM架构,ARM架构的CPU本身集成了多个寄存器,Dalvik将部分寄存器映射到了ARM寄存器上,还有一部分则通过调用栈进行模拟。注意:Dalvik中用到的寄存器都是32位的,支持任何类型,64位类型用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命名法的表现形式如下表所示。通过观察可以发现,使用p命名法表示的Dalvik汇编代码,通过寄存器的前缀更容易判断寄存器到底是局部变量寄存器还是参数寄存器,在Dalvik汇编代码较长,使用寄存器较多的情况下,这种优势将更加明显。表:v命名法与p命名法:

v命名法                                          
p命名法                                  
寄存器含义
v0
v0
第一个局部变量寄存器
v1
v1
第二个局部变量寄存器
...
...
中间的局部变量寄存器依次递增
vM-N
p0
第一个参数寄存器
...
...
中间的参数寄存器分别依次递增
vM-1
pN-1
第N个参数寄存器

5. Dalvik字节码的类型,方法与字段表示方法

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

5.1. 类型

Dalvik字节码只有两种类型基本类型引用类型。Dalvik使用这两种类型来表示Java语言的全部类型,除了对象与数组属于引用对象外,其他的Java类型都是基本类型。BakSmali严格遵守了DEX文件格式中的类型描述符定义。类型描述符对照如下表:

语法                                    
含义
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位的类型,它们的值是使用相邻两个寄存器来存储的,如 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中的字符串数组。

5.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”表示这是一个直接方法。

5.3. 字段

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

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

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

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

展开阅读全文
打赏
0
6 收藏
分享
加载中
更多评论
打赏
0 评论
6 收藏
0
分享
返回顶部
顶部