3.4.1 文件格式
无论是普通类、抽象类、接口类或者内部类,在反编译出的代码中,它们都以单独的Smali 文件来存放。每个 smali 文件头 3 行描述了当前类的一些信息,格式如下。
———————————————————————————————————————
.class <访问权限> [修饰关键字] <类名>
.super <父类名>
.source <源文件名>
———————————————————————————————————————
打开 HelloWorld.smali 文件,头 3 行代码如下。
———————————————————————————————————————
.class public LHelloWorld;
.super Landroid/app/Activity;
.source "HelloWorld.java"
———————————————————————————————————————
第 1 行“.class”指令指定了当前类的类名。在本例中,类的访问权限为 public,类名为“LHelloWorld;” ,类名开头的 L 是遵循 Dalvik 字节码的相关约定,表示后面跟随的字符串为一个类。
第 2 行的“.super ”指令指定了当前类的父类。本例中的“LHelloWorld;”的父类为“Landroid/app/Activity;” 。
第 3 行的“.source”指令指定了当前类的源文件名。经过混淆的 dex 文件,反编译出来的 smali 代码可能没有源文件信息,因此“.source”行的代码可能为空。
前 3 行代码过后就是类的主体部分了,一个类可以由多个字段或方法组成。
smali 文件中字段的声明使用“.field”指令。字段有静态字段与实例字段两种。静态字段的声明格式如下。
———————————————————————————————————————
#static fields
.field <访问权限> static [修饰关键字] <字段名>:<字段类型>
———————————————————————————————————————
baksmali 在生成 Smali 文件时,会在静态字段声明的起始处添加“static fields”注释,
Smali 文件中的注释与 Dalvik 语法一样,也是以井号“#”开头。 “.field”指令后面跟着的是访问权限,可以是 public、private、protected 之一。修饰关键字描述了字段的其它属性,如synthetic。指令的最后是字段名与字段类型,使用冒号“:”分隔,语法上与 Dalvik 也是一样的。
实例字段的声明与静态字段类似,只是少了 static 关键字,它的格式如下。
———————————————————————————————————————
#instance fields
.field <访问权限> [修饰关键字] <字段名>:<字段类型>
———————————————————————————————————————
比如以下的实例字段声明。
———————————————————————————————————————
#instance fields
.field private btn:Landroid/widget/Button;
———————————————————————————————————————
第 1 行的“instance fields”是 baksmali 生成的注释,第 2 行表示一个私有字段 btn,它
的类型为“Landroid/widget/Button;” 。如果一个类中含有方法,那么类中必然会有相关方法的反汇编代码,Smali 文件中方法的声明使用“.method”指令。方法有直接方法与虚方法两种。直接方法的声明格式如下。
———————————————————————————————————————
#direct methods
.method <访问权限> [修饰关键字] <方法原型>
<.locals>
[.parameter]
[.prologue]
[.line]
<代码体>
.end method
———————————————————————————————————————
“direct methods” 是 baksmali 添加的注释, 访问权限和修饰关键字与字段的描述相同,方法原型描述了方法的名称、参数与返回值。
“.locals ”指定了使用的局部变量的个数。
“.parameter”指定了方法的参数,与 Dalvik 语法中使用“.parameters”指定参数个数不同,
每个“.parameter”指令表明使用一个参数,比如方法中有使用到 3 个参数,那么就会出现3 条“.parameter”指令。
“.prologue”指定了代码的开始处,混淆过的代码可能去掉了该指令。 “.line”指定了该处指令在源代码中的行号,同样的,混淆过的代码可能去除了行号信息。
虚方法(从父类继承的方法)的声明与直接方法相同,只是起始处的注释为“virtual methods” 。
如果一个类实现了接口,会在 smali 文件中使用“.implements”指令指出。相应的格式声明如下。
———————————————————————————————————————
#interfaces
.implements <接口名>
———————————————————————————————————————
“#interfaces”是 baksmali 添加的接口注释, “.implements”是接口关键字,后面的接口名是 DexClassDef 结构中 interfacesOff 字段指定的内容。
如果一个类使用了注解,会在 smali 文件中使用“.annotation”指令指出。注解的格式声明如下。
———————————————————————————————————————
#annotations
.annotation [注解属性] <注解类名>
[注解字段=值]
.endannotation
———————————————————————————————————————
注解的作用范围可以是类、方法或字段。如果注解的作用范围是类, “.annotation”指
令会直接定义在 smali 文件中,如果是方法或字段, “.annotation”指令则会包含在方法或
字段定义中。例如下面的代码。
———————————————————————————————————————
#instance fields
.field public sayWhat:Ljava/lang/String;
.annotation runtime LMyAnnoField;
info="Hellomyfriend"
.end annotation
.end field
———————————————————————————————————————
实例字段 sayWhat 为 String 类型, 它使用了 MyAnnoField 注解, 注解字段 info 值为 “Hellomyfriend” 。将其转换为 Java 代码为:
———————————————————————————————————————
@MyAnnoField(info="Hellomyfriend")
public String sayWhat
———————————————————————————————————————
invoke-kind {vC, vD, vE, vF,
vG}, meth@BBBB
|- invoke-virtual
|- invoke-super
|- invoke-direct
|- invoke-static
|- invoke-interface
调用指定的方法,{vC,…,vG}为传入方法的参数列
表。具体使用哪种调用方式,视方法的对象类型和
方法本身类型而定。
invoke-virtual 调用实例的虚方法,通常成员对
象实例的方法都以该指令调用。
invoke-super 调用实例的父类方法。
invoke-direct 调用直接方法,通常私有方法都以
该指令调用。
invoke-static 调用静态方法。
invoke-interface 调用接口的方法
3.2 Dalvik 虚拟机字节码的类型、方法和字段的表示方法
3.2.1 类型
Dalvik 字节码有两种类型,基本类型和引用类型。对象和数组是引用类型,其它都是基
本类型。
Dalvik 字节码类型描述符
描述符 类型
V void,只能用于返回值类型
Z boolean
B byte
S short
C char
I int
J long(64 位)
F float
D double(64 位)
L Java 类类型
[ 数组类型
每个 Dalvik 寄存器都是 32 位大小,对于小于或者等于 32 位长度的类型来说,一个寄存
器就可以存放该类型的值,而像 J、D 等 64 位的类型,它们的值是使用相邻两个寄存器来存
储的,如 v0 与 v1、v3 与 v4 等。
Java 中的对象在 smali 中以 Lpackage/name/ObjectName;的形式表示。前面的 L 表示
这是一个对象类型,package/name/表示该对象所在的包,ObjectName 是对象的名字,“;”
表示对象名称的结束。相当于 java 中的 package.name.ObjectName。例 如: Ljava/lang/String;
相当于 java.lang.String。
“[”类型可以表示所有基本类型的数组。[I 表示一个整型一维数组,相当于 java 中的
int[]。对于多维数组,只要增加[就行了,[[I 相当于 int[][],[[[I 相当于 int[][][]。注意每一维
的最多 255 个。对象数组的表示:[Ljava/lang/String;表示一个 String 对象数组。
3.2.2 方法
方法调用的表示格式:Lpackage/name/ObjectName;->MethodName(III)Z。
Lpackage/name/ObjectName;表示类型,MethodName 是方法名,III 为参数(在此是 3 个整
型参数) ,Z 是返回类型(bool 型) 。函数的参数是一个接一个的,中间没有隔开。
一个更复杂的例子:method(I[[IILjava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
在 java 中则为:String method(int, int[][], int, String, Object[])
3.2.3 字段
字段,即 java 中类的成员变量,表示格式:
Lpackage/name/ObjectName;->FieldName:Ljava/lang/String; 即包名,字段名
和字段类型,字段名与字段类型是以冒号“:”分隔。
3.3 Dalvik 虚拟机字节码指令解析
在对 3.1 中的 Dalvik 虚拟机字节码指令表中涉及的指令进行分别解析前,先介绍 Dalvik
虚拟机字节码中寄存器的命名法。
3.3.1 两种不同的寄存器表示法
在 Dalvik 虚拟机字节码中寄存器的命名法中主要有 2 种:v 命名法和 p 命名法。假设一
个函数使用到 M 个寄存器,并且该函数有 N 个入参,根据 Dalvik 虚拟机参数传递方式中的
规定:入参使用最后的 N 个寄存器中,局部变量使用从 v0 开始的前 M-N 个寄存器。比如,
某函数 A 使用了 5 个寄存器,2 个显式的整形参数,如果函数 A 是非静态方法,函数被调用
时会传入一个隐式的对象引用,因此实际传入的参数个数是 3 个。根据传参规则,局部变量
将使用前 2 个寄存器,参数会使用后 3 个寄存器。
v 命名法采用小写字母“v”开头的方式表示函数中用到的局部变量与参数,所有的寄
存器命名从 v0 开始,依次递增。对于上文的函数 A,v 命名法会用到 v0、v1、v2、v3、v4
等 5 个寄存器,v0 与 v1 表示函数 A 的局部变量,v2 表示传入的隐式对象引用,v3 与 v4 表
示实际传入的 2 个整形参数。
P 命名法对函数的局部变量寄存器命名没有影响,它的命名规则是:函数的入参从 p0
开始命名,依次递增。对于上文的函数 A,p 命名法会用到 v0、v1、p0、p1、p2 等 5 个寄
存器,v0 与 v1 表示函数 A 的局部变量,p0 表示传入的隐式对象引用,p1 与 p2 表示实际传
入的 2 个整形参数。此时,p0、p1、p2 实际上分别表示 v2、v3、v4,只是命名不一样而已。
在实际的 Smali 文件中,几乎都是使用了 p 命名法,主要原因是使用 p 命名法能够通过
寄存器的名字前缀就能很容易判断寄存器到底是局部变量还是函数的入参。初次学习 smali
语法时容易对寄存器 p0 表示的意义出现混乱,这主要体现在静态方法和非静态方法中。其
实只要理解 p 命名法的定义后就可以很清楚的理解。在 smali 语法中,在调用非静态方法时
需要传入该方法所在对象的引用,因此此时 p0 表示的是传入的隐式对象引用,从 p1 开始
才是实际传入的入参。但是在调用静态方法时,由于静态方法不需要构建对象的引用,因而
也就不需要传入该方法所在对象的引用,因此此时从 p0 开始就是实际传入的入参。
在 Dalvik 指令中使用“v 加数字”的方法来索引寄存器,如:v0、v1、v15、v255,但每
条指令使用的寄存器索引范围都有限制(因为 Dalvik 指令字节码必须字节对齐) ,这里我们
使用一个大写字母来表示 4 位数据宽度的取值范围,如:指令 move vA, vB,目的寄存器 vA
可使用 v0 ~ v15 的寄存器,源寄存器 vB 可以使用 v0 ~ v15 寄存器。指令 move/from16 vAA,
vBBBBB,目的寄存器 vAA 可使用 v0 ~ v255 的寄存器,源寄存器 vB 可以使用 v0 ~ v65535 寄
存器。简而言之,当目的寄存器和源寄存器中有一个寄存器的编号大于 15 时,即需要加上
/from16 指令才能得到正确运行。初次学习 Smali 语法时也容易对这一点不能理解,不注意
就会导致 Smali 文件汇编为 dex 文件的时候出现编译错误。 比如, 按照前面总结的 p 命名法,
当 p0 实际表示的寄存器编号大于 15 时,此时 Smali 语句 move v0,p0 就会编译出错
smali 学习笔记 1)
最新推荐文章于 2023-11-30 20:19:02 发布