avatar

Catalog
Dalvik语言基础

前言

Dalvik虚拟机有专门的指令集及专门的指令格式(Dalvik Executable Format)和调用规范。由Dalvik指令集组成的代码称为Dalvik汇编代码,由这种代码表示的语言称为Dalvik汇编语言。

Dalvik汇编语言拥有专门的机器模型和类似于C语言的调用约定,并有一套完整的设计准则,具体设计准则可以参考官方文档

Dalvik可执行指令格式

在学习Dalvik可执行指令格式之前,先要对格式有个大致的了解,这里截取了格式表的一部分。

根据格式表可以发现,Dalvik可执行指令的格式由两个因素决定,指令的位描述(布局)指令格式标识(符)

指令的位描述

位描述约定如下:

  • 由一个或多个空格分隔的”单词“组成,每个单词描述一个16位代码单元。
  • 每个字母表示4位,按照从高位到低位的顺序进行排列,并使用”|“分隔以便于用户分辨。
  • A~Z表示格式中的字段,这些字段随后由语法列做进一步定义
  • ”op“表示8位的操作码。
  • “Ø”表示所有在指示位置的位必须为零

例如,”B|A|op CCCC“格式表示其包含两个16位代码单元。第一个指令字的高8位是两个4位值,低8位是操作码;第二个指令字是一个16位的值。

指令格式标识

指令格式标识,又称为格式ID,位于格式表的第二列,用于在其它文档和代码中识别该格式。

大多数格式ID包含三个字符,前两个的十进制数,最后一个是字母:

  • 第1个十进制数表示指令是由多少个16位的字组成的。

  • 第2个十进制数表示指令所含寄存器的数量上限(某些格式使用的寄存器数量是可变的),特殊标记“r”用于标识所使用的寄存器的范围。

  • 第3个字符为类型码,表示指令所使用的额外数据的类型,参考下表。

    助记符 位数 含义
    b 8 8位有符号立即数
    c 16、32 常量池索引
    f 16 接口常量(仅对静态链接格式有效)
    h 16 有符号立即数 hat(32位或64位值的高阶位,低阶位全为0)
    i 32 有符号立即数(整型)或32位浮点数
    l 64 有符号立即数(长整型)或64位双精度浮点数
    m 16 方法常量(仅对静态链接格式有效)
    n 4 有符号立即数(半字节)
    s 16 有符号立即数(短整型)
    t 8、16、32 跳转,分支
    x 0 无额外数据

一种特殊的情况是指令的末尾多出一个字母。如果是字母s,表示指令采用静态链接;如果是字母i,表示指采用内联链接。如果是其它格式(例如“20bc”),表示包含两个数据块。

以指令格式标识“22x”为例,第1个数字2表示指令由两个16位字组成,第2个数字2表示指令使用两个寄存器,字母x表示没有使用额外的数据。

Dalvik指令语法

格式表的第三列指出了指令中所使用的人类可识别的语法。约定如下:

  • 每条指令以命名的操作码开始,后面可选择使用一个或多个参数,并且参数之间用逗号分隔
  • 如果一个参数对应第一列(位描述)中的一个字段,相应字段的字母将出现在语法中,每个字母代表字段中的四位。
  • 如果参数采用vX的形式表示,说明它是一个寄存器,例如 v0v1 等。这里用“v”而不用“r”的目的是避免与基于该虚拟机架构本身的寄存器产生命名冲突(例如,ARM架构的寄存器名称以”r“开头)
  • 如果参数采用#+X的形式表示,说明它是一个常量
  • 如果参数采用+X的形式表示,说明它是一个相对指令的地址偏移量
  • 如果参数所采用的形式为kind@X,说明它是一个常量池索引值。其中“kind”表示所引用的常量池的种类,可以是“string”(字符串池索引)、“type”(类型池索引)、“field”(字段池索引)、“meth”(方法池索引)和“site”(调用点索引)。
  • 如果格式值并非明确地包含在语法中,而是选择使用某种变体,则每个变体都以“[X=N]”(例如:“[A=2]”)为前缀来表示对应关系。

以指令 op vAA, string@BBBB 为例,该指令使用了一个寄存器参数 vAA, 附加了一个字符串常量池索引值 string@BBBB

DEX反汇编工具

主流的DEX文件反汇编工具有Android官方的dexdump和第三方的baksmali,两者在语法上略有差异。以前一篇代码中的foo()函数进行分析。

使用dexdump进行反汇编

smali
1
2
3
4
5
6
7
8
$ dexdump -d Hello.dex
...
|[000198] 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
...

使用baksmali进行反汇编

smali
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
# 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
...

