安卓逆向入门笔记——smali基础

smali基础语法

dalvik字节码

1、先来了解一下dalvik虚拟机:

dalvik虚拟机是Android 5.0以前用于运行安卓应用的虚拟机,从 Android 4.4 开始,Google 开始引入了全新的虚拟机 ART(Android Runtime),直到Android5.0开始ART虚拟机就替代了dalvik虚拟机。既然dalvik虚拟机被ART虚拟机替代了,那我们还有学的必要吗?ART 是向下兼容的,ART虚拟机对DEX字节码的运行是兼容的,因此针对 Dalvik 开发的应用也能在 ART 环境中运作。但是Dalvik 采用的一些技术并不适用于 ART,但不妨碍我们了解和学习dalvik字节码。

2、我们再来了解一下dalvik寄存器和寄存器的命名方法:

Dalvik寄存器中的寄存器都是32位大小,支持所有类型,对于小于或等于32位的类型,使用一个寄存器就可以了;对于64位(long和double)类型,需要使用两个相邻的寄存器来存储。

寄存器的命名方法有两种:v命名法和p命名法:

​      v命名法:局部变量寄存器用v开头数字结尾的符号来表示,如v0、 v1、v2。

​      p命名法:函数参数寄存器则使用p开头数字结尾的符号来表示,如p0、p1、p2。

特别注意的是,p0不一定是函数中的第一个参数,在非static函数中,p0代指“this",p1表示函数的第一个 参数,p2代表函数中的第二个参数。而在static函数中p0才对应第一个参数(因为Java的static方法中没有this方法)

寄存器赋值:

const/4 p5, 0x1 //p5赋值1

const/16 v0, 0xa //v0赋值10,在16进制里a表示10

const/4和const/16是表示定义一个4位和16位值,这里只是修饰一下,记住Dalvik寄存器是32位寄存器,这只是表示32位寄存器中存的值是4位和16位而已,不要被这个给影响了。

3、再来了解一下dalvik字节码的类型与Java数据类型的对应关系:

数据类型对应:

smali类型 java类型 注释
V void 无返回值
Z boolean 布尔值类型,返回0或1
B byte 字节类型,返回字节
S short 短整数类型,返回数字
C char 字符类型,返回字符
I int 整数类型,返回数字
J long (64位 需要2个寄存器存储) 长整数类型,返回数字
F float 单浮点类型,返回数字
D double (64位 需要2个寄存器存储) 双浮点类型,返回数字
string String 文本类型,返回字符串
Lxxx/xxx/xxx object 对象类型,返回对象

long类型和double类型都是64位,需要两个寄存器来存储,有时在smali代码中,原本从0到n的寄存器在中间少了一个,那就有可能是其中有数据是long类型或者double类型的,在赋值代码中虽然用到了两个寄存器,但隐藏了其中一个。

4、了解smali和Java之间的转换流程:

xxx.java ==> xxx.class ==> 使用dx工具将.class打包成.dex文件 ==> 使用baksmali工具将.dex文件反编译成.smali文件 ==> 反编译成smali文件后对该文件进行修改成功 ==> 使用samli工具将.smali文件打包成.dex文件(回编译)

5、了解dalvik方法:

