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
2
3
4
//java中
java.lang.String
//Dalvik中
Ljava/lang/String;

[ 类型表示所有基本类型的数组。[ 后面紧跟基本类型描述符,例如 [I 表示 int 类型的一维数组,相当于 Java 中的 int[]。多维数组的表示则是使用多个多个 [ ,例如[[I表示int[][]

L 与 [ 可以同时使用来表示对象数组。如 [Ljava.lang.String;表示Java中的字符串数组。

二、方法

Dalvik 使用方法名、类型参数与返回值来详细描述一个方法。这样做的目的是:

  1. 方便 Dalvik 虚拟机在运行时从方法表中快速地找到正确的方法
  2. Dalvik 虚拟机可以使用它们来做一些静态分析,比如 Dalvik 字节码的验证与优化。

方法的格式如下:

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

Lpackage/name/ObjectName;表示类,MethodName表示类中的方法,(III)Z是方法的签名部分,括号中的III表示参数(在这里具体为三个整型参数),Z表示方法的返回类型(在这里具体为布尔型)。

来一个复杂的例子:

1
.method public M(I[[IILjava/lang/String;[Ljava/lang/Object;)Ljava/lang/String

对应 Java 代码如下:

1
public String M(int, int[][], int, String, Object[])

方法代码以.method指令开始,以 .end method指令结束。在.method指令后面会跟随.registers num表示使用到的寄存器个数为 num 个,.parameter指定函数的一个参数,.prologue指定函数代码的起始位置 。

三、字段

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

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

字段由类型(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
move-wide/from16 vAA,VBBBB

根据约定,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-byteint-to-charint-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@xxxinvoke-type/range {vA...VN}, method@xxx 。两种类型的指令在作用上无区别,只是前者最多接收5个参数,大于5个参数的方法则使用后者调用。

invoke-type代指如下指令:

指令 含义
invoke-virtual 调用实例的虚方法
invoke-super 调用实例的父类方法
invoke-direct 调用实例的直接方法
invoke-static 调用实例的静态方法
inovke-interface 调用实例的接口方法

方法调用指令的返回值必须使用move-result[后缀]指令获取。

例如:

1
2
invoke-virtual      {v3, v0}, Lcom/ciscn/glass/MainActivity;->checkFlag(Ljava/lang/String;)Z # method@3c66
move-result v3

返回指令

返回指令为return。

  • return-void:表示函数从一个 void 方法返回。
  • return vA:表示函数返回一个32位的非对象类型的值。
  • return-wide vA:表示函数返回一个64位的非对象类型的值。
  • return-object vA:表示函数返回一个对象类型的值。

字段操作指令

字段操作指令用来对对象实例的字段进行读写操作。对普通字段与静态字段操作有两种指令集,分别是iinstanceop vA, vB, field@xxxsstaticop 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 寄存器指定用来存放读取或需要设置的数组元素的值。arrayopagetaput两大类指令,格式与字段操作指令的类似,这里不再举例。

实例操作指令

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

  • 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软件安全与逆向分析》


smali语法学习
http://example.com/2023/09/12/Android安全/smali语法摘录/
作者
gla2xy
发布于
2023年9月12日
许可协议