«

Android逆向 - Dalvik语言基础

ljierui 发布于 阅读:41 技术杂谈


Dalvik语言基础

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

Dalvik汇编的设计准则

  1. 采用基于寄存器的设计。方法在内存中创建后即拥有固定大小的栈帧,栈帧占用的空间取决于方法中指定的寄存器数目。运行时使用的数据和代码都存储在DEX文件中。
  2. 如果整数与浮点数按位表示,可以使用32位的寄存器来存放。相邻的两个寄存器表示64位,可用于存放大数据类型。寄存器不需要考虑对齐边界。
  3. 如果用来保存对象引用,寄存器必须能容纳引用类型。
  4. 不同的数据类型按位表示。
  5. 在调用约定上,使用N个寄存器来表示N个参数。对wide类型(32位),使用相邻两个寄存器的组合来传递。对实例方法,第1个参数传递的是this指针。

Dalvik指令格式

Dalvik指令组成,指令语法由指令的位描述和指令格式标识决定,约定如下:

  1. 每16位的字用空格分开。
  2. 每个字母表示4位,每个字母按顺序从高字节到低字节排序,每4位之间可能使用竖线 “|” 将不同的内容分开。
  3. 顺序采用英文大写字母A~Z 表示4位的操作码。op表示8位的操作码。
    以指令格式"A|G|op BBBB F|E|D|C" 为例子,两个空格将指令分成了大小均为16位的三部分:
  4. 第1个16位部分是"A|G|op",其高8位由"A"和"G"组成,低字节则由操作码"op"组成
  5. 第2个16位部分由"BBBB"组成,表示一个16位的偏移量
  6. 第3个16位部分由"F"E"D"C" 四个4字节组成,它们分别表示寄存器的参数

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

  1. 指令格式标识大都由三个字符组成。其中,前两个是数字,最后一个是字母
  2. 第1个数字表示指令是由多少个16位的字组成
  3. 第2个数字表示指令最多使用的寄存器的个数。特殊标记r用于标识所使用的寄存器的范围
  4. 第3个字母为类型码,表示指令所使用的额外数据的类型| | | |
    以指令格式标识"22x"为例,第1个数字2表示指令由两个16位字组成,第2个数字2表示指令使用两个寄存器,字母x表示没有使用额外的数据。

Dalvik指令对语法进行了一些说明,约定如下:

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

Dex反编译工具

dexdump : 命令 : dexdump -d Hello.dex
baksmali :命令 : baksmali -o baksmaliout Hello.dex
区别:baksmali 使用的是p命名法; dexdump使用的是v命名法

Dalvik寄存器

Dalvik虚拟机是基于寄存器架构的,支持0-65535个寄存器,初始为v0

寄存器的命名规则

寄存器命名以"v"和"p"两种前缀来命名,两者的区别

"v"命名法:

  1. 用于存储局部变量和方法内部的中间计算结果
  2. 这些寄存器是方法内部使用的,通常由 .locals 指令声明
    const v0, 0x1 // v0 和 v1 是局部变量寄存器,存储了常量和计算结果
    add-int v1, v0, v0

"p"命名法:

  1. 用于存储传递给方法的参数。这些寄存器也可以存储局部变量,但它们的初始作用是接收方法的输入参数
  2. p 寄存器的数量和方法参数的数量有关,由 .param 指令或自动分配决定
    .param p1 // p1 是一个参数寄存器,存储了传递给方法的第一个参数
    invoke-virtual {p1}, Ljava/lang/String;->length()I
    move-result v0

寄存器编号

  1. v 寄存器的编号是从 0 开始的,表示局部变量。
  2. p 寄存器的编号也从 0 开始,但它们是根据方法参数的顺序分配的。
  3. 静态方法:p0 是第一个参数。
  4. 实例方法:p0 通常表示 this 引用,而 p1 开始是实际参数

Dalvik字节码

