# 1.7.2 Dalvik 指令集

* [Dalvik 虚拟机](#dalvik-虚拟机)
* [Dalvik 指令集](#dalvik-指令集)
  * [指令格式](#指令格式)
  * [寄存器](#寄存器)
  * [类型、方法和字段](#类型方法和字段)
  * [空操作指令](#空操作指令)
  * [数据操作指令](#数据操作指令)
  * [返回指令](#返回指令)
  * [数据定义指令](#数据定义指令)
  * [锁指令](#锁指令)
  * [实例操作指令](#实例操作指令)
  * [数组操作指令](#数组操作指令)
  * [异常指令](#异常指令)
  * [跳转指令](#跳转指令)
  * [比较指令](#比较指令)
  * [字段操作指令](#字段操作指令)
  * [方法调用指令](#方法调用指令)
  * [数据转换指令](#数据转换指令)
  * [数据运算指令](#数据运算指令)
* [smali 语法](#smali-语法)
  * [循环语句](#循环语句)
  * [switch 语句](#switch-语句)
  * [try-catch 语句](#trycatch-语句)
* [更多资料](#更多资料)

## Dalvik 虚拟机

Android 程序运行在 Dalvik 虚拟机中，它与传统的 Java 虚拟机不同，完全基于寄存器架构，数据通过直接通过寄存器传递，大大提高了效率。Dalvik 虚拟机属于 Android 运行时环境，它与一些核心库共同承担 Android 应用程序的运行工作。Dalvik 虚拟机有自己的指令集，即 smali 代码，下面会详细介绍它们。

## Dalvik 指令集

### 指令格式

Dalvik 指令语法由指令的**位描述**与指令**格式标识**来决定。

位描述约定如下：

* 每 16 位使用空格分隔。
* 每个字母占 4 位，按照顺序从高字节到低字节排列。
* 顺序采用 A\~Z 的单个大写字母作为一个 4 位的操作码，op 表示一个 8 位的操作码。
* ”∅“来表示这字段所有位为0值。

指令格式约定如下：

* 指令格式标识大多由三个字符组成，前两个是数字，最后一个是字母。
* 第一个数字表示指令有多少个 16 位的字组成。
* 第二个数字表示指令最多使用寄存器的个数。
* 第三个字母为类型码，表示指令用到的额外数据的类型。

### 寄存器

Dalvik 寄存器都是 32 位的，如果是 64 位的数据，则使用相邻的两个寄存器来表示。

寄存器有两种命名法：v 命名法和 p 命名法。如果一个函数使用到 M 个寄存器，其中有 N 个参数，那么参数会使用最后的 N 个寄存器，而局部变量使用从 v0 开始的前 M-N 个寄存器。在 v 命名法中，不管寄存器中是参数还是局部变量，都以 v 开头。而 p 命名法中，参数命名从 p0 开始，依次递增，在代码比较复杂的时候，使用 p 命名法可以清楚地区分开参数和局部变量，大多数工具使用的也是 p 命名法。

### 类型、方法和字段

Dalvik 字节码只有基本类型和引用类型两种。除了对象类型和数组类型是引用类型外，其余的都是基本类型：

| 语法 | 含义      |
| -- | ------- |
| V  | void    |
| Z  | boolean |
| B  | byte    |
| S  | short   |
| C  | char    |
| I  | int     |
| J  | long    |
| F  | float   |
| D  | double  |
| L  | 对象类型    |
| \[ | 数组类型    |

* 对象类型格式是 `L<包名>/<类名>;`，如 String 表示为 `Ljava/lang/String;`。
* 数组类型格式是 `[` 加上类型，如 `int[]` 表示为 `[I`，`int[][]` 表示为 `[[I`。

Dalvik 使用方法名、类型参数和返回值来描述一个方法。方法格式如下：

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

例如把下面的 Java 代码转换成 smali：

```
# Java
String method(int, int [][], int, String, Object[])

# smali
.method method(I[[IILjava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
.end method
```

字段格式如下：

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

### 空操作指令

空操作指令的助记符为 `nop`，值为 00，通常用于对齐代码。

### 数据操作指令

数据操作指令为 `move`，原型为 `move destination, source`。

* `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/from16vAA, vBBBB`、`move-wide/16 vAAAA, vBBBB`：与 move-wide 相同
* `move-object vA, vB`：对象赋值，都是 4 位
* `move-object/from16 vAA, vBBBB`：对象赋值，源寄存器 16 位，目的寄存器 8 位
* `move-object/16 vAAAA, vBBBB`：对象赋值，都是 16 位
* `move-result vAA`：将上一个 invoke 类型指令操作的单字非对象结果赋值给 vAA 寄存器
* `move-result-wide vAA`：将上一个 invoke 类型指令操作的双字非对象结果赋值给 vAA 寄存器
* `move-result-object vAA`：将上一个 invoke 类型指令操作的对象结果赋值给 vAA 寄存器
* `move-exception vAA`：保存一个运行时发生的异常到 vAA 寄存器

### 返回指令

基础字节码为 `return`。

* `return-void`：从一个 void 方法返回
* `return vAA`：返回一个 32 位非对象类型的值，返回值寄存器位 8 位的寄存器 vAA
* `return-wide vAA`：返回一个 64 位非对象类型的值，返回值寄存器为 8 位的 vAA
* `return-object vAA`：返回一个对象类型的值，返回值寄存器为 8 位的 vAA

### 数据定义指令

基础字节码为 `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

### 锁指令

用在多线程程序中对同一对象操作。

* `monitor-enter vAA`：为指定的对象获取锁
* `monitor-exit vAA`：释放指定的对象的锁

### 实例操作指令

* `check-cast vAA, type@BBBB`
* `check-cast/jumbo vAAAA, type@BBBBBBBB`：将 vAA 寄存器中的对象引用转换成指定的类型，如果失败会抛出 ClassCastException 异常。如果类型 B 指定的是基本类型，对于非基本类型的 A 来说，运行始终会失败
* `instance-of vA, vB, type@CCCC`
* `instance-of vAAAA, vBBBB, type@CCCCCCCC`：判断 vB 寄存器中的对象引用是否可以转换成指定的类型，如果可以 vA 寄存器赋值为 1，否则 vA 寄存器赋值为 0
* `new-instance vAA, type@BBBB`
* `new-instance vAAAA, type@BBBBBBBB`：构造一个指定类型对象的新实例，并将对象引用赋值给 vAA 寄存器，类型符 type 指定的类型不能是数组类

### 数组操作指令

* `array-length vA, vB`：获取vB寄存器中数组的长度并将值赋给vA寄存器。
* `new-array vA, vB, type@CCCC`
* `new-array/jumbo vAAAA, vBBBB, type@CCCCCCCC`：构造指定类型（type@CCCCCCCC）与大小（vBBBB）的数组，并将值赋给 vAAAA 寄存器
* `filled-new-array {vC, vD, vE, vF, vG}, type@BBBB`：构造指定类型（type@BBBB）和大小（vA）的数组并填充数组内容。vA 寄存器是隐含使用的，处理指定数组的大小外还指定了参数的个数，vC\~vG 是使用的参数寄存器列表。
* `filled-new-array/range {vCCCC .. vNNNN}, type@BBBB`：同上，只是参数寄存器使用 range 字节码后缀指定了取值范围，vC 是第一个参数寄存器，N=A+C-1。
* `fill-array-data vAA, +BBBBBBBB`：用指定的数据来填充数组，vAA 寄存器为数组引用，引用必须为基础类型的数组，在指令后面紧跟一个数据表。
* `arrayop vAA, vBB, vCC`：对 vBB 寄存器指定的数组元素进行取值和赋值。vCC 寄存器指定数组元素索引，vAA 寄存器用来存放读取的或需要设置的数组元素的值。读取元素使用 aget 类指令，元素赋值使用 aput 类指令。

### 异常指令

* `throw vAA`：抛出 vAA 寄存器中指定类型的异常

### 跳转指令

有三种跳转指令：无条件跳转（goto）、分支跳转（switch）和条件跳转（if）。

* `goto +AA`
* `goto/16 +AAAA`
* `goto/32 +AAAAAAAA`：无条件跳转到指定偏移处，不能为 0
* `packed-switch vAA, +BBBBBBBB`：分支跳转指令。vAA 寄存器为 switch 分支中需要判断的值，BBBBBBBB 指向一个 packed-switch-payload 格式的偏移表，表中的值是有规律递增的
* `sparse-switch vAA, +BBBBBBBB`：分支跳转指令。vAA 寄存器为 switch 分支中需要判断的值，BBBBBBBB 指向一个 `sparse-switch-payload` 格式的偏移表，表中的值是无规律的偏移量
* `if-test vA, vB, +CCCC`：条件跳转指令。比较 vA 寄存器与 vB 寄存器的值，如果比较结果满足就跳转到 CCCC 指定的偏移处，CCCC 不能为 0。`if-test` 类型的指令有：
  * `if-eq`：if(vA==vB)
  * `if-ne`：if(vA!=vB)
  * `if-lt`：if(vA\<vB)
  * `if-ge`：if(vA>=vB)
  * `if-gt`：if(vA>vB)
  * `if-le`：if(vA<=vB)
* `if-testz vAA, +BBBB`：条件跳转指令。拿 vAA 寄存器与 0 比较，如果比较结果满足或值为 0 就跳转到 BBBB 指定的偏移处，BBBB 不能为 0。`if-testz` 类型的指令有：
  * `if-eqz`：if(!vAA)
  * `if-nez`：if(vAA)
  * `if-ltz`：if(vAA<0)
  * `if-gez`：if(vAA>=0)
  * `if-gtz`：if(vAA>0)
  * `if-lez`：if(vAA<=0)

### 比较指令

对两个寄存器的值进行比较，格式为 cmpkind vAA, vBB, vCC，其中 vBB 和 vCC 寄存器是需要比较的两个寄存器或两个寄存器对，比较的结果放到 vAA 寄存器。指令集中共有5条比较指令：

* `cmpl-float`
* `cmpl-double`：如果 vBB 寄存器大于 vCC 寄存器，结果为 -1，相等结果为 0，小于结果为 1
* `cmpg-float`
* `cmpg-double`：如果 vBB 寄存器大于 vCC 寄存器，结果为 1，相等结果为 0，小于结果为 -1
* `cmp-long`：如果 vBB 寄存器大于 vCC 寄存器，结果为 1，相等结果为 0，小于结果为 -1

### 字段操作指令

用于对对象实例的字段进行读写操作。对普通字段与静态字段操作有两种指令集，分别是 `iinstanceop vA, vB, field@CCCC` 与 `sstaticop vAA, field@BBBB`。扩展为 `iinstanceop/jumbo vAAAA, vBBBB, field@CCCCCCC` 与 `sstaticop/jumbo vAAAA, field@BBBBBBBB`。

普通字段指令的指令前缀为 `i`，静态字段的指令前缀为 `s`。字段操作指令后紧跟字段类型的后缀。

### 方法调用指令

用于调用类实例的方法，基础指令为 `invoke`，有 `invoke-kind {vC, vD, vE, vF, vG}, meth@BBBB` 和 `invoke-kind/range {vCCCC .. vNNNN}, meth@BBBB` 两类。扩展为 `invoke-kind/jumbo {vCCCC .. vNNNN}, meth@BBBBBBBB` 这类指令。

根据方法类型的不同，共有如下五条方法调用指令：

* `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`：调用实例的接口方法

方法调用的返回值必须使用 `move-result*` 指令来获取，如：

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

### 数据转换指令

格式为 `unop vA, vB`，vB 寄存器或vB寄存器对存放需要转换的数据，转换后结果保存在 vA 寄存器或 vA寄存器对中。

* 求补
  * `neg-int`
  * `neg-long`
  * `neg-float`
  * `neg-double`
* 求反
  * `not-int`
  * `not-long`
* 整型数转换
  * `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`

### 数据运算指令

包括算术运算符与逻辑运算指令。

数据运算指令有如下四类：

* `binop vAA, vBB, vCC`：将 vBB 寄存器与 vCC 寄存器进行运算，结果保存到 vAA 寄存器。以下类似
* `binop/2addr vA, vB`
* `binop/lit16 vA, vB, #+CCCC`
* `binop/lit8 vAA, vBB, #+CC`

第一类指令可归类为：

* `add-type`：vBB + vCC
* `sub-type`：vBB - vCC
* `mul-type`：vBB \* vCC
* `div-type`：vBB / vCC
* `rem-type`：vBB % vCC
* `and-type`：vBB AND vCC
* `or-type`：vBB OR vCC
* `xor-type`：vBB XOR vCC
* `shl-type`：vBB << vCC
* `shr-type`：vBB >> vCC
* `ushr-type`：（无符号数）vBB >> vCC

## smali 语法

类声明：

```
.class <访问权限> [修饰关键字] <类名>
.super <父类名>
.source <源文件名>
```

字段声明：

```
# static fields
.field <访问权限> static [修饰关键字] <字段名>:<字段类型>

# instance fields
.field <访问权限> [修饰关键字] <字段名>:<字段类型>
```

方法声明：

```
# direct methods
.method <访问权限> [修饰关键字] <方法原型>
    [.locals]
    [.param]
    [.prologue]
    [.line]
<代码体>
.end method

# virtual methods
.method <访问权限> [修饰关键字] <方法原型>
    [.locals]
    [.param]
    [.prologue]
    [.line]
<代码体>
.end method
```

需要注意的是，在一些老教程中，会看到 `.parameter`，表示使用的寄存器个数，但在最新的语法中已经不存在了，取而代之的是 `.param`，表示方法参数。

接口声明：

```
# interfaces
.implements <接口名>
```

注释声明：

```
# annotations
.annotation [注释属性] <注释类名>
    [注释字段 = 值]
.end annotation
```

### 循环语句

```
# for
Iterator<对象> <对象名> = <方法返回一个对象列表>;
for(<对象> <对象名>:<对象列表>){
    [处理单个对象的代码体]
}

# while
Iterator<对象> <迭代器> = <方法返回一个迭代器>;
while(<迭代器>.hasNext()){
    <对象> <对象名> = <迭代器>.next();
    [处理单个对象的代码体]
}
```

比如下面的 Java 代码：

```java
public void encrypt(String str) {
    String ans = "";
    for (int i = 0 ; i < str.length(); i++){
        ans += str.charAt(i);
    }
    Log.e("ans:", ans);
}
```

对应下面的 smali：

```
# public void encrypt(String str) {
.method public encrypt(Ljava/lang/String;)V
.locals 4
.parameter p1, "str"    # Ljava/lang/String;
.prologue

# String ans = "";
const-string v0, ""
.local v0, "ans":Ljava/lang/String;

# for (int i  0 ; i < str.length(); i++){
# int i=0 =>v1
const/4 v1, 0x0
.local v1, "i":I
:goto_0     # for_start_place

# str.length()=>v2
invoke-virtual {p1}, Ljava/lang/String;->length()I
move-result v2

# i<str.length()
if-ge v1, v2, :cond_0

# ans += str.charAt(i);
# str.charAt(i) => v2
new-instance v2, Ljava/lang/StringBuilder;
invoke-direct {v2}, Ljava/lang/StringBuilder;-><init>()V
invoke-virtual {v2, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v2

#str.charAt(i) => v3
invoke-virtual {p1, v1}, Ljava/lang/String;->charAt(I)C
move-result v3

# ans += v3 =>v0
invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;->append(C)Ljava/lang/StringBuilder;
move-result-object v2
invoke-virtual {v2}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0

# i++
add-int/lit8 v1, v1, 0x1
goto :goto_0

# Log.e("ans:", ans);
:cond_0
const-string v2, "ans:"
invoke-static {v2, v0}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I
return-void
.end method
```

### switch 语句

```java
public void encrypt(int flag) {
        String ans = null;
        switch (flag){
            case 0:
                ans = "ans is 0";
                break;
            default:
                ans = "noans";
                break;
        }
        Log.v("ans:", ans);
    }
```

对应下面的 smali：

```
# public void encrypt(int flag) {
.method public encrypt(I)V
    .locals 2
    .param p1, "flag"    # I
    .prologue

# String ans = null;
    const/4 v0, 0x0
    .local v0, "ans":Ljava/lang/String;

# switch (flag){
    packed-switch p1, :pswitch_data_0   # pswitch_data_0指定case区域的开头及结尾

# default: ans="noans"
    const-string v0, "noans"

# Log.v("ans:", ans)
    :goto_0
    const-string v1, "ans:"
    invoke-static {v1, v0}, Landroid/util/Log;->v(Ljava/lang/String;Ljava/lang/String;)I
    return-void

# case 0: ans="ans is 0"
    :pswitch_0            # pswitch_<case的值>
    const-string v0, "ans is 0"
    goto :goto_0          # break
    nop
    :pswitch_data_0 #case区域的结束
    .packed-switch 0x0    # 定义case的情况
        :pswitch_0   #case 0
    .end packed-switch
.end method
```

根据 switch 语句的不同，case 也有两种方式：

```
# packed-switch
packed-switch p1, :pswitch_data_0
...
:pswitch_data_0
.packed-switch 0x0
    :pswitch_0
    :pswitch_1

# spase-switch
sparse-switch p1,:sswitch_data_0
...
sswitch_data_0
.sparse-switch
    0xa -> : sswitch_0
    0xb -> : sswitch_1 # 字符会转化成数组
```

### try-catch 语句

```java
public void encrypt(int flag) {
    String ans = null;
    try {
        ans = "ok!";
    } catch (Exception e){
        ans = e.toString();
    }
    Log.d("error", ans);
}
```

对应的下面的 smali：

```
# public void encrypt(int flag) {
.method public encrypt(I)V
    .locals 3
    .param p1, "flag"    # I
    .prologue

# String ans = null;
    const/4 v0, 0x0
    .line 20
    .local v0, "ans":Ljava/lang/String;

# try { ans="ok!"; }
    :try_start_0    # 第一个try开始，
    const-string v0, "ok!"
    :try_end_0      # 第一个try结束(主要是可能有多个try)
    .catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0

# Log.d("error", ans);
    :goto_0
    const-string v2, "error"
    invoke-static {v2, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
    return-void

# catch (Exception e){ans = e.toString();}
    :catch_0        #第一个catch
    move-exception v1
    .local v1, "e":Ljava/lang/Exception;
    invoke-virtual {v1}, Ljava/lang/Exception;->toString()Ljava/lang/String;
    move-result-object v0
    goto :goto_0
.end method
```

## 更多资料

* 《Android软件安全与逆向分析》
* [Dalvik opcodes](http://www.blogjava.net/midea0978/archive/2012/01/04/367847.html)
* [android逆向分析之smali语法](http://lib.csdn.net/article/android/7043)
