Android逆向 - Dalvik语言基础
Dalvik语言基础
Dalvik虚拟机有专门的指令集及专门的指令格式和调用规范。由Dalvik指令集组成的代码称为Dalvik汇编代码,由这种代码组成的语言称为Dalvik汇编语言。
Dalvik汇编的设计准则
- 采用基于寄存器的设计。方法在内存中创建后即拥有固定大小的栈帧,栈帧占用的空间取决于方法中指定的寄存器数目。运行时使用的数据和代码都存储在DEX文件中。
- 如果整数与浮点数按位表示,可以使用32位的寄存器来存放。相邻的两个寄存器表示64位,可用于存放大数据类型。寄存器不需要考虑对齐边界。
- 如果用来保存对象引用,寄存器必须能容纳引用类型。
- 不同的数据类型按位表示。
- 在调用约定上,使用N个寄存器来表示N个参数。对wide类型(32位),使用相邻两个寄存器的组合来传递。对实例方法,第1个参数传递的是this指针。
Dalvik指令格式
Dalvik指令组成,指令语法由指令的位描述和指令格式标识决定,约定如下:
- 每16位的字用空格分开。
- 每个字母表示4位,每个字母按顺序从高字节到低字节排序,每4位之间可能使用竖线 “|” 将不同的内容分开。
- 顺序采用英文大写字母A~Z 表示4位的操作码。op表示8位的操作码。
以指令格式"A|G|op BBBB F|E|D|C" 为例子,两个空格将指令分成了大小均为16位的三部分: - 第1个16位部分是"A|G|op",其高8位由"A"和"G"组成,低字节则由操作码"op"组成
- 第2个16位部分由"BBBB"组成,表示一个16位的偏移量
- 第3个16位部分由"F"E"D"C" 四个4字节组成,它们分别表示寄存器的参数
单独使用位标识是无法确定一条指令的,必须通过指令格式标识来指定指令的格式编码,约定如下:
- 指令格式标识大都由三个字符组成。其中,前两个是数字,最后一个是字母
- 第1个数字表示指令是由多少个16位的字组成
- 第2个数字表示指令最多使用的寄存器的个数。特殊标记r用于标识所使用的寄存器的范围
- 第3个字母为类型码,表示指令所使用的额外数据的类型| | | |
以指令格式标识"22x"为例,第1个数字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" 为例,该指令使用了一个寄存器参数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"命名法:
- 用于存储局部变量和方法内部的中间计算结果
- 这些寄存器是方法内部使用的,通常由 .locals 指令声明
const v0, 0x1 // v0 和 v1 是局部变量寄存器,存储了常量和计算结果 add-int v1, v0, v0
"p"命名法:
- 用于存储传递给方法的参数。这些寄存器也可以存储局部变量,但它们的初始作用是接收方法的输入参数
- p 寄存器的数量和方法参数的数量有关,由 .param 指令或自动分配决定
.param p1 // p1 是一个参数寄存器,存储了传递给方法的第一个参数 invoke-virtual {p1}, Ljava/lang/String;->length()I move-result v0
寄存器编号
- v 寄存器的编号是从 0 开始的,表示局部变量。
- p 寄存器的编号也从 0 开始,但它们是根据方法参数的顺序分配的。
- 静态方法:p0 是第一个参数。
- 实例方法:p0 通常表示 this 引用,而 p1 开始是实际参数
Dalvik字节码
Dalvik字节码有自己的类型、方法及字段表示方法,这些内容与Dalvik虚拟机指令集一起组成了Dalvik汇编代码。
Dalivk字节码只有两种类型基本类型和引用类型。用这两种类型来表示Java语言的全部类型,除了对象和数组属于引用类型,其他的Java类型都属于基本类型
- v:void(只用于返回值类型)
- Z:boolean
- B:byte
- S:short
- C:chat
- I:int
- J:Long
- F:float
- D:double
- L:Java类类型
- [:数组类型(多个[表示多维数组)
方法
方法的表现比类型复杂一些。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指令的语法与助词符由如下特点。
- 采用从目标到源的方式
- 根据字节码大小与类型的不同,为一些字节码添加了名称后缀以消除歧义。
- 32位常规类型的字节码未添加任何后缀
- 64位常规类型的字节码添加-wide后缀
- 对特殊类型的字节码,根据具体类型添加后缀,如-boolean,-byte,-char等。
- 根据字节码布局与选项的不同,为一些字节码添加了字节码后缀以消除歧义,后缀通过在字节码主名称后添加斜杠分割。
- 在指令集的描述中,宽度值中的每个字母都表示4位的宽度。
- 以 "move-wide/from16 vAA,vBBBB" 为例子,move是基础字节码,表示一个基本操作;-wide为名称后缀,表示操作的数据宽度;from16为字节码后缀,表示源为一个16位寄存器引用变量;vAA为目的寄存器,它始终在源的前面,取值范围v0-v255;vBBBB为源寄存器,取值范围为v0-v65535
- A~H 代表4位数值,表示0~15; AA~HH代表8位的数值,代表0~255;AAAA~HHHH代表16位数值,0~65535
空操作指令
空操作指令的助记符为
nop
,它的值为00。nop指令通常用于代码对齐,不进行实际操作数据操作指令
数据操作指令为
move
,根据字节码大小与类型不同有不同后缀:- 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/from16 vAA,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: 将上一个invo ke类型指令操作的单字非对象结果赋予vAA 寄存器。
- move-result-wide vAA: 将上一个invo ke类型指令操作的双字非对象结果赋予vAA寄存器。
- move-result-object vAA:将上一个invo ke类型指令操作的对象结果赋予vAA寄存器。
- move-exception vAA : 将一个在运行时发生的异常保存到vAA寄存器中。这条指令必须在异常发生时由异常处理器使用,否则无效。
返回指令
返回指令是指函数结束时运行的最后一条指令,它的基础字节码为return
,如下四条返回指令。
- return-void: 函数从1个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
锁指令
锁指令多用在多线程程序对同一对象的操作中。Dalvik指令集中如下有两条锁指令
- monitor-enter vAA指令用于为指定对象获取锁。
- monitor-exit vAA指令用于释放指定对象的锁。
实例操作指令
与实例相关的操作包括实例的类型转换、检查及创建等
- check-cast vAA,type@BBBB :用于将vAA寄存器中的对象引用转换成指定的类型,如果失败会抛出ClassCastException异常。
- instance-of vA,vB,type@CCCC : 用于判断vB寄存器中的对象引用是否可以转换成指定的类型,如果可以就为vA寄存器赋值1,否则为0。
- new-instance vAA,type@BBB :构造一个指定类型对象的新实例,并将对象引用赋值给vAA寄存器。
- check-cast/jumbo vAAAA,type@BBBBBB : 与check-cast vAA,type@BBB 指令相同,只是前者的寄存器与指令索引的取值范围更大。
- instance-of/jumbo vAAAA,vBBBB,type@CCCCCCCC :与instance-of vA,vB,type@CCCC 相同,前者的寄存器与索引的取值范围更大。
- new-instance/jumbo vAAAA,type@BBBBBBBB : 与new-instance vAA,type@BBB相同,前者的寄存器与索引的取值范围更大。
数组操作指令
数组操作包括获取数组长度、新建数组、数组赋值、数组元素取值与赋值等
- array-length vA,vB : 获取给定vB寄存器中数组的长度,并将值赋予vA寄存器。数组产犊值就是数组中条目的个数
- new-array vA,vB type@CCCC :构造指定类型(type@CCCC)和大小(vB)的数组,并将值赋予vA寄存器。
- filled-new-array{vC,vD,vE,vF,vG},type@BBBB :构造指定类型(type@BBBB) 和大小(vA)的数组并填充数组内容。vA寄存器时隐含使用的,除了指定数组的大小,还指定了参数的个数。
- fill-array-data vAA,+BBBBBBBB : 用指定的数据来填充数组,vAA寄存器为数组引用,在指令后面会紧跟一个数据表。
- new-array/jumbo vAAAA,vBBBB,type@CCCCCCCC : 与new-array vA,vB,type@CCCC指令相同,只是寄存器与指令索引的取值范围更大。
- 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。
- goto +AA : 用于无条件跳转到指定偏移处,偏移量AA不能为0.
- goto/16 +AAAA : 用于无条件跳转到指定偏移处,偏移量AAAA不能为0.
- goto/32 +AAAAAAAA : 用于无条件跳转到指定偏移处.
- packed-switch vAA, +BBBBBBBB : 分支跳转指令,vAA寄存器位switch分支中需要判断的值,BBBBBBBB指向一个packed -switchpayload格式的偏移表
- if-test vA,vB,+CCCC : 用于比较vA寄存器与vB寄存器的值
- if-eq : 如果vA等于vB则跳转。
- if-ne : 如果vA不等于vB则跳转。
- if-lt : 如果vA小于vB则跳转
- if-ge :如果vA大于等于vB则跳转
- if-gt : 如果vA大于vB则跳转
- if-le :如果vA小于等于vB则跳转
比较指令
比较指令用于对两个寄存器的值(浮点型或长整型)进行比较,格式为cmpkind vAA,vBB,vCC
,其中vBB与vCC是需要比较的两个寄存器或两个寄存器对,比较的结果将被放到vAA寄存器。
- cmpl-float : 用于比较两个单精度浮点数
- cmpg-float : 用于比较两个单精度浮点数
- cmpl-double : 用于比较两个双精度浮点数
- cmpg-double :用于比较两个双精度浮点数
- cmp-long : 比较两个长整型数
字段操作指令
字段操作指令用于对对象实例的字段进行读写操作,字段的类型可以是Java中有效的数据类型。
对普通字段操作与静态字段操作,有两种指令集,分别是iinstanceop vA,vB,field@CCCC
与sstaticop vAA,field@BBBB
方法调用指令
方法调用指令负责调用类实例的方法,基础指令为invoke
。方法调用指令有invoke-kind {vC,vD,vE,vF,vG},meth@BBBB
与invoke-kind/range{vCCCC..vNNNN},meth@BBBB
两类。这两类指令在作用上并无不同,只是后者在设置参数寄存器时使用range来指定寄存器的范围。根据方法类型不同,如下五条方法调用指令:
- 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 :调用实例的接口方法
数据转换指令
数据转换指令用于将一种类型的数值转换成另一个类型的数值,格式为unop vA,vB。在vB寄存器或vB寄存器对中存放了需要转换的数据。转换结果保存在vA寄存器或vA寄存器对中。数据转换指令列举如下
- neg-int : 用于对整型数求补
- not-int :用于对整数型求反
- neg-long : 用于对长整型数求补
- not-long : 用于对长整型数求反
- neg-float : 用于对单精度浮点型数求补
- neg-double : 用于对双精度浮点型数求补
- int-to-long : 将整型数转换为长整型数
- int-to-float:将整型数转换为单精度浮点型数
- int-to-double:将整型数转换为双精度浮点型数
- long-to-int:将长整型数转换为整型数
数据运算指令
数据运算指令包括算术运算指令与逻辑运算指令。
- add-int v0, v1, v2:v0 = v1 + v2(整数加法)。
- add-int/2addr v0, v1:v0 += v1(将结果存储在同一寄存器中)。
- sub-int v0, v1, v2:v0 = v1 - v2(整数减法)。
- sub-int/2addr v0, v1:v0 -= v1。
- mul-int v0, v1, v2:v0 = v1 * v2(整数乘法)。
- mul-int/2addr v0, v1:v0 *= v1。
- div-int v0, v1, v2:v0 = v1 / v2(整数除法)。
- div-int/2addr v0, v1:v0 /= v1。
- rem-int v0, v1, v2:v0 = v1 % v2(整数求余)。
- rem-int/2addr v0, v1:v0 %= v1。
- add-long v0, v1, v2:v0 = v1 + v2(长整型加法)。
- add-long/2addr v0, v1:v0 += v1。
- sub-long v0, v1, v2:v0 = v1 - v2(长整型减法)。
- sub-long/2addr v0, v1:v0 -= v1。
- mul-long v0, v1, v2:v0 = v1 * v2(长整型乘法)。
- mul-long/2addr v0, v1:v0 *= v1。
- div-long v0, v1, v2:v0 = v1 / v2(长整型除法)。
- div-long/2addr v0, v1:v0 /= v1。
- rem-long v0, v1, v2:v0 = v1 % v2(长整型求余)。
- rem-long/2addr v0, v1:v0 %= v1。
- add-float v0, v1, v2:v0 = v1 + v2(浮点数加法)。
- add-float/2addr v0, v1:v0 += v1。
- sub-float v0, v1, v2:v0 = v1 - v2(浮点数减法)。
- sub-float/2addr v0, v1:v0 -= v1。
- mul-float v0, v1, v2:v0 = v1 * v2(浮点数乘法)。
- mul-float/2addr v0, v1:v0 *= v1。
- div-float v0, v1, v2:v0 = v1 / v2(浮点数除法)。
- div-float/2addr v0, v1:v0 /= v1。
- rem-float v0, v1, v2:v0 = v1 % v2(浮点数求余)。
- rem-float/2addr v0, v1:v0 %= v1。
- add-double v0, v1, v2:v0 = v1 + v2(双精度加法)。
- add-double/2addr v0, v1:v0 += v1。
- sub-double v0, v1, v2:v0 = v1 - v2(双精度减法)。
- sub-double/2addr v0, v1:v0 -= v1。
- mul-double v0, v1, v2:v0 = v1 * v2(双精度乘法)。
- mul-double/2addr v0, v1:v0 *= v1。
- div-double v0, v1, v2:v0 = v1 / v2(双精度除法)。
- div-double/2addr v0, v1:v0 /= v1。
- rem-double v0, v1, v2:v0 = v1 % v2(双精度求余)。
- rem-double/2addr v0, v1:v0 %= v1。
- and-int v0, v1, v2:v0 = v1 & v2。
- and-int/2addr v0, v1:v0 &= v1。
- and-long v0, v1, v2:v0 = v1 & v2(长整型)。
- or-int v0, v1, v2:v0 = v1 | v2。
- or-int/2addr v0, v1:v0 |= v1。
- or-long v0, v1, v2:v0 = v1 | v2(长整型)。
- xor-int v0, v1, v2:v0 = v1 ^ v2。
- xor-int/2addr v0, v1:v0 ^= v1。
- xor-long v0, v1, v2:v0 = v1 ^ v2(长整型)。
- shl-int v0, v1, v2:v0 = v1 << v2(整数左移)。
- shl-int/2addr v0, v1:v0 <<= v1。
- shr-int v0, v1, v2:v0 = v1 >> v2(整数算术右移)。
- shr-int/2addr v0, v1:v0 >>= v1。
- ushr-int v0, v1, v2:v0 = v1 >>> v2(整数逻辑右移)。
- 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逆向 - Dalvik语言基础
文章链接:https://fuckdog.org/post-57.html
本站文章均为原创,未经授权请勿用于任何商业用途