Dalvik字节码有自己的类型、方法及字段表示方法,这些内容与Dalvik虚拟机指令集一起组成了Dalvik汇编代码。
Dalivk字节码只有两种类型基本类型和引用类型。用这两种类型来表示Java语言的全部类型,除了对象和数组属于引用类型,其他的Java类型都属于基本类型

  1. v:void(只用于返回值类型)
  2. Z:boolean
  3. B:byte
  4. S:short
  5. C:chat
  6. I:int
  7. J:Long
  8. F:float
  9. D:double
  10. L:Java类类型
  11. [:数组类型(多个[表示多维数组)

方法

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

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

解释:"Lpackage/name/ObjectName"应该被理解为一个类型,MethodName为具体的方法名,(III)表示3个整型参数,Z表示boolean类型返回值
baksmali生成的方法代码以.method指令开始,以.end method指令结果,根据方法类型的不同,在方法指令前可能会用"#"来添加注释。例如"# virtual methods"表示这是一个虚方法,"# direct methods"表示这是一个直接方法

字段

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

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

解析:字段由类型(Lpackage/name/ObjectName)、字段名(FieldName)与字段类型(Ljava/lang/String)组成,字段名与字段类型用冒号分开。
baksmail生成的字段代码以.field指令开头,根据字段类型的不同,在字段指令前可能会用"#"来添加注释。"# instance fields"这是实例字段,"# static fields"这是静态字段。

Dalvik指令集

指令类型

Dalvik指令模仿了C语言的调用约定。Dalvik指令的语法与助词符由如下特点。

返回指令

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

  1. return-void: 函数从1个void方法返回
  2. return vAA : 函数返回一个32位非对象类型的值,返回值为8位寄存器vAA
  3. return-wide vAA: 函数返回一个64位非对象类型的值,返回值为8位寄存器对vAA
  4. return-object vAA : 函数返回一个对象类型的值,返回值为8位寄存器vAA

数据定义指令

数据定义指令用于定义程序中常用到的常量、字符串、类等数据,它的基础字节码为const,例举如下:

  1. const/4 vA,#+B : 用于将数值符号扩展为32位后赋予寄存器vA
  2. const/16 vAA, #+BBBB : 用于将数值符号扩展为32位后赋予寄存器vAA

锁指令

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

  1. monitor-enter vAA指令用于为指定对象获取锁。
  2. monitor-exit vAA指令用于释放指定对象的锁。

实例操作指令

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

  1. check-cast vAA,type@BBBB :用于将vAA寄存器中的对象引用转换成指定的类型,如果失败会抛出ClassCastException异常。
  2. instance-of vA,vB,type@CCCC : 用于判断vB寄存器中的对象引用是否可以转换成指定的类型,如果可以就为vA寄存器赋值1,否则为0。
  3. new-instance vAA,type@BBB :构造一个指定类型对象的新实例,并将对象引用赋值给vAA寄存器。
  4. check-cast/jumbo vAAAA,type@BBBBBB : 与check-cast vAA,type@BBB 指令相同,只是前者的寄存器与指令索引的取值范围更大。
  5. instance-of/jumbo vAAAA,vBBBB,type@CCCCCCCC :与instance-of vA,vB,type@CCCC 相同,前者的寄存器与索引的取值范围更大。
  6. new-instance/jumbo vAAAA,type@BBBBBBBB : 与new-instance vAA,type@BBB相同,前者的寄存器与索引的取值范围更大。

数组操作指令

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

  1. array-length vA,vB : 获取给定vB寄存器中数组的长度,并将值赋予vA寄存器。数组产犊值就是数组中条目的个数
  2. new-array vA,vB type@CCCC :构造指定类型(type@CCCC)和大小(vB)的数组,并将值赋予vA寄存器。
  3. filled-new-array{vC,vD,vE,vF,vG},type@BBBB :构造指定类型(type@BBBB) 和大小(vA)的数组并填充数组内容。vA寄存器时隐含使用的,除了指定数组的大小,还指定了参数的个数。
  4. fill-array-data vAA,+BBBBBBBB : 用指定的数据来填充数组,vAA寄存器为数组引用,在指令后面会紧跟一个数据表。
  5. new-array/jumbo vAAAA,vBBBB,type@CCCCCCCC : 与new-array vA,vB,type@CCCC指令相同,只是寄存器与指令索引的取值范围更大。
  6. filled-new-aray/jumbo{vCCCC ... vNNNNN},type@BBBBBB : 与filled-new-array/range{} 相同,只是前者的指令索引的取值范围。
    7.arrayop vAA,vBB,vCC : 对vBB寄存器指定的数组元素进行取值与赋值。

异常指令

Dalvik指令集中有一个用于抛出异常的指令, throw vAA:抛出vAA寄存器中指定类型的异常

跳转指令

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

  1. goto +AA : 用于无条件跳转到指定偏移处,偏移量AA不能为0.
  2. goto/16 +AAAA : 用于无条件跳转到指定偏移处,偏移量AAAA不能为0.
  3. goto/32 +AAAAAAAA : 用于无条件跳转到指定偏移处.
  4. packed-switch vAA, +BBBBBBBB : 分支跳转指令,vAA寄存器位switch分支中需要判断的值,BBBBBBBB指向一个packed -switchpayload格式的偏移表
  5. if-test vA,vB,+CCCC : 用于比较vA寄存器与vB寄存器的值
  6. if-eq : 如果vA等于vB则跳转。
  7. if-ne : 如果vA不等于vB则跳转。
  8. if-lt : 如果vA小于vB则跳转
  9. if-ge :如果vA大于等于vB则跳转
  10. if-gt : 如果vA大于vB则跳转
  11. if-le :如果vA小于等于vB则跳转

比较指令

比较指令用于对两个寄存器的值(浮点型或长整型)进行比较,格式为cmpkind vAA,vBB,vCC,其中vBB与vCC是需要比较的两个寄存器或两个寄存器对,比较的结果将被放到vAA寄存器。

  1. cmpl-float : 用于比较两个单精度浮点数
  2. cmpg-float : 用于比较两个单精度浮点数
  3. cmpl-double : 用于比较两个双精度浮点数
  4. cmpg-double :用于比较两个双精度浮点数
  5. cmp-long : 比较两个长整型数

字段操作指令

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

方法调用指令

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

  1. invoke-virtual 或 invoke-virtual/range : 调用实例的虚方法
  2. invoke-super 或 invoke-super/range : 调用实例的父类方法
  3. invoke-direct 或 invoke-direct/range : 调用实例的直接方法
  4. invoke-static 或 invoke-static/range : 调用实例的静态方法
  5. invoke-interface 或 invoke-interface/range :调用实例的接口方法

数据转换指令

数据转换指令用于将一种类型的数值转换成另一个类型的数值,格式为unop vA,vB。在vB寄存器或vB寄存器对中存放了需要转换的数据。转换结果保存在vA寄存器或vA寄存器对中。数据转换指令列举如下

  1. neg-int : 用于对整型数求补
  2. not-int :用于对整数型求反
  3. neg-long : 用于对长整型数求补
  4. not-long : 用于对长整型数求反
  5. neg-float : 用于对单精度浮点型数求补
  6. neg-double : 用于对双精度浮点型数求补
  7. int-to-long : 将整型数转换为长整型数
  8. int-to-float:将整型数转换为单精度浮点型数
  9. int-to-double:将整型数转换为双精度浮点型数
  10. long-to-int:将长整型数转换为整型数

数据运算指令

数据运算指令包括算术运算指令与逻辑运算指令。

  1. add-int v0, v1, v2:v0 = v1 + v2(整数加法)。
  2. add-int/2addr v0, v1:v0 += v1(将结果存储在同一寄存器中)。
  3. sub-int v0, v1, v2:v0 = v1 - v2(整数减法)。
  4. sub-int/2addr v0, v1:v0 -= v1。
  5. mul-int v0, v1, v2:v0 = v1 * v2(整数乘法)。
  6. mul-int/2addr v0, v1:v0 *= v1。
  7. div-int v0, v1, v2:v0 = v1 / v2(整数除法)。
  8. div-int/2addr v0, v1:v0 /= v1。
  9. rem-int v0, v1, v2:v0 = v1 % v2(整数求余)。
  10. rem-int/2addr v0, v1:v0 %= v1。
  11. add-long v0, v1, v2:v0 = v1 + v2(长整型加法)。
  12. add-long/2addr v0, v1:v0 += v1。
  13. sub-long v0, v1, v2:v0 = v1 - v2(长整型减法)。
  14. sub-long/2addr v0, v1:v0 -= v1。
  15. mul-long v0, v1, v2:v0 = v1 * v2(长整型乘法)。
  16. mul-long/2addr v0, v1:v0 *= v1。
  17. div-long v0, v1, v2:v0 = v1 / v2(长整型除法)。
  18. div-long/2addr v0, v1:v0 /= v1。
  19. rem-long v0, v1, v2:v0 = v1 % v2(长整型求余)。
  20. rem-long/2addr v0, v1:v0 %= v1。
  21. add-float v0, v1, v2:v0 = v1 + v2(浮点数加法)。
  22. add-float/2addr v0, v1:v0 += v1。
  23. sub-float v0, v1, v2:v0 = v1 - v2(浮点数减法)。
  24. sub-float/2addr v0, v1:v0 -= v1。
  25. mul-float v0, v1, v2:v0 = v1 * v2(浮点数乘法)。
  26. mul-float/2addr v0, v1:v0 *= v1。
  27. div-float v0, v1, v2:v0 = v1 / v2(浮点数除法)。
  28. div-float/2addr v0, v1:v0 /= v1。
  29. rem-float v0, v1, v2:v0 = v1 % v2(浮点数求余)。
  30. rem-float/2addr v0, v1:v0 %= v1。
  31. add-double v0, v1, v2:v0 = v1 + v2(双精度加法)。
  32. add-double/2addr v0, v1:v0 += v1。
  33. sub-double v0, v1, v2:v0 = v1 - v2(双精度减法)。
  34. sub-double/2addr v0, v1:v0 -= v1。
  35. mul-double v0, v1, v2:v0 = v1 * v2(双精度乘法)。
  36. mul-double/2addr v0, v1:v0 *= v1。
  37. div-double v0, v1, v2:v0 = v1 / v2(双精度除法)。
  38. div-double/2addr v0, v1:v0 /= v1。
  39. rem-double v0, v1, v2:v0 = v1 % v2(双精度求余)。
  40. rem-double/2addr v0, v1:v0 %= v1。
  41. and-int v0, v1, v2:v0 = v1 & v2。
  42. and-int/2addr v0, v1:v0 &= v1。
  43. and-long v0, v1, v2:v0 = v1 & v2(长整型)。
  44. or-int v0, v1, v2:v0 = v1 | v2。
  45. or-int/2addr v0, v1:v0 |= v1。
  46. or-long v0, v1, v2:v0 = v1 | v2(长整型)。
  47. xor-int v0, v1, v2:v0 = v1 ^ v2。
  48. xor-int/2addr v0, v1:v0 ^= v1。
  49. xor-long v0, v1, v2:v0 = v1 ^ v2(长整型)。
  50. shl-int v0, v1, v2:v0 = v1 << v2(整数左移)。
  51. shl-int/2addr v0, v1:v0 <<= v1。
  52. shr-int v0, v1, v2:v0 = v1 >> v2(整数算术右移)。
  53. shr-int/2addr v0, v1:v0 >>= v1。
  54. ushr-int v0, v1, v2:v0 = v1 >>> v2(整数逻辑右移)。
  55. ushr-int/2addr v0, v1:v0 >>>= v1。

Dalvik指令练习

实现输出HelloWorld

.class public LHelloWorld; #定义类名
.super Ljava/lang/Object; #定义父类
.method public static main([Ljava/lang/String;)V #声明静态main()方法
    .registers 4 #程序使用v0,v1,v2寄存器和一个参数寄存器
    .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 #返回空值
.end method

Android逆向