这两种反汇编代码的结构大致相同,方法名、字段类型和代码指令序列一致。差异仅仅在于dexdump使用的都是“v”开头的寄存器,baksmali同时使用“v”和“p”开头的寄存器。baksmali使用的是p命名法,dexdump使用的是v命名法。其中baksmali要更为主流一些。

Dalvik寄存器

Dalvik虚拟机是基于寄存器架构的,其代码中使用了大量的寄存器。Dalvik虚拟机运行在ARM架构的CPU上。ARM架构的CPU本身集成了多个寄存器,Dalvik将部分寄存器映射到了ARM寄存器上还有一部分通过调用栈进行模拟。(映射到ARM寄存器比较好理解,但是调用栈模拟寄存器的过程较为复杂,这部分等到一周目后再作讨论)

Dalvik使用的寄存器都是32位的,支持所有类型。对于64位类型,可以用两个相邻的寄存器来表示。

Dalvik虚拟机支持65536个寄存器。根据Dalvik指令格式表,可以发现形如 “ØØ|op AAAA BBBB” 的指令,它的语法格式为 “op vAAAA, vBBBB”,其中每个字母代表4位。AAAA或BBBB的最大值是2的16次方-1,即65535。因此DVM寄存器的范围是v0~v65535

寄存器命名法

前面提到了,相比使用”v“命名法的dexdump,使用”p“命名法的baksmali反汇编工具更为主流;下面来看看这两类命名法有哪些区别。

首先来看v命名法,寄存器命名从 v0 开始递增。对于foo()函数,v命名法使用 v0, v1, v2, v3, v4 共5个寄存器, v0 与 v1 表示函数的局部变量寄存器,v2 用于表示被传入的Hello对象引用,v3 与 v4 分别用于表示两个传入的整型参数。

再看p命名法,对于foo()函数,p命名法使用 v0, v1, p0, p1, p2 共5个寄存器,v0 与 v1 同样用于表示函数的局部变量寄存器;p0用于表示被传入的 Hello 对象引用,p1 和 p2 分别用于表示两个传入的整型参数。

通过比较可以发现,在使用寄存器较多的情况下,p命名法更容易判断到底是局部变量寄存器还是参数寄存器,因而更为主流。对于有M个寄存器和N个参数的函数来说,可以将规律总结为下表:

v命名法 p命名法 寄存器含义
v0 v0 第1个局部变量寄存器
v1 v1 第2个局部变量寄存器
…… …… 依次递增,且两者相同
vM-N p0 第1个参数寄存器
…… …… 依次递增,两者不同
vM-1 pN-1 第N个参数寄存器

Dalvik字节码

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

类型

Dalvik 字节码只有两种类型,分别是基本类型和引用类型。Dalvik 使用这两种类型来表示 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汇编代码 Java代码
Lpackage/name/ObjectName; package.name.ObjectName
Ljava/lang/String; java.lang.String
[I int[]
[[ int[] [](最大维数为255)
[Ljava/lang/String; String[]

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

方法

方法的表现形式要比类型复杂一些。Dalvik使用方法名、类型参数与返回值来描述一个方法。格式如下:

smali
1
Lpackage/name/ObjectName;->MethodName(III)Z
  • Lpackage/name/ObjectName;:类型
  • MethodName:方法名
  • (III)Z:方法的签名部分:
    • III:方法的参数
    • Z:方法的返回类型

来看一个复杂点的例子:

smali
1
method(I[[IILjava/lang/String;[Ljava/lang/Object;)Ljava/lang/String

转换成 Java 代码后如下:

java
1
String method(int, int[][], int, String, Object[])

经过 baksmali 生成的方法代码以 .method 指令开始,以 .end method 指令结束,根据方法类型的不同,在方法指令前可能会用 ‘#’ 来添加注释。例如,”# virtual methods“ 表示这是一个虚方法, “# direct methods“ 表示这是一个直接方法。

字段

字段与方法相似,只是字段没有方法签名域中的参数和返回值取而代之的是字段的类型。其格式如下:

smali
1
Lpackage/name/ObjectName;->FieldName:Ljava/lang/String;
  • Lpackage/name/ObjectName;:类型
  • FieldName:字段名
  • Ljava/lang/String;:字段类型

baksmali生成的字段代码以 .field 指令开头,表现形式与方法类似,例如,”# instance fields“ 表示这是一个实例字段, “# static fields“ 表示这是一个静态字段。

参考资料

参考书籍:《Android软件安全权威指南》—— 丰生强

参考链接:

Author: cataLoc
Link: http://cataloc.gitee.io/blog/2020/06/22/Dalvik%E8%AF%AD%E8%A8%80%E5%9F%BA%E7%A1%80/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