前言
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
“的形式表示,说明它是一个寄存器,例如v0
、v1
等。这里用“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进行反汇编:
1 | $ dexdump -d Hello.dex |
使用baksmali进行反汇编:
1 | ... |
这两种反汇编代码的结构大致相同,方法名、字段类型和代码指令序列一致。差异仅仅在于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使用方法名、类型参数与返回值来描述一个方法。格式如下:
1 | Lpackage/name/ObjectName;->MethodName(III)Z |
- Lpackage/name/ObjectName;:类型
- MethodName:方法名
- (III)Z:方法的签名部分:
- III:方法的参数
- Z:方法的返回类型
来看一个复杂点的例子:
1 | method(I[[IILjava/lang/String;[Ljava/lang/Object;)Ljava/lang/String |
转换成 Java 代码后如下:
1 | String method(int, int[][], int, String, Object[]) |
经过 baksmali 生成的方法代码以 .method
指令开始,以 .end method
指令结束,根据方法类型的不同,在方法指令前可能会用 ‘#’ 来添加注释。例如,”# virtual methods
“ 表示这是一个虚方法, “# direct methods
“ 表示这是一个直接方法。
字段
字段与方法相似,只是字段没有方法签名域中的参数和返回值,取而代之的是字段的类型。其格式如下:
1 | Lpackage/name/ObjectName;->FieldName:Ljava/lang/String; |
- Lpackage/name/ObjectName;:类型
- FieldName:字段名
- Ljava/lang/String;:字段类型
baksmali生成的字段代码以 .field 指令开头,表现形式与方法类似,例如,”# instance fields
“ 表示这是一个实例字段, “# static fields
“ 表示这是一个静态字段。
参考资料
参考书籍:《Android软件安全权威指南》—— 丰生强
参考链接:
- https://source.android.com/devices/tech/dalvik/instruction-formats (官方文档-Dalvik可执行指令格式)
- https://source.android.com/devices/tech/dalvik/dalvik-bytecode#instructions (官方文档-Dalvik字节码)
- https://source.android.com/devices/tech/dalvik/dex-format (官方文档-Dalvik可执行文件格式)
- https://blog.csdn.net/p312011150/article/details/80501724 (CSDN-Android Dex文件格式II)