//一个私有、静态、不可变的方法   方法名
.method private static final onCreate$lambda-2([Ljava/lang/String;)Z//(这里面是方法的参数)这里是方法返回值类型,表示布尔值类型,返回假或真

.method表示定义了一个方法;

private、static、final表示该方法是一个一个私有、静态、不可变的方法;

onCreate$lambda-2是该方法定义的方法名,方法名后面括号中是该方法的参数;

[Ljava/lang/String;该参数表示接收字符串类型的一维数组,其中[表示一维数组,在smali的方法参数中每一个[就表示一维数组,比如[[就表示二维数组;java中类是一种数据类型,类用L表示,L后面接的是完整类名,也就是包名和类名。

6、了解dalvik字段:

dalvik创建字段需要指定字段的访问标志(修饰符)、字段名、字段类型和初始值等信息。下面是一个示例字段的创建代码:

.field private static count:I = 0

上述代码创建了一个名为count的私有静态字段,类型为整型,初始值为0。

dalvik指令集

1、dalvik指令集格式:

基础字节码-名称后缀/字节码后缀 目的寄存器 源寄存器/常量

  • Dalvik指令集中参数采用从源寄存器到目标寄存器的方式。

  • 根据dalvik字节码的大小与类型不同,可能会添加名称后缀以消除岐义。具体来说,字节码的类型可以分为常规类型和特殊类型,根据类型的不同添加的后缀也不同,常规类型的字节码不添加任何后缀,而特殊类型的字节码根据具体类型添加后缀,可能是以下几种后缀之一:

    • -boolean:布尔类型
    • -byte:字节类型
    • -char:字符类型
    • -short:短整型
    • -int:整型
    • -long:长整型
    • -float:浮点型
    • -double:双精度浮点型
    • -object:对象类型
    • -string:字符串类型
    • -class:类类型
    • -void:无返回值类型
  • 常规类型的字节码主要有以下几种:

    • move:将一个寄存器的值复制到另一个寄存器中
    • add:将两个寄存器的值相加,并将结果存储到目标寄存器中
    • sub:将两个寄存器的值相减,并将结果存储到目标寄存器中
    • mul:将两个寄存器的值相乘,并将结果存储到目标寄存器中
    • div:将两个寄存器的值相除,并将结果存储到目标寄存器中
    • rem:将两个寄存器的值取余,并将结果存储到目标寄存器中
    • neg:将一个寄存器的值取反,并将结果存储到目标寄存器中
    • and:将两个寄存器的值进行按位与操作,并将结果存储到目标寄存器中
    • or:将两个寄存器的值进行按位或操作,并将结果存储到目标寄存器中
    • xor:将两个寄存器的值进行按位异或操作,并将结果存储到目标寄存器中
    • shl:将一个寄存器的值左移指定位数,并将结果存储到目标寄存器中
    • shr:将一个寄存器的值右移指定位数,并将结果存储到目标寄存器中
    • ushr:将一个寄存器的值无符号右移指定位数,并将结果存储到目标寄存器中
    • not:将一个寄存器的值进行按位取反操作,并将结果存储到目标寄存器中
    • cmp:比较两个寄存器的值,并将结果存储到目标寄存器中
  • 此外,根据字节码的大小与布局的不同,也可能添加字节码后缀以消除岐义。这些后缀通过在字节码主名称后添加斜杠“/”来分隔开。例如,一些指令可能会添加以下后缀:

    • -wide:64位宽度

    • -from16:源为16位寄存器引用

    • -to16:目标为16位寄存器引用

    • -range:指示该指令的操作数是一个范围。

  • 32位常规类型的字节码没有添加任何后缀。

  • 64位常规类型的字节码添加 -wide后缀。

  • 例如这条指令:“move-wide/from16 vAA, vBBBB”:

    • 这条指令是移动命令,用于将源寄存器中存储的数据移动到目标寄存器中。
    • 其中“wide”表示数据宽度为64位,“from16”表示源寄存器为一个16位寄存器引用,vAA表示目标寄存器,它的取值范围是v0~v255,vBBBB表示源寄存器,它的取值范围是v0~v65535。
    • Dalvik指令集中的大多数指令都需要使用寄存器作为源操作数或目的操作数。其中,A/B/C/D/E/F/G/H表示一个4位数值,可以表示0~15的数值或v0~v15的寄存器。AA/BB/CC/DD/EE/FF/GG/HH表示一个8位数值,可以表示0~255的数值或v0~v255的寄存器。AAAA/BBBB/CCCC/DDDD/EEEE/FFFF/GGGG/HHHH表示一个16位数值,可以表示0~65535的数值或v0~v65535的寄存器。

2、dalvik指令集的使用:

  • 空操作指令

    • 空操作指令的助记符为nop。它的值为00,通常nop指令被用来作对齐代码之用,无实际操作,就和空格差不多。
  • 数据操作指令

    • move指令的三种作用:

    • 第一种作用:进行赋值操作

    • 第二种作用:move-result接收方法返回值操作

    • 第三种作用:处理异常的操作

    • move指令集的使用:

    • move vx, vy 指令将寄存器vy中的值移动到寄存器vx中。例如,move v1, v2表示将寄存器v2中的值移动到寄存器v1中,源寄存器与目的寄存器都为4位。

    • move/from16 vx, vy 指令将16位寄存器vy中的值移动到寄存器vx中。例如,move/from16 v1, v2表示将16位寄存器v2中的值移动到寄存器v1中,源寄存器为16位,目的寄存器为8位(范围必须在256以内)。注意,寄存器中存储的是值的引用,也可以理解为存储的是值的地址。

    • move-wide/from16 vx, vy 指令用于移动long/double类型的数据,将其从16位寄存器vy移动到范围必须在256内(8位)的寄存器vx中。例如,move-wide/from16 v1, v2表示将16位寄存器v2中的long/double类型的值移动到8位寄存器v1中。注意,寄存器中存储的是值的引用,也可以理解为存储的是值的地址。

    • move/16 vx, vy 指令将寄存器vy的值移动到寄存器vx中。例如,move/16 v1,v2表示将16位寄存器v2中的值移动到16位寄存器v1中,源寄存器与目的寄存器都为16位。

    • move-object vx, vy 指令将一个对象引用从寄存器vy移动到寄存器vx中。例如,move-object v1, v2表示将对象引用从寄存器v2移动到寄存器v1中。

    • move-object/from16 vx, vy 指令将对象引用从16位寄存器vy移动到寄存器vx中。例如,move-object/from16 v1, v2表示将对象引用从16位寄存器v2移动到寄存器v1中。

    • move-result vx 操作将上一条指令的返回值存储到指令中的目标寄存器vx中。例如,move-result v1表示将上一条指令的返回值移动到寄存器v1中。

    • move-result-wide vx 操作与move-result类似,但是用于保存长整数或双精度浮点数类型的返回值。例如,move-result-wide v1表示将长整数或双精度浮点数类型的返回值移动到寄存器v1中。

    • move-result-object vx 操作与move-result类似,但是用于保存引用类型的返回值。例如,move-result-object v1表示将引用类型的返回值移动到寄存器v1中。

    • move-exception vx 操作用于捕获异常对象并将其移动到指定寄存器vx中。例如,move-exception v1表示将异常对象移动到寄存器v1中。

    • 返回指令(重点)

    • return-void:用于表示函数从一个void方法中返回,返回值为空。例如,return-void表示从当前方法中返回值为空。

    • return vx:用于返回一个整数类型的值,该值存储在寄存器vx中。例如,return v1表示从当前方法中返回寄存器v1中存储的整数类型的值。

    • return-wide vx:用于返回一个长整数或双精度浮点数类型的值,该值存储在寄存器vx和vx+1中。例如,return-wide v1表示从当前方法中返回寄存器v1和v2中存储的长整数或双精度浮点数类型的值。

    • return-object vx:用于返回一个对象引用类型的值,该值存储在寄存器vx中。例如,return-object v1表示从当前方法中返回寄存器v1中存储的对象引用类型的值。

    • return-boolean vx:用于返回一个布尔类型的值,该值存储在寄存器vx中。例如,return-boolean v1表示从当前方法中返回寄存器v1中存储的布尔类型的值。

    • return-byte vx:用于返回一个字节类型的值,该值存储在寄存器vx中。例如,return-byte v1表示从当前方法中返回寄存器v1中存储的字节类型的值。

    • return-char vx:用于返回一个字符类型的值,该值存储在寄存器vx中。例如,return-char v1表示从当前方法中返回寄存器v1中存储的字符类型的值。

    • return-short vx:用于返回一个短整数类型的值,该值存储在寄存器vx中。例如,return-short v1表示从当前方法中返回寄存器v1中存储的短整数类型的值。

    • 数据定义指令(重点)

    • const/4 vA,#+B:用于将一个4位的数值符号扩展为32后赋值给寄存器vA。其中,vA表示目标寄存器,#+B表示常量值,取值范围为-8到7。

    • const/16 vAA, #+BBBB:用于将一个16位的数据符号扩展为32位后赋给寄存器vAA。其中,vAA表示目标寄存器,#+BBBB表示常量值,取值范围为-32768到32767。

    • const vAA, #+BBBBBBBB:用于将一个32位的数值赋给寄存器vAA。其中,vAA表示目标寄存器,#+BBBBBBBB表示常量值,取值范围为-2147483648到2147483647。

    • const/high16 vAA, #+BBBB0000:用于将一个高16位的数值以低16位为零的方式扩展为32位后赋给寄存器vAA。其中,vAA表示目标寄存器,#+BBBB0000表示常量值的高16位。

    • const-wide/16 vAA,#+BBBB:用于将16位的数值符号扩展为64位后赋值给寄存器vAA和vAA+1。其中,vAA和vAA+1表示目标寄存器(因为dalvik寄存器是32位的,所以存储64位数据需要两个寄存器存储),#+BBBB表示常量值,取值范围为-32768到32767。

    • const-wide/32 vAA, #+BBBBBBBB:用于将32位的数值符号扩展为64位后赋值给寄存器vAA和vAA+1。其中,vAA和vAA+1表示目标寄存器,#+BBBBBBBB表示常量值。

    • const-wide vAA, #+BBBBBBBBBBBBBBBB:用于将64位的数值赋给寄存器vAA和vAA+1。其中,vAA和vAA+1表示目标寄存器,#+BBBBBBBBBBBBBBBB表示常量值。

    • const-wide/high16 vAA, #+BBBB000000000000:用于将一个高16位的数值以其他位为零的方式扩展为64位后赋给寄存器vAA和vAA+1。其中,vAA和vAA+1表示目标寄存器,#+BBBB000000000000表示常量值的高16位。

    • const-string vAA,string@BBBB:用于将字符串常量引用存储到寄存器vAA中,通过字符串ID或字符串。其中,vAA表示目标寄存器,string@BBBB表示字符串在常量池中的索引。

    • const-string/jumbo vAA, string@BBBBBBBB:与const-string类似,但用于存储较长的字符串常量引用。

    • const-class vAA,type@BBBB:用于将类对象常量存储到寄存器vAA中,通过类型ID或类型(如Object.class)。其中,vAA表示目标寄存器,type@BBBB表示类在常量池中的索引。

    • const-class/jumbo vAAAA, type@BBBBBBBB:与const-class类似,但const-class/jumbo指令占用两个字节,值为0xooff,并且其操作数比常规的const-class指令操作数更大。这是Android4.0中新增的指令。

    • 实例操作指令

    • new-instance vAA, type@BBBB:根据类型或者类型ID新建一个对象实例,并将新建的对象的引用存入目标寄存器vAA中。

    • instance-of vA, vB, type@CCCC:检查vB寄存器中的对象引用是否是type@CCCC对应类型的实例,如果是vA寄存器存入非零值,否则vA寄存器存入零。

    • check-cast vAA, type@BBBB:检查vAA寄存器中的对象引用是否可以转换成type@BBBB对应类型的实例。如不可转换,抛出ClassCastException异常,否则继续执行。另外还有一点需要注意,该指令只能用于转换成引用类型,不能用于转换成基本类型。

    • instance-of/jumbo vAAAA, vBBBB, type@CCCCCCCC:指令功能与“instance-of vA, vB, type@CCCC”相同,只是寄存器值与指令的索引取值范围更大。

    • new-instance/jumbo vAAAA, type@BBBBBBBB:指令功能与“new-instance vAA, type@BBBB”相同,只是寄存器值与指令的索引取值范围更大。

    • 数组操作指令

    • array-length vA, vB:计算vB寄存器中数组引用的元素长度并将长度存入vA寄存器。

    • new-array vA, vB, type@CCCC:根据指定类型或类型ID(type@CCCC)与大小(vB寄存器存入数组的长度)构造一个数组,并将数组的引用赋给vA寄存器。

    • filled-new-array {vC, vD, vE, vF, vG}, type@BBBB:根据指定类型或类型ID(type@BBBB)与大小(vA)构造一个数组并填充数组内容,vA寄存器是隐含使用的,除了指定数组的大小外还指定了参数的个数,vC~vG是使用到的参数寄存器序号。指令会将寄存器 vC 到 vG 中的值填充到新数组中,并将新数组引用保存到寄存器 vA 中。还有一点需要注意,filled-new-array 指令创建的数组是一个新的对象,它与原数组没有任何关系。另外,该指令只能用于创建引用类型的数组,不能用于创建基本类型的数组。

    • filled-new-array/range {vCCCC .. vNNNN}, type@BBBB:filled-new-array/range 指令与 filled-new-array 指令非常类似,只是它的参数寄存器是以范围形式给出的,vCCCC 和 vNNNN 分别表示参数寄存器的起始位置和结束位置,type@BBBB 表示数组元素的类型。vC 是 filled-new-array/range 指令的第一个参数寄存器,表示数组大小。N=A+C-1 表示参数寄存器的结束位置,其中 A 表示数组大小,C 表示第一个参数寄存器的编号。因为 filled-new-array/range 指令的参数寄存器是连续的一段,所以 N 的值等于 A+C-1,即参数寄存器的结束位置。例如,如果 filled-new-array/range {v2 .. v5}, type@BBBB 指令被执行,那么数组大小为 v2 中存储的整数值,参数寄存器的编号为 v2、v3、v4、v5,因此 N=A+C-1=4+v2-1=3+v2。

    • fill-array-data vAA, 偏移量:用指定的字面量数组数据填充到目标数组中,字面量数组数据的位址是当前指令位址加偏移量的和。该指令填充的数组类型必须为基本类型的数组和字符串数组,其中基本类型包括 boolean、byte、short、char、int、float 和 double。填充数组时,字面量数组数据的每个元素都必须与目标数组的元素类型相同。另外,该指令只能用于填充已经创建的目标数组,不能用于创建新的数组对象。

      举个例子,假设有如下代码:

...
fill-array-data v0, :array_0  // 将字面量数组数据填充到 v0 寄存器所代表的数组中,:array_0为字面量数组数据的偏移地址(偏移量)
...
:array_0
  .array-data 2            // 表示每个数组元素占用两个字节
      0x14ebs             // 数组元素值为 0x14eb(short 类型)
      0x15f0s             // 数组元素值为 0x15f0(short 类型)
      0x15c7s             // 数组元素值为 0x15c7(short 类型)
      0x15c7s             // 数组元素值为 0x15c7(short 类型)
      0x15c7s             // 数组元素值为 0x15c7(short 类型)
      0x15c7s             // 数组元素值为 0x15c7(short 类型)
      0x15c7s             // 数组元素值为 0x15c7(short 类型)
      0x15c7s             // 数组元素值为 0x15c7(short 类型)
...

  • 异常指令

  • throw vAA:抛出异常对象,异常对象的引用在vAA寄存器。

  • 跳转指令(重点)

  • 无条件跳转指令

    • goto 目标:通过短偏移量无条件跳转到目标。
  • 分支跳转指令

    • packed-switch vAA, 索引表偏移量:实现一个switch语句,case常量是连续的,这个指令使用索引表,vAA是在表中找到具体case的指令偏移量的索引,如果无法在表中找到vAA对应的索引将继续执行下一个指令(即default case),执行完case中的代码后,如果Java源代码设置了break强制跳出switch,就会无条件跳转到所有case的出口处,如果没有强制跳出switch,就会执行完default case中的代码后再跳转到所有case的出口处继续执行代码。

    举个例子,假设有如下代码:

    Java代码:

switch (this.mType) {
            case 1:
                return Math.signum(0.5d - (getP(d) % 1.0d));
            case 2:
                abs = Math.abs((((getP(d) * 4.0d) + 1.0d) % 4.0d) - 2.0d);
                break;
            case 3:
                return (((getP(d) * 2.0d) + 1.0d) % 2.0d) - 1.0d;
            case 4:
                abs = ((getP(d) * 2.0d) + 1.0d) % 2.0d;
                break;
            case 5:
                return Math.cos(this.PI2 * getP(d));
            case 6:
                double abs2 = 1.0d - Math.abs(((getP(d) * 4.0d) % 4.0d) - 2.0d);
                abs = abs2 * abs2;
                break;
            default:
                return Math.sin(this.PI2 * getP(d));
        }

 smali代码:

packed-switch v0, :pswitch_data_5e   # 带着v0寄存器中的值跳转到索引表偏移量:pswitch_data_5e位址寻找对应的case指令偏移量的索引

    .line 120
    # default分支语句
    iget-wide v0, p0, Landroidx/constraintlayout/motion/utils/Oscillator;->PI2:D

    invoke-virtual {p0, p1, p2}, Landroidx/constraintlayout/motion/utils/Oscillator;->getP(D)D

    move-result-wide p1

    mul-double/2addr v0, p1

    invoke-static {v0, v1}, Ljava/lang/Math;->sin(D)D

    move-result-wide p1

    return-wide p1

    .line 132
    :pswitch_17 # case 6
    invoke-virtual {p0, p1, p2}, Landroidx/constraintlayout/motion/utils/Oscillator;->getP(D)D

    move-result-wide p1

    mul-double/2addr p1, v1

    rem-double/2addr p1, v1

    sub-double/2addr p1, v3

    invoke-static {p1, p2}, Ljava/lang/Math;->abs(D)D

    move-result-wide p1

    sub-double p1, v5, p1

    mul-double/2addr p1, p1

    :goto_25  # 所有case的出口,也可以说是在case中执行break后跳转到此处
    sub-double/2addr v5, p1

    return-wide v5

    .line 130
    :pswitch_27 # case 5
    iget-wide v0, p0, Landroidx/constraintlayout/motion/utils/Oscillator;->PI2:D

    invoke-virtual {p0, p1, p2}, Landroidx/constraintlayout/motion/utils/Oscillator;->getP(D)D

    move-result-wide p1

    mul-double/2addr v0, p1

    invoke-static {v0, v1}, Ljava/lang/Math;->cos(D)D

    move-result-wide p1

    return-wide p1

    .line 128
    :pswitch_33 # case 4
    invoke-virtual {p0, p1, p2}, Landroidx/constraintlayout/motion/utils/Oscillator;->getP(D)D

    move-result-wide p1

    mul-double/2addr p1, v3

    add-double/2addr p1, v5

    rem-double/2addr p1, v3

    goto :goto_25 # 跳转到偏移量为goto_25的目标位址

    .line 126
    :pswitch_3b # case 3
    invoke-virtual {p0, p1, p2}, Landroidx/constraintlayout/motion/utils/Oscillator;->getP(D)D

    move-result-wide p1

    mul-double/2addr p1, v3

    add-double/2addr p1, v5

    rem-double/2addr p1, v3

    sub-double/2addr p1, v5

    return-wide p1

    .line 124
    :pswitch_44 # case2
    invoke-virtual {p0, p1, p2}, Landroidx/constraintlayout/motion/utils/Oscillator;->getP(D)D

    move-result-wide p1

    mul-double/2addr p1, v1

    add-double/2addr p1, v5

    rem-double/2addr p1, v1

    sub-double/2addr p1, v3

    invoke-static {p1, p2}, Ljava/lang/Math;->abs(D)D

    move-result-wide p1

    goto :goto_25 # 跳转到偏移量为goto_25的目标位址

    :pswitch_51 # case 1
    const-wide/high16 v0, 0x3fe0000000000000L    # 0.5

    .line 122
    invoke-virtual {p0, p1, p2}, Landroidx/constraintlayout/motion/utils/Oscillator;->getP(D)D

    move-result-wide p1

    rem-double/2addr p1, v5

    sub-double/2addr v0, p1

    invoke-static {v0, v1}, Ljava/lang/Math;->signum(D)D

    move-result-wide p1

    return-wide p1

    :pswitch_data_5e
    .packed-switch 0x1 # 索引表,case常量从1开始,依次递增
        :pswitch_51 # case 1
        :pswitch_44 # case 2
        :pswitch_3b # case 3
        :pswitch_33 # case 4
        :pswitch_27 # case 5
        :pswitch_17 # case 6
    .end packed-switch
  • parse-switch vAA, 查询表偏移量:实现一个switch语句,case常量是非连续的,这个指令使用查询表,用于表示case常量和每个case常量的偏移量。如果vAA寄存器中的值无法在表中匹配将继续执行下一个指令(即default case)。执行完case中的代码后,如果Java源代码设置了break强制跳出switch,就会无条件跳转到所有case的出口处,如果没有强制跳出switch,就会执行完default case中的代码后再跳转到所有case的出口处继续执行代码。

举个例子,假设有如下代码:

Java代码:

switch (transit) {
            case 4097:
                return 8194;
            case 4099:
                return 4099;
            case 8194:
                return 4097;
            default:
                return 0;
        }

smali代码:

sparse-switch p0, :sswitch_data_e  # sparse-switch指令,传入寄存器p0的值作为查找表的索引,:sswitch_data_e是查询表的偏移量

    .line 2025                    # 源代码的行号
    :goto_4                       # 这里是所有case的出口,如果case常量没匹配上查询表中的值,那么v0寄存器中的值就为0,这里就返回零,也就是default下的代码return 0;
    return v0                     # 返回v0寄存器中的值

    .line 2016                    # 源代码的行号
    :sswitch_5                    # 标签,对应表中的第一个case常量0x1001
    const/16 v0, 0x2002           # 将0x2002赋值给v0寄存器                 
    .line 2017                    # 源代码的行号
    goto :goto_4                  # 无条件跳转到:goto_4标签处执行

    .line 2019                    # 源代码的行号
    :sswitch_8                    # 标签,对应表中的第三个case常量0x2002
    const/16 v0, 0x1001           # 将0x1001赋值给v0寄存器       
    .line 2020                    # 源代码的行号 
    goto :goto_4                  # 无条件跳转到:goto_4标签处执行 

    .line 2022                    # 源代码的行号                       
    :sswitch_b                    # 标签,对应表中的第二个case常量0x1003
    const/16 v0, 0x1003           # 将0x1003赋值给v0寄存器 
    goto :goto_4                  # 无条件跳转到:goto_4标签处执行

    .line 2014                    # 源代码的行号
    :sswitch_data_e               # 标签,作为查询表的偏移量
    .sparse-switch                # 表示接下来是一个查询表
        0x1001 -> :sswitch_5      # case常量0x1001跳转到标签:sswitch_5处执行
        0x1003 -> :sswitch_b      # case常量0x1003跳转到标签:sswitch_b处执行
        0x2002 -> :sswitch_8      # case常量0x2002跳转到标签:sswitch_8处执行
    .end sparse-switch            # 标志着查询表的结束

条件跳转指令

  • if-eq vA, vB, 目标:如果vA == vB,跳转到目标。

    if-ne vA, vB, 目标:如果vA != vB,跳转到目标。

    if-lt vA, vB, 目标:如果vA < vB,跳转到目标。

    if-ge vA, vB, 目标:如果vA >= vB,跳转到目标。

    if-gt vA, vB, 目标:如果vA > vB,跳转到目标。

    if-le vA, vB, 目标:如果vA <= vB,跳转到目标。

    if-eqz vA, 目标:如果vA == 0,跳转到目标。

    if-nez vA, 目标:如果vA != 0,跳转到目标。

    if-ltz vA, 目标:如果vA < 0,跳转到目标。

    if-gez vA, 目标:如果vA >= 0,跳转到目标。

    if-gtz vA, 目标:如果vA > 0,跳转到目标。

    if-lez vA, 目标:如果vA <= 0,跳转到目标。

  • 比较指令

  • cmpl-float vAA,vBB,vCC:比较vBB和vCC的float值并在vAA存入int型返回值。非数值默认为小于,如果参数为非数值将返回-1。如果vBB寄存器大于vCC寄存器,则结果为-1,相等则结果为0,小于则结果为1。

  • cmpg-float vAA,vBB,vCC:比较vBB和vCC的float值并在vAA存入int型返回值。非数值默认为大于,如果参数为非数值将返回1。如果vBB寄存器大于vCC寄存器,则结果为1,相等则结果为0,小于则结果为-1。

  • cmpl-double vAA,vBB,vCC:比较vBB和vCC的double值并在vAA存入int型返回值。非数值默认为小于,如果参数为非数值将返回-1。如果vBB,vBB+1寄存器的值大于vCC,vCC+1寄存器的值,则结果为-1,相等则结果为0,小于则结果为1。

  • cmpg-double vAA,vBB,vCC:比较vBB和vCC的double值并在vAA存入int型返回值。非数值默认为大于,如果参数为非数值将返回1。如果vBB,vBB+1寄存器的值大于vCC,vCC+1寄存器的值,则结果为1,相等则结果为0,小于则结果为-1。

  • cmp-long vAA,vBB,vCC:比较vBB和vCC的long值并在vAA存入int型返回值。如果vBB寄存器大于vCC寄存器,则结果为1,相等则结果为0,小则结果为-1。

  • 字段操作指令

  • iput-object vAA,vBB,字段ID:根据字段ID将vAA寄存器的值存入实例的对象引用字段,vBB寄存器中是该实例的引用。例如:

iput-object v0, p0, Lbin/mt/apksignaturekillerplus/HookApplication;->appPkgName:Ljava/lang/String;

  • 可以看到例子中将v0寄存器中Ljava/lang/String;类型的值(实例的对象引用)存入到p0寄存器中,p0寄存器中是Lbin/mt/apksignaturekillerplus/HookApplication;类型下字段名为appPkgName的实例字段,并且实例字段appPkgName的存储值类型为Ljava/lang/String;

  • iput-boolean vAA,字段ID:根据字段ID将vAA寄存器的值存入实例的boolean型字段,vBB寄存器中是该实例的引用。例如:

iput-boolean p1, p0, Lbin/mt/plugin/api/translation/TranslationEngine$Configuration;->acceptTranslated:Z

  • 可以看到例子中将p1寄存器中boolean型的值存入到p0寄存器中,p0寄存器中是Lbin/mt/plugin/api/translation/TranslationEngine$Configuration;类型下字段名为acceptTranslated的实例字段,并且实例字段acceptTranslated的存储值类型为boolean。

  • iput-wide vAA,vBB,字段ID:根据字段ID将vAA,vAA+1寄存器的值存入实例的double/long型字段,vBB寄存器中是该实例的引用。例如:

iput-wide v1, p0, Lcom/alipay/android/phone/mrpc/core/l;->e:J

  • 可以看到例子中将v1,v2寄存器中long型的值存入到p0寄存器中,p0寄存器中是Lcom/alipay/android/phone/mrpc/core/l;类型下字段名为e的实例字段,并且实例字段e的存储值类型为long。

  • iput vAA,vBB,字段ID:根据字段ID将vAA寄存器的值存入实例的int型字段,vBB寄存器中是该实例的引用。例如:

iput v0, p0, Lcom/alipay/android/phone/mrpc/core/e;->a:I

  • 可以看到例子中将v0寄存器中int型的值存入到p0寄存器中,p0寄存器中是Lcom/alipay/android/phone/mrpc/core/e;类型下字段名为a的实例字段,并且实例字段a的存储值类型为boolean。

  • iget-object vAA,vBB,字段ID:根据字段ID读取一个实例的对象引用字段到vAA,vBB寄存器中是该实例的引用。例如:

iget-object v4, p0, Lbin/mt/apksignaturekillerplus/HookApplication;->appPkgName:Ljava/lang/String;

  • 可以看到例子中将p0寄存器中Ljava/lang/String;类型的值(实例的对象引用)存入到v4寄存器中,p0寄存器中是Lbin/mt/apksignaturekillerplus/HookApplication;类型下字段名为appPkgName的实例字段,并且实例字段appPkgName的存储值类型为Ljava/lang/String;

  • iget-boolean vAA,vBB,字段ID:根据字段ID读取实例的boolean型字段到vAA,vBB寄存器中是该实例的引用。例如:

iget-boolean v1, p0, Lbin/mt/plugin/api/translation/TranslationEngine$ConfigurationBuilder;->acceptTranslated:Z

  • 可以看到例子中将p0寄存器中boolean型的值存入到v1寄存器中,p0寄存器中是Lbin/mt/plugin/api/translation/TranslationEngine$ConfigurationBuilder;类型下字段名为acceptTranslated的实例字段,并且实例字段acceptTranslated的存储值类型为boolean。

  • iget-wide vAA,vBB,字段ID:根据字段ID读取实例的double/long型字段到vAA,vAA+1,vBB寄存器中是该实例的引用。例如:

iget-wide v3, p0, Lcom/alipay/android/phone/mrpc/core/l;->g:J

  • 可以看到例子中将p0寄存器中long型的值存入到v3,v4寄存器中,p0寄存器中是Lcom/alipay/android/phone/mrpc/core/l;类型下字段名为g的实例字段,并且实例字段g的存储值类型为long。

  • iget vAA,vBB,字段ID:根据字段ID读取实例的int型字段到vAA,vBB寄存器中是该实例的引用。例如:

iget v1, v8, Landroid/util/TypedValue;->data:I

  • 可以看到例子中将v8寄存器中int型的值存入到v1寄存器中,v8寄存器中是Landroid/util/TypedValue;类型下字段名为data的实例字段,并且实例字段data的存储值类型为int。

  • sput-object vAA,字段ID:根据字段ID将vAA寄存器中的对象引用赋值到对象引用静态字段。例如:

sput-object v0, Lbin/tools/inject/InjectedLog;->TIME_FORMAT1:Ljava/text/SimpleDateFormat;

  • 可以看到例子中将v0寄存器中Ljava/text/SimpleDateFormat;类型的值(对象引用)存入到Lbin/tools/inject/InjectedLog;类型下的存储值为Ljava/text/SimpleDateFormat;类型的对象引用静态字段TIME_FORMAT1中。

  • sput-boolean vAA,字段ID:根据字段ID将vAA寄存器中的值赋值到boolean型静态字段。例如:

sput-boolean v6, Lcom/alipay/sdk/cons/a;->r:Z

  • 可以看到例子中将v6寄存器中boolean类型的值存入到Lcom/alipay/sdk/cons/a;类型下的存储值为boolean类型的静态字段r中。

  • sput-wide vAA,字段ID:根据字段ID将vAA,vAA+1寄存器中的值赋值到double/long型静态字段。例如:

sput-wide v0, Lcom/alipay/sdk/app/PayTask;->i:J

  • 可以看到例子中将v0,v1寄存器中long类型的值存入到Lcom/alipay/sdk/app/PayTask;类型下的存储值为long类型的静态字段i中。

  • sput vAA,字段ID:根据字段ID将vAA寄存器中的值赋值到int型静态字段。例如:

sput v0, Lcom/google/android/material/appbar/AppBarLayout;->DEF_STYLE_RES:I

  • 可以看到例子中将v0寄存器中int类型的值存入到Lcom/google/android/material/appbar/AppBarLayout;类型下的存储值为int类型的静态字段DEF_STYLE_RES中。

  • sget-object vAA,字段ID:根据字段ID读取静态对象引用字段到vAA。例如:

sget-object v17, Ljava/lang/System;->out:Ljava/io/PrintStream;

  • 可以看到例子中将Ljava/lang/System;类型下的静态字段out中的Ljava/io/PrintStream;类型的值(对象引用)存入v17寄存器中。

  • sget-boolean vAA,字段ID:根据字段ID读取静态boolean型字段到vAA。例如:

sget-boolean v1, Lbin/mt/plus/Features;->ۘf:Z

  • 可以看到例子中将Lbin/mt/plus/Features;类型下的静态字段f中的boolean类型的值存入v1寄存器中。

  • sget-wide vAA,字段ID:根据字段ID读取静态double/long型字段到vAA,vAA+1。例如:

sget-wide v2, Lcom/alipay/android/phone/mrpc/core/b;->a:J

  • 可以看到例子中将Lcom/alipay/android/phone/mrpc/core/b;类型下的静态字段a中的long类型的值存入v2,v3寄存器中。

  • sget vAA,字段ID:根据字段ID读取静态int型字段到vAA。例如:

sget v0, Landroid/os/Build$VERSION;->SDK_INT:I

  • 可以看到例子中将Landroid/os/Build$VERSION;类型下的静态字段SDK_INT中的int类型的值存入v0寄存器中。

方法调用指令(重点)

  • invoke-virtual {参数},方法名:调用带参数的实例的虚方法(虚方法相当于Java中的普通方法)。

  • invoke-virtual/range {vAA .. vBB},方法名:调用以寄存器范围为参数的虚方法。该指令第一个寄存器和寄存器的数量将会传递给方法。该指令与普通的invoke-virtual指令相比,可以同时传递多个参数,提高了效率。例如:

sget-object v17, Ljava/lang/System;->out:Ljava/io/PrintStream;

const-string/jumbo v18, "PmsHook success."

invoke-virtual/range {v17 .. v18}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V

  • 在这条指令中,寄存器范围为{v17 .. v18},调用java.io.PrintStream类的println(String)方法,该方法需要一个参数,参数类型为java.lang.String,并且不返回任何值。该指令的第一个寄存器为v17,寄存器的数量为2,因此v17和v18两个寄存器都将作为参数传递给方法,其中v18存储了传递给println()方法的参数值,v17则存储了PrintStream对象的引用。在执行该指令之前,需要先将参数值存储在寄存器v18中,将PrintStream对象的引用存储在寄存器v17中。该虚方法的作用是将参数值打印到控制台上。在Dalvik字节码中,方法的参数和返回值都是通过寄存器进行传递的。

  • invoke-super {参数},方法名:调用带参数的直接父类的虚方法。

  • invoke-super/range  {vAA .. vBB},方法名:调用以寄存器范围为参数的直接父类的虚方法。该指令第一个寄存器和寄存器的数量将会传递给方法。

  • invoke-direct {参数},方法名:不解析直接调用带参数的方法,例如构造方法、静态语句块。

  • invoke-direct/range {vAA .. vBB},方法名:不解析直接调用以寄存器范围为参数的方法。该指令第一个寄存器和寄存器的数量将会传递给方法。

  • invoke-static {参数},方法名:调用带参数的静态方法。

  • invoke-static/range {vAA .. vBB},方法名:调用以寄存器范围为参数的静态方法。该指令第一个寄存器和寄存器的数量将会传递给方法。

  • invoke-interface {参数},方法名:调用带参数的接口方法。

  • invoke-interface/range {vAA .. vBB},方法名:调用以寄存器范围为参数的接口方法。该指令第一个寄存器和寄存器的数量将会传递给方法。

  • 数据转换指令

  • neg-int:对整型数求补。取补指令是按位取反后加1,即将二进制数中的0变为1,1变为0,然后再加1。在Dalvik指令集中,取补指令的格式为“neg vA, vB”,表示将vB寄存器中的值按位取反后加1后存储到vA寄存器中。

  • not-int:对整型数求反。取反指令是按位取反,即将二进制数中的0变为1,1变为0。在Dalvik指令集中,取反指令的格式为“not vA, vB”,表示将vB寄存器中的值按位取反后存储到vA寄存器中。

  • neg-long:对长整型数求补。

  • not-long:对长整型数求反。

  • neg-float:对单精度浮点型数求补。

  • neg-double:对双精度浮点型数求补。

  • int-to-byte:将整型数转换为字节型。格式为“int-to-byte vA, vB”,表示将vB寄存器中的整型数转换为字节型后存放到vA寄存器中。需要注意的是,由于字节型数的范围是-128到127,因此在转换时可能会出现溢出情况。

  • int-to-char:将整型数转换为字符型。格式为“int-to-char vA, vB”,表示将vB寄存器中的整型数转换为字符型后存放到vA寄存器中。因为char类型是无符号的,大小为两个字节,所以可以表示的最大值就是2^16-1=65535。字符型数的范围是0到65535,在转换时可能会出现溢出情况。

  • int-to-short:将整型数转换为短整型。格式为“int-to-short vA, vB”,表示将vB寄存器中的整型数转换为短整型后存放到vA寄存器中。短整型数的范围是-32768到32767,因此在转换时可能会出现溢出情况。

  • long-to-int:将长整型数转换为整型数。格式为“long-to-int vA, vB”,表示将vB寄存器中的长整型数转换为整型数后存放到vA寄存器中。需要注意的是,由于长整型数的范围比整型数大,因此在转换时可能会出现溢出情况。

  • long-to-float:将长整型数转换为单精度浮点型数。格式为“long-to-float vA, vB”,表示将vB寄存器中的长整型数转换为单精度浮点型数后存放到vA寄存器中。

  • long-to-double:将长整型数转换为双精度浮点型数。格式为“long-to-double vA, vB”,表示将vB寄存器中的长整型数转换为双精度浮点型数后存放到vA寄存器中。

  • float-to-int:将单精度浮点型数转换为整型数。格式为“float-to-int vA, vB”,表示将vB寄存器中的单精度浮点型数转换为整型数后存放到vA寄存器中。需要注意的是,由于单精度浮点型数的范围比整型数大,因此在转换时可能会出现溢出情况。

  • float-to-long:将单精度浮点型数转换为长整型数。格式为“float-to-long vA, vB”,表示将vB寄存器中的单精度浮点型数转换为长整型数后存放到vA寄存器中。

  • float-to-double:将单精度浮点型数转换为双精度浮点型数。格式为“float-to-double vA, vB”,表示将vB寄存器中的单精度浮点型数转换为双精度浮点型数后存放到vA寄存器中。

  • double-to-int:将双精度浮点型数转换为整型数。格式为“double-to-int vA, vB”,表示将vB寄存器中的双精度浮点型数转换为整型数后存放到vA寄存器中。需要注意的是,由于双精度浮点型数的范围比整型数大,因此在转换时可能会出现溢出情况。

  • double-to-long:将双精度浮点型数转换为长整型数。格式为“double-to-long vA, vB”,表示将vB寄存器中的双精度浮点型数转换为长整型数后存放到vA寄存器中。

  • double-to-float:将双精度浮点型数转换为单精度浮点型数。格式为“double-to-float vA, vB”,表示将vB寄存器中的双精度浮点型数转换为单精度浮点型数后存放到vA寄存器中。需要注意的是,由于单精度浮点型数的范围比双精度浮点型数小,因此在转换时可能会出现精度损失的情况。

  • int-to-byte:将整型数转换为字节型。格式为“int-to-byte vA, vB”,表示将vB寄存器中的整型数转换为字节型后存放到vA寄存器中。

  • int-to-char:将整型数转换为字符型。格式为“int-to-char vA, vB”,表示将vB寄存器中的整型数转换为字符型后存放到vA寄存器中。

  • int-to-short:将整型数转换为短整型。格式为“int-to-short vA, vB”,表示将vB寄存器中的整形数转换为短整形后存放到vA寄存器中。

  • 数据运算指令

  • add-type:进行加法运算。格式为“add-int vA, vB, vC”,表示将vB寄存器中的整型数与vC寄存器中的整型数相加,结果存放到vA寄存器中。

  • sub-type:进行减法运算。格式为“sub-int vA, vB, vC”,表示将vB寄存器中的整型数减去vC寄存器中的整型数,结果存放到vA寄存器中。

  • mul-type:进行乘法运算。格式为“mul-int vA, vB, vC”,表示将vB寄存器中的整型数与vC寄存器中的整型数相乘,结果存放到vA寄存器中。

  • div-type:进行除法运算。格式为“div-int vA, vB, vC”,表示将vB寄存器中的整型数除以vC寄存器中的整型数,结果存放到vA寄存器中。

  • rem-type:进行取余运算。格式为“rem-int vA, vB, vC”,表示将vB寄存器中的整型数除以vC寄存器中的整型数的余数,结果存放到vA寄存器中。

  • and-type:进行按位与运算。格式为“and-int vA, vB, vC”,表示将vB寄存器中的整型数与vC寄存器中的整型数进行按位与运算,结果存放到vA寄存器中。

  • or-type:进行按位或运算。格式为“or-int vA, vB, vC”,表示将vB寄存器中的整型数与vC寄存器中的整型数进行按位或运算,结果存放到vA寄存器中。

  • xor-type:进行按位异或运算。格式为“xor-int vA, vB, vC”,表示将vB寄存器中的整型数与vC寄存器中的整型数进行按位异或运算,结果存放到vA寄存器中。

  • shl-type:有符号左移。格式为“shl-type vA, vB, vC”,表示将vB寄存器中的有符号整型数左移vC位,结果存放到vA寄存器中。左移时,低位补0,高位舍弃。

  • shr-type:有符号右移。格式为“shr-type vA, vB, vC”,表示将vB寄存器中的有符号整型数右移vC位,结果存放到vA寄存器中。右移时,高位补符号位,低位舍弃。

  • ushr-type:无符号右移。格式为“ushr-type vA, vB, vC”,表示将vB寄存器中的无符号整型数右移vC位,结果存放到vA寄存器中。右移时,高位补0,低位舍弃。

 smali文件

无论是普通类、抽象类、接口类还是内部类,在反编译出来的代码中,它们都是以单独的smali文件进行存放的。每个smali文件中的语句都遵循着一套语法规范,这里想要讲解的正是这套smali文件中的语法规范。

  • Smali中开头的包信息以及包信息格式:

    .class <访问权限> [修饰关键字] <类名>

    .super <父类名>

    .source <源文件名>

例如:

.class public interface abstract Landroid/support/v4/os/IResultReceiver;
// 这个类是android.support.v4.os这个包下名为IResultReceiver的接口类
.super Ljava/lang/Object; // 该类继承了父类java.lang.Object
.source "IResultReceiver.java" // 这个smali文件在还是Java文件时,文件名为IResultReceiver.java

 静态字段定义

**.field <访问权限> static [修饰关键字] <字段名>:<字段类型>**

例如:

.field private static final PARAMETER_BUFFER:Ljava/lang/ThreadLocal;

实例字段定义(java中的普通字段)

相比起静态字段定义,实例字段就少了一个静态修饰符static而已,格式:

.field <访问权限> [修饰关键字] <字段名>:<字段类型>

例如:

.field public final mConstructorArgs:[Ljava/lang/Object;

直接方法定义

# direct methods
.method <访问权限> [修饰关键字] <方法名称、参数还有返回值>
  <.registers>  // 指定了方法中寄存器的总数,这个数量是参数寄存器和本地寄存器的总和。
  <.local>  // 这个指令表明方法中非参数寄存器(本地寄存器)的个数
  [.param]  // 表明了方法的参数,每个.param指令表示一个参数,方法使用了几个参数就有几个.param指令。
  [.prologue]  // 表明了方法中代码的开始处
  [.line]  // 表名了该处代码在源代码中的行号
  <代码体>
.end method

例如:

.method public static spatialSampling(Landroid/gesture/Gesture;IZ)[F
// 定义一个公共静态方法,返回一个浮点数数组
// 方法名为spatialSampling,参数为Gesture对象、整型bitmapSize和布尔型keepAspectRatio
  .locals 0
  // 定义本地寄存器数为0

  .param p0, "gesture"    # Landroid/gesture/Gesture;
  // 定义一个参数p0,类型为Gesture,表示手势对象
  .param p1, "bitmapSize"    # I
  // 定义一个参数p1,类型为整型,表示位图大小
  .param p2, "keepAspectRatio"    # Z
  // 定义一个参数p2,类型为布尔型,表示是否保持宽高比

  new-instance p0, Ljava/lang/RuntimeException;
  // 创建一个RuntimeException对象,将其引用赋值给p0

  invoke-direct {p0}, Ljava/lang/RuntimeException;->()V
  // 调用RuntimeException对象的构造方法,初始化该对象

  throw p0
  // 抛出RuntimeException对象

.end method
// 方法结束

 虚方法(java中的普通方法)

# virtual methods
.method <访问权限> [修饰关键字] <方法名称、参数还有返回值>
  <.registers>  // 指定了方法中寄存器的总数,这个数量是参数寄存器和本地寄存器的总和。
  <.local>  // 这个指令表明方法中非参数寄存器(本地寄存器)的个数
  [.param]  // 表明了方法的参数,每个.param指令表示一个参数,方法使用了几个参数就有几个.param指令。
  [.prologue]  // 表明了方法中代码的开始处
  [.line]  // 表名了该处代码在源代码中的行号
  <代码体>
.end method

除了顶头的注释和直接方法不同,其他没什么区别,我们来看一个例子,例如:

# virtual methods
// 定义虚方法

.method public final createConnectionKeepAliveStrategy()Lorg/apache/http/conn/ConnectionKeepAliveStrategy;
// 方法名为createConnectionKeepAliveStrategy,返回值类型为ConnectionKeepAliveStrategy对象
// final关键字表示该方法不能被子类重写
  .locals 1
  // 定义本地寄存器数为1

  new-instance v0, Lcom/alipay/android/phone/mrpc/core/f;
  // 创建一个com.alipay.android.phone.mrpc.core.f类的实例对象,并将其引用赋值给v0

  invoke-direct {v0, p0}, Lcom/alipay/android/phone/mrpc/core/f;->(Lcom/alipay/android/phone/mrpc/core/d;)V
  // 调用v0对象的构造方法,将p0对象作为参数传入。
  // 这里要注意v0寄存器中存放的是com.alipay.android.phone.mrpc.core.f类的实例对象,也可以说是java中的this。

  return-object v0
  // 将v0对象返回
.end method
// 方法结束

接口

# interfaces

.implements <接口名>

例如:

# interfaces

.implements Ljava/lang/reflect/InvocationHandler;
// 当前类实现了接口Ljava/lang/reflect/InvocationHandler;

注解

在讲注解之前先解释一下什么是类型签名:在Java中,每个类、接口、数组、枚举、注解和基本类型都有一个唯一的类型签名。类型签名是一个字符串,用来描述该类型的全限定名、类型参数、维度等信息。类型签名的格式如下:

 ::=  |  | 
 ::= L  ; * 
 ::= [ 
 ::= T  ;
 ::= *
 ::=  | 
 ::= ? ?
 ::= extends  | super 
 ::=  | 

下面是类型签名的详细解释:

  1. :表示一个类型签名。它可以是
  2. :表示一个类类型。它的格式为L ; *,其中是类的全限定名,使用斜杠(/)分隔包名和类名,例如java/lang/String表示类的泛型参数,可以有多个。
  3. :表示一个数组类型。它的格式为[ ,其中是数组元素的类型。
  4. :表示一个类型变量。它的格式为T ;,其中是类型变量的标识符。
  5. :表示一个或多个类型参数。它的格式为*,即可以有零个或多个类型参数。
  6. :表示一个类型参数。它可以是
  7. :表示一个通配符类型参数。它的格式为? ?,其中表示通配符的上界或下界。
  8. :表示通配符的上界或下界。它可以是extends super ,表示通配符的上界或下界是某个参考类型。
  9. :表示一个参考类型。它可以是

在Smali代码中,类型签名的格式与Java基本一致,只是用L来表示类类型,用[来表示数组类型,用T来表示类型变量。例如,类型签名Ljava/lang/Deprecated表示的是Java内置类Deprecated的类型签名。

注解的格式:

# annotation
.annotation [注解属性] <注解类名>
  [注解字段 = 值]
.end annotation

在Smali中,可以使用注解来提供额外的信息和指令给编译器和虚拟机。

以下是Smali注解的一些常见规范和详解:

  1. .annotation:用于声明一个注解类型。语法为.annotation {},其中可以是publicprotectedprivatepackage是注解类型的全限定名,是注解的元素。
  2. .subannotation:用于声明一个嵌套注解类型。语法为.subannotation {},与.annotation类似。
  3. .end annotation:用于结束一个注解类型的声明。
  4. .enum:用于声明一个枚举类型。语法为.enum {},其中可以是publicprotectedprivatepackage是枚举类型的全限定名,是枚举的元素。
  5. .end enum:用于结束一个枚举类型的声明。
  6. .field:用于为字段添加注解。语法为.field {},其中是字段的声明,是字段的注解。
  7. .method:用于为方法添加注解。语法为.method {},其中是方法的声明,是方法的注解。
  8. .parameter:用于为方法参数添加注解。语法为.parameter ,其中是参数的索引,从0开始,是参数的注解。
  9. .prologue:用于指定方法的前导代码。
  10. .line:用于指定源代码中的行号。
  11. .end method:用于结束一个方法的声明。
  12. .catch:用于捕获异常。语法为.catch {},其中是异常类型的全限定名,是捕获异常后的处理代码。
  13. .catchall:用于捕获所有异常。语法为.catchall {},其中是捕获异常后的处理代码。
  14. .locals:用于指定方法中的本地变量数量。语法为.locals ,其中是本地变量的数量。
  15. .end:用于结束一个代码块或注解的声明。

注解的作用范围可以是类、方法或者字段。

如果注解的作用范围是类,".annotation"指令会直接定义在smali文件中,例如:

.class public interface abstract annotation Ll/۬ۘ;
.super Ljava/lang/Object;
.source "H5TW"

# annotation
// 定义注解
.annotation runtime Ljava/lang/annotation/Retention;
// 定义一个运行时注解Retention
// runtime表示该注解在运行时可见,即可以通过反射获取该注解信息
// Ljava/lang/annotation/Retention表示Retention注解的类型签名
  value = .enum Ljava/lang/annotation/RetentionPolicy;->CLASS:Ljava/lang/annotation/RetentionPolicy;
  // 枚举类型,表示Retention注解的保留策略为CLASS
  // .enum表示定义一个枚举类型
  // Ljava/lang/annotation/RetentionPolicy表示RetentionPolicy枚举的类型签名
  // ->表示枚举类型的访问符
  // CLASS表示RetentionPolicy枚举的一个值,表示注解保留在类文件中,但在运行时不可见
.end annotation
// 注解定义结束

如果是方法或字段,".annotation"指令则会包含在方法或字段中定义。

注解方法,例如:

.method public onTouch(Landroid/view/View;Landroid/view/MotionEvent;)Z
  .locals 0
  .annotation build Landroid/annotation/SuppressLint;
      // 定义一个编译时注解SuppressLint
      // build表示该注解是Android SDK提供的注解
      // Landroid/annotation/SuppressLint表示SuppressLint注解的类型签名
  value = {
      "ClickableViewAccessibility"
  }
  // 定义注解的属性value,表示需要忽略的lint警告类型
  // "ClickableViewAccessibility"表示需要忽略的lint警告类型,即点击事件缺少无障碍支持
.end annotation
// 注解定义结束

注解字段,例如:

.field public top:F
  .annotation runtime Ljava/lang/Deprecated;
      // 定义一个运行时注解Deprecated
      // runtime表示该注解在运行时可见,即可以通过反射获取该注解信息
      // Ljava/lang/Deprecated表示Deprecated注解的类型签名
  .end annotation
  // 注解定义结束

.end field

你可能感兴趣的:(编程&逆向,android,笔记)