smali语法学习
一、类型
Dalvik 字节码只有两种类型,基本类型和引用类型。Dalvik 使用这两种类型来表示 Java 语言的全部类型,除了对象与数组属于引用对象外,其他的 Java 类型都是基本类型。
语法 | 含义 |
---|---|
V | void,只用于返回值类型 |
Z | boolean |
B | byte |
S | short |
C | char |
I | int |
J | long |
F | float |
D | double |
L | Java类类型 |
[ | 数组类型 |
每个 Dalvik 寄存器都是 32 位大小,对于超过 32 位的,比如 J、D 类型,它们就需要使用两个相邻的寄存器(寄存器对)来存储值。
这里对于最后两个类型进行详细说明。
L 类型表示 Java 类型中的任何类。这些类在 Java 代码中以 package.name.ObjectName
方法引用,但在 Dalvik 汇编代码中,类以 Lpackage/name/ObjectName;
形式表示,注意最后面有个分号。例如:
1 |
|
[ 类型表示所有基本类型的数组。[ 后面紧跟基本类型描述符,例如 [I
表示 int 类型的一维数组,相当于 Java 中的 int[]
。多维数组的表示则是使用多个多个 [ ,例如[[I
表示int[][]
。
L 与 [ 可以同时使用来表示对象数组。如 [Ljava.lang.String;
表示Java中的字符串数组。
二、方法
Dalvik 使用方法名、类型参数与返回值来详细描述一个方法。这样做的目的是:
- 方便 Dalvik 虚拟机在运行时从方法表中快速地找到正确的方法
- Dalvik 虚拟机可以使用它们来做一些静态分析,比如 Dalvik 字节码的验证与优化。
方法的格式如下:
1 |
|
Lpackage/name/ObjectName;
表示类,MethodName
表示类中的方法,(III)Z
是方法的签名部分,括号中的III
表示参数(在这里具体为三个整型参数),Z
表示方法的返回类型(在这里具体为布尔型)。
来一个复杂的例子:
1 |
|
对应 Java 代码如下:
1 |
|
方法代码以.method
指令开始,以 .end method
指令结束。在.method
指令后面会跟随.registers num
表示使用到的寄存器个数为 num 个,.parameter
指定函数的一个参数,.prologue
指定函数代码的起始位置 。
三、字段
字段与方法很相似,只是字段没有方法签名域的参数和返回值,取而代之的是字段的类型。字段格式如下:
1 |
|
字段由类型(Lpackage/name/ObjectName;
)、字段名(FieldName
)与字段类型(Ljava/lang/String;
)组成,并用:
将字段名和字段类型隔开。
字段代码以.field
指令开头。
四、寄存器命名方法
v命名法:采用以v
开头的方式表示函数中用到的局部变量和参数,所有的寄存器命名从 v0 开始,依次递增。
p命名法:采用以p
开头的方式表示函数中引入参数,从 p0 开始,依次递增。但对函数的局部变量寄存器的命名没有影响,即仍然使用 v 命名法。
五、指令集
指令特点
Dalvik 指令在调用格式上模仿了 C 语言的调用约定。Dalvik 指令的语法和助词符有如下特点:
- 参数采用从目标(destination)到源(source)的方式。
- 根据字节码的大小与类型不同,一些字节码添加了名称后缀以消除歧义、
- 32位常规类型的字节码不添加任何后缀。
- 64位常规类型的字节码添加
-wide
后缀。 - 特殊类型的字节码根据具体类型添加后缀。它们可以是
-boolean
、-byte
、-char
、-short
、-int
、-long
、-float
、-double
、-object
、-string
、-class
、-void
之一。
- 根据字节码的布局和选项不同,一些字节码添加了字节码后缀以消除歧义。这些后缀通过在字节码主名称后添加
/
来分隔。
约定:在指令集的描述中,宽度值中每个字母表示宽度为4位。例如
1 |
|
根据约定,vAA的取值范围位v0~v255;vBBBB的取值范围为v0~v65535。以下谈到的寄存器取值、位数、范围都是指寄存器编号。
数据定义指令
声明变量,基础字节码为 const。
- const[/4/16/high16] vA, xx:表示将 xx 赋值给寄存器 vA。
[]
表示可选内容,根据 xx 的长度选择,比如16位的就选/16
,32位就不选。/high16
则是取 xx 的高16位,右扩展成32位。 - const-wide[/16/32/high16] vA, xx:表示将 xx 赋值给寄存器对vA。
- const-string[/jumbo] vA, string@xxxx:通过字符串索引(string@xxxx)构造一个字符串给寄存器 vA。字符串索引较大时会添加指令后缀
/jumbo
。 - const-class[/jumbo] vA, type@xxxx:通过类型索引获取一个类型引用并赋给寄存器 vA。
数据操作指令
数据操作指令为move。
- move[/16/from16] vA, vB:寄存器赋值,将 vB 寄存器的值赋值给 vA 寄存器。指令后缀
/16
表示源寄存器和目的寄存器都是16位。指令后缀/from16
表示源寄存器位16位,目的寄存器为8位。 - mov-wide[/from16] vA, vB:寄存器对赋值。
- move-object[/16/from16] vA, vB:对象赋值。
- move-result vA:将上一个 invoke 类型指令操作的单字非对象结果赋给 vA 寄存器。
- move-result-wide vA:将上一个 invoke 类型指令操作的双字非对象结果赋给 vA 寄存器。
- move-result-object vA:将上一个 invoke 类型指令操作的对象结果赋给 vA 寄存器。
- move-exception vA:使用 vA 寄存器保存一个运行时发生的异常。这条指令必须是异常发生时的异常处理器的一条指令,否则该指令无效。
数据转换指令
数据转换指令用于将一种类型的数值转换成另一种类型。他的格式为unop vA, vB
,后者存放需要转换的数据,转换后的结果保存在 vA 寄存器(或 vA 寄存器对)中。
- 求补指令
neg-xxx
:如neg-int
表示对整型数求补,neg-long
表示对长整型数求补。 - 求反指令
not-xxx
:如not-int
表示对整型数求反,not-long
表示对长整型数求反。 - 数据类型转换指令
xxx-to-xxx
:如int-to-float
表示将整型数转成单精度浮点型,double-to-long
表示将双精度浮点型数转成长整型。int
型额外还有int-to-byte
、int-to-char
、int-to-short
指令。
数据运算指令
数据运算指令有如下四类:
- binop vA, vB, vC:将 vB 寄存器与 vC 寄存器进行运算,结果保存到 vA 寄存器中。
- binop/2addr vA, vB:将 vA 寄存器与 vB 寄存器进行运算,结果保存到 vA 寄存器中。指令后缀
/2addr
表示使用两地址(即两个寄存器)的形式进行运算。 - binop/lit16 vA, vB, xx:将 vB 寄存器与常量xx 进行运算,结果保存到 vA 寄存器中。指令后缀
/lit16
指明常量是16位的。 - binop/lit8 vA, vB, xx:将 vB 寄存器与常量xx 进行运算,结果保存到 vA 寄存器中。指令后缀
/lit8
指明常量是8位的。
其中 binop 代指如下运算指令:
运算指令 | 含义 |
---|---|
add-type | 加法运算,后缀-type代指-int、-long、-float、-double。 |
sub-type | 减法运算 |
mul-type | 乘法运算 |
div-type | 除法运算 |
rem-type | 模运算 |
and-type | 与运算 |
or-type | 或运算 |
xor-type | 异或运算 |
shl-type | 有符号数左移 |
shr-type | 有符号数右移 |
ushr-type | 无符号数右移 |
跳转指令
Dalvik指令集中有三种跳转指令:无条件跳转(goto)、分支跳转(switch)、条件跳转(if)。
无条件跳转指令
- goto xxx
- goto/16 xxx
- goto/32 xxx
指令后缀根据由偏移量 xxx 的位数决定,偏移量不能为0。
分支跳转指令
- packed-switch vA, xxx:vA 寄存器为 switch 分支中需要判断的值,xxx 指向一个
packed-switch-payload
格式的偏移表(switch汇编中对应的的小表?),表中的值是有规律递增的。 - sparse-switch vA, xxx:vA 寄存器为 switch 分支中需要判断的值,xxx 指向一个
sparse-switch-payload
格式的偏移表,表中的值是无规律的偏移量。
条件跳转指令
有两种指令格式,第一种指令格式为if-test vA, vB, xxxx
,表示比较 vA 寄存器和 vB 寄存器的值,如果比较结果满足就跳转到xxxx指定的偏移处,偏移量不能为0。if-test
代指如下指令:
指令 | 含义 |
---|---|
if-eq | 如果vA = vB则跳转 |
if-ne | 如果vA != vB则跳转 |
if-lt | 如果vA < vB则跳转 |
if-ge | 如果vA >= vB则跳转 |
if-gt | 如果vA > vB则跳转 |
if-le | 如果vA <= vB则跳转 |
第二种指令格式为if-testz vA,xxxx
,表示拿 vA 寄存器的值与 0 比较,如果比较结果满足就跳转到xxxx指定的偏移处,偏移量不能为0。if-testz
代指的指令同上,只不过后面多了一个z
。
比较指令
比较指令的格式为cmp-type vA, VB, vC
,表示 vB 寄存器(对)与 vC 寄存器(对)进行比较,比较的结果存放在 vA 寄存器中。cmp-type
代指如下指令:
指令 | 含义 |
---|---|
cmpl-float | 比较两个单精度浮点数。如果vB > vC,则结果为-1,相等结果为0,小于结果为1。 |
cmpg-float | 比较两个单精度浮点数。如果vB > vC,则结果为1,相等结果为0,小于结果为-1。 |
cmpl-double | 比较两个双精度浮点数。如果vB > vC,则结果为-1,相等结果为0,小于结果为1 |
cmpg-double | 比较两个双精度浮点数。如果vB > vC,则结果为1,相等结果为0,小于结果为-1。 |
cmp-long | 比较两个长整型数。如果vB > vC,则结果为1,相等结果为0,小于结果为-1。 |
方法调用指令
方法调用指令为 invoke,负责调用类实例的方法。该指令有两种格式,分别为invoke-type {vA,vB,...}, method@xxx
和invoke-type/range {vA...VN}, method@xxx
。两种类型的指令在作用上无区别,只是前者最多接收5个参数,大于5个参数的方法则使用后者调用。
invoke-type
代指如下指令:
指令 | 含义 |
---|---|
invoke-virtual | 调用实例的虚方法 |
invoke-super | 调用实例的父类方法 |
invoke-direct | 调用实例的直接方法 |
invoke-static | 调用实例的静态方法 |
inovke-interface | 调用实例的接口方法 |
方法调用指令的返回值必须使用move-result[后缀]
指令获取。
例如:
1 |
|
返回指令
返回指令为return。
- return-void:表示函数从一个 void 方法返回。
- return vA:表示函数返回一个32位的非对象类型的值。
- return-wide vA:表示函数返回一个64位的非对象类型的值。
- return-object vA:表示函数返回一个对象类型的值。
字段操作指令
字段操作指令用来对对象实例的字段进行读写操作。对普通字段与静态字段操作有两种指令集,分别是iinstanceop vA, vB, field@xxx
和sstaticop vA, field@xxx
。
普通字段指令的指令前缀为i
,比如对普通字段的读操作使用iget
指令。
静态字段指令的指令前缀为s
,比如对静态字段的读操作使用sget
指令。
根据访问的字段类型不同,字段操作指令后面会跟随字段类型的后缀,比如iget-byte
指令等。
普通字段操作指令有(只举例get操作,put操作相同,替换get即可):iget、iget-wide、iget-object、iget-boolean、iget-byte、iget-char、iget-short。
静态字段操作指令有(只举例get操作,put操作相同,替换get即可):sget、sget-wide、sget-object、sget-boolean、sget-byte、sget-char、sget-short。
数组操作指令
数组操作包括获取数组长度、新建数组、数组赋值、数组元素取值与赋值等操作。
array-length vA, vB:表示获取 vB 寄存器中数组的长度并将值赋值给 vA 寄存器。
new-array vA, vB, type@xxx:表示构造指定类型(type@xxx)与大小(vB)的数组,并赋值给 vA 寄存器。可添加指令后缀
/jumbo
。filled-new-array {vC, vD, vE, vF, vG}, type@xxx:构造指定类型(type@xxx)与大小(vA)的数组并填充数组内容。vA 寄存器是隐式使用的,除了指定数组的大小以外,还指定了参数的个数,vC~vG 是使用到的参数寄存器序列。
可添加指令后缀
/range
,功能相同,只是指定了参数寄存器的取值范围。可添加指令后缀
/jumbo
。filled-array-data vA, xxx:用指定的数据来填充数组,数组必须为基本类型的数组。
格式arrayop vA, vB, vC:表示对 vB 寄存器指定的数组元素进行取值和赋值,其中 vC 寄存器指定数组元素索引,vA 寄存器指定用来存放读取或需要设置的数组元素的值。
arrayop
指aget
和aput
两大类指令,格式与字段操作指令的类似,这里不再举例。
实例操作指令
与实例相关的操作包括实例的类型转换、检查、新建等。
- check-cast vA, type@xxx:表示将 vA 寄存器中的对象引用转换成指定的类型。
- instance-of vA, vB, type@xxx:表示判断 vB 寄存器中的对象引用是否可以转成指定的类型,如果可以则给 vA 寄存器赋值为1,否则赋值为0。
- new-instance vA, type@xxx:表示构造一个指定类型对象的新实例,并将对象引用赋值给 vA 寄存器,类型符 type 不能是数组类。
以上指令都可以添加指令后缀/jumbo
,功能不变,只是寄存器值与指令的索引取值范围更大。
锁指令
锁指令多用于多线程程序中对同一对象的操作。Dalvik 指令集中有两条锁指令:
- monitor-enter vA表示获取指定对象的锁。
- monitor-exit vA表示释放指定对象的锁。
异常指令
Dalvik指令集中有一条指令用于抛出异常:
- throw vA:表示抛出 vA 寄存器存放的异常。
空操作指令
空操作指令为 nop,他的机器码为 0x00(与PC端的nop(0x90)不同)。该指令不进行任何操作。
参考:
《Android软件安全与逆向分析》