返回介绍

1.7.2 Dalvik 指令集

发布于 2022-02-28 21:35:51 字数 37788 浏览 1035 评论 0 收藏 0

  • smali 语法
  • 更多资料
  • Dalvik 虚拟机

    Android 程序运行在 Dalvik 虚拟机中,它与传统的 Java 虚拟机不同,完全基于寄存器架构,数据通过直接通过寄存器传递,大大提高了效率。Dalvik 虚拟机属于 Android 运行时环境,它与一些核心库共同承担 Android 应用程序的运行工作。Dalvik 虚拟机有自己的指令集,即 smali 代码,下面会详细介绍它们。

    Dalvik 指令集

    指令格式

    Dalvik 指令语法由指令的位描述与指令格式标识来决定。

    位描述约定如下:

    • 每 16 位使用空格分隔。
    • 每个字母占 4 位,按照顺序从高字节到低字节排列。
    • 顺序采用 A~Z 的单个大写字母作为一个 4 位的操作码,op 表示一个 8 位的操作码。
    • ”∅“来表示这字段所有位为0值。

    指令格式约定如下:

    • 指令格式标识大多由三个字符组成,前两个是数字,最后一个是字母。
    • 第一个数字表示指令有多少个 16 位的字组成。
    • 第二个数字表示指令最多使用寄存器的个数。
    • 第三个字母为类型码,表示指令用到的额外数据的类型。

    寄存器

    Dalvik 寄存器都是 32 位的,如果是 64 位的数据,则使用相邻的两个寄存器来表示。

    寄存器有两种命名法:v 命名法和 p 命名法。如果一个函数使用到 M 个寄存器,其中有 N 个参数,那么参数会使用最后的 N 个寄存器,而局部变量使用从 v0 开始的前 M-N 个寄存器。在 v 命名法中,不管寄存器中是参数还是局部变量,都以 v 开头。而 p 命名法中,参数命名从 p0 开始,依次递增,在代码比较复杂的时候,使用 p 命名法可以清楚地区分开参数和局部变量,大多数工具使用的也是 p 命名法。

    类型、方法和字段

    Dalvik 字节码只有基本类型和引用类型两种。除了对象类型和数组类型是引用类型外,其余的都是基本类型:

    语法含义
    Vvoid
    Zboolean
    Bbyte
    Sshort
    Cchar
    Iint
    Jlong
    Ffloat
    Ddouble
    L对象类型
    [数组类型
    • 对象类型格式是 L<包名>/<类名>;,如 String 表示为 Ljava/lang/String;
    • 数组类型格式是 [ 加上类型,如 int[] 表示为 [Iint[][] 表示为 [[I

    Dalvik 使用方法名、类型参数和返回值来描述一个方法。方法格式如下:

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

    例如把下面的 Java 代码转换成 smali:

    # Java
    String method(int, int [][], int, String, Object[])
    
    # smali
    .method method(I[[IILjava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
    .end method
    

    字段格式如下:

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

    空操作指令

    空操作指令的助记符为 nop,值为 00,通常用于对齐代码。

    数据操作指令

    数据操作指令为 move,原型为 move destination, source

    • 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/from16vAA, vBBBBmove-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:将上一个 invoke 类型指令操作的单字非对象结果赋值给 vAA 寄存器
    • move-result-wide vAA:将上一个 invoke 类型指令操作的双字非对象结果赋值给 vAA 寄存器
    • move-result-object vAA:将上一个 invoke 类型指令操作的对象结果赋值给 vAA 寄存器
    • move-exception vAA:保存一个运行时发生的异常到 vAA 寄存器

    返回指令

    基础字节码为 return

    • return-void:从一个 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
    • const vAA, #+BBBBBBBB:将数值赋值给寄存器 vAA
    • const/high16 vAA, #+BBBB0000:将数值右边零扩展为 32 位后赋值给寄存器 vAA
    • const-wide/16 vAA, #+BBBB:将数值符号扩展为 64 位后赋值给寄存器 vAA
    • const-wide/32 vAA, #+BBBBBBBB:将数值符号扩展为 64 位后赋值给寄存器 vAA
    • const-wide vAA, #+BBBBBBBBBBBBBBBB:将数值赋给寄存器对 vAA
    • const-wide/high16 vAA, #+BBBB000000000000:将数值右边零扩展为 64 位后赋值给寄存器对 vAA
    • const-string vAA, string@BBBB:通过字符串索引构造一个字符串并赋值给寄存器 vAA
    • const-string/jumbo vAA, string@BBBBBBBB:通过字符串索(较大)引构造一个字符串并赋值给寄存器 vAA
    • const-class vAA, type@BBBB:通过类型索引获取一个类型引用并赋值给寄存器 vAA
    • const-class/jumbo vAAAA, type@BBBBBBBB:通过给定的类型索引获取一个类引用并赋值给寄存器 vAAAA。这条指令占用两个字节,值为 0x00ff

    锁指令

    用在多线程程序中对同一对象操作。

    • monitor-enter vAA:为指定的对象获取锁
    • monitor-exit vAA:释放指定的对象的锁

    实例操作指令

    • check-cast vAA, type@BBBB
    • check-cast/jumbo vAAAA, type@BBBBBBBB:将 vAA 寄存器中的对象引用转换成指定的类型,如果失败会抛出 ClassCastException 异常。如果类型 B 指定的是基本类型,对于非基本类型的 A 来说,运行始终会失败
    • instance-of vA, vB, type@CCCC
    • instance-of vAAAA, vBBBB, type@CCCCCCCC:判断 vB 寄存器中的对象引用是否可以转换成指定的类型,如果可以 vA 寄存器赋值为 1,否则 vA 寄存器赋值为 0
    • new-instance vAA, type@BBBB
    • new-instance vAAAA, type@BBBBBBBB:构造一个指定类型对象的新实例,并将对象引用赋值给 vAA 寄存器,类型符 type 指定的类型不能是数组类

    数组操作指令

    • array-length vA, vB:获取vB寄存器中数组的长度并将值赋给vA寄存器。
    • new-array vA, vB, type@CCCC
    • new-array/jumbo vAAAA, vBBBB, type@CCCCCCCC:构造指定类型(type@CCCCCCCC)与大小(vBBBB)的数组,并将值赋给 vAAAA 寄存器
    • filled-new-array {vC, vD, vE, vF, vG}, type@BBBB:构造指定类型(type@BBBB)和大小(vA)的数组并填充数组内容。vA 寄存器是隐含使用的,处理指定数组的大小外还指定了参数的个数,vC~vG 是使用的参数寄存器列表。
    • filled-new-array/range {vCCCC .. vNNNN}, type@BBBB:同上,只是参数寄存器使用 range 字节码后缀指定了取值范围,vC 是第一个参数寄存器,N=A+C-1。
    • fill-array-data vAA, +BBBBBBBB:用指定的数据来填充数组,vAA 寄存器为数组引用,引用必须为基础类型的数组,在指令后面紧跟一个数据表。
    • arrayop vAA, vBB, vCC:对 vBB 寄存器指定的数组元素进行取值和赋值。vCC 寄存器指定数组元素索引,vAA 寄存器用来存放读取的或需要设置的数组元素的值。读取元素使用 aget 类指令,元素赋值使用 aput 类指令。

    异常指令

    • throw vAA:抛出 vAA 寄存器中指定类型的异常

    跳转指令

    有三种跳转指令:无条件跳转(goto)、分支跳转(switch)和条件跳转(if)。

    • goto +AA
    • goto/16 +AAAA
    • goto/32 +AAAAAAAA:无条件跳转到指定偏移处,不能为 0
    • packed-switch vAA, +BBBBBBBB:分支跳转指令。vAA 寄存器为 switch 分支中需要判断的值,BBBBBBBB 指向一个 packed-switch-payload 格式的偏移表,表中的值是有规律递增的
    • sparse-switch vAA, +BBBBBBBB:分支跳转指令。vAA 寄存器为 switch 分支中需要判断的值,BBBBBBBB 指向一个 sparse-switch-payload 格式的偏移表,表中的值是无规律的偏移量
    • if-test vA, vB, +CCCC:条件跳转指令。比较 vA 寄存器与 vB 寄存器的值,如果比较结果满足就跳转到 CCCC 指定的偏移处,CCCC 不能为 0。if-test 类型的指令有:
      • if-eq:if(vA==vB)
      • if-ne:if(vA!=vB)
      • if-lt:if(vA<vB)
      • if-ge:if(vA>=vB)
      • if-gt:if(vA>vB)
      • if-le:if(vA<=vB)
    • if-testz vAA, +BBBB:条件跳转指令。拿 vAA 寄存器与 0 比较,如果比较结果满足或值为 0 就跳转到 BBBB 指定的偏移处,BBBB 不能为 0。if-testz 类型的指令有:
      • if-eqz:if(!vAA)
      • if-nez:if(vAA)
      • if-ltz:if(vAA<0)
      • if-gez:if(vAA>=0)
      • if-gtz:if(vAA>0)
      • if-lez:if(vAA<=0)

    比较指令

    对两个寄存器的值进行比较,格式为 cmpkind vAA, vBB, vCC,其中 vBB 和 vCC 寄存器是需要比较的两个寄存器或两个寄存器对,比较的结果放到 vAA 寄存器。指令集中共有5条比较指令:

    • cmpl-float
    • cmpl-double:如果 vBB 寄存器大于 vCC 寄存器,结果为 -1,相等结果为 0,小于结果为 1
    • cmpg-float
    • cmpg-double:如果 vBB 寄存器大于 vCC 寄存器,结果为 1,相等结果为 0,小于结果为 -1
    • cmp-long:如果 vBB 寄存器大于 vCC 寄存器,结果为 1,相等结果为 0,小于结果为 -1

    字段操作指令

    用于对对象实例的字段进行读写操作。对普通字段与静态字段操作有两种指令集,分别是 iinstanceop vA, vB, field@CCCCsstaticop vAA, field@BBBB。扩展为 iinstanceop/jumbo vAAAA, vBBBB, field@CCCCCCCsstaticop/jumbo vAAAA, field@BBBBBBBB

    普通字段指令的指令前缀为 i,静态字段的指令前缀为 s。字段操作指令后紧跟字段类型的后缀。

    方法调用指令

    用于调用类实例的方法,基础指令为 invoke,有 invoke-kind {vC, vD, vE, vF, vG}, meth@BBBBinvoke-kind/range {vCCCC .. vNNNN}, meth@BBBB 两类。扩展为 invoke-kind/jumbo {vCCCC .. vNNNN}, meth@BBBBBBBB 这类指令。

    根据方法类型的不同,共有如下五条方法调用指令:

    • invoke-virtualinvoke-virtual/range:调用实例的虚方法
    • invoke-superinvoke-super/range:调用实例的父类方法
    • invoke-directinvoke-direct/range:调用实例的直接方法
    • invoke-staticinvoke-static/range:调用实例的静态方法
    • invoke-interfaceinvoke-interface/range:调用实例的接口方法

    方法调用的返回值必须使用 move-result* 指令来获取,如:

    invoke-static {}, Landroid/os/Parcel;->obtain()Landroid/os/Parcel;
    move-result-object v0
    

    数据转换指令

    格式为 unop vA, vB,vB 寄存器或vB寄存器对存放需要转换的数据,转换后结果保存在 vA 寄存器或 vA寄存器对中。

    • 求补
      • neg-int
      • neg-long
      • neg-float
      • neg-double
    • 求反
      • not-int
      • not-long
    • 整型数转换
      • int-to-long
      • int-to-float
      • int-to-double
    • 长整型数转换
      • long-to-int
      • long-to-float
      • long-to-double
    • 单精度浮点数转换
      • float-to-int
      • float-to-long
      • float-to-double
    • 双精度浮点数转换
      • double-to-int
      • double-to-long
      • double-to-float
    • 整型转换
      • int-to-byte
      • int-to-char
      • int-to-short

    数据运算指令

    包括算术运算符与逻辑运算指令。

    数据运算指令有如下四类:

    • binop vAA, vBB, vCC:将 vBB 寄存器与 vCC 寄存器进行运算,结果保存到 vAA 寄存器。以下类似
    • binop/2addr vA, vB
    • binop/lit16 vA, vB, #+CCCC
    • binop/lit8 vAA, vBB, #+CC

    第一类指令可归类为:

    • add-type:vBB + vCC
    • sub-type:vBB - vCC
    • mul-type:vBB * vCC
    • div-type:vBB / vCC
    • rem-type:vBB % vCC
    • and-type:vBB AND vCC
    • or-type:vBB OR vCC
    • xor-type:vBB XOR vCC
    • shl-type:vBB << vCC
    • shr-type:vBB >> vCC
    • ushr-type:(无符号数)vBB >> vCC

    smali 语法

    类声明:

    .class <访问权限> [修饰关键字] <类名>
    .super <父类名>
    .source <源文件名>
    

    字段声明:

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

    方法声明:

    # direct methods
    .method <访问权限> [修饰关键字] <方法原型>
        [.locals]
        [.param]
        [.prologue]
        [.line]
    <代码体>
    .end method
    
    # virtual methods
    .method <访问权限> [修饰关键字] <方法原型>
        [.locals]
        [.param]
        [.prologue]
        [.line]
    <代码体>
    .end method
    

    需要注意的是,在一些老教程中,会看到 .parameter,表示使用的寄存器个数,但在最新的语法中已经不存在了,取而代之的是 .param,表示方法参数。

    接口声明:

    # interfaces
    .implements <接口名>
    

    注释声明:

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

    循环语句

    # for
    Iterator<对象> <对象名> = <方法返回一个对象列表>;
    for(<对象> <对象名>:<对象列表>){
        [处理单个对象的代码体]
    }
    
    # while
    Iterator<对象> <迭代器> = <方法返回一个迭代器>;
    while(<迭代器>.hasNext()){
        <对象> <对象名> = <迭代器>.next();
        [处理单个对象的代码体]
    }
    

    比如下面的 Java 代码:

    public void encrypt(String str) {
        String ans = "";
        for (int i = 0 ; i < str.length(); i++){
            ans += str.charAt(i);
        }
        Log.e("ans:", ans);
    }
    

    对应下面的 smali:

    # public void encrypt(String str) {
    .method public encrypt(Ljava/lang/String;)V
    .locals 4
    .parameter p1, "str"    # Ljava/lang/String;
    .prologue
    
    # String ans = "";
    const-string v0, ""
    .local v0, "ans":Ljava/lang/String;
    
    # for (int i  0 ; i < str.length(); i++){
    # int i=0 =>v1
    const/4 v1, 0x0
    .local v1, "i":I
    :goto_0     # for_start_place
    
    # str.length()=>v2
    invoke-virtual {p1}, Ljava/lang/String;->length()I
    move-result v2
    
    # i<str.length()
    if-ge v1, v2, :cond_0
    
    # ans += str.charAt(i);
    # str.charAt(i) => v2
    new-instance v2, Ljava/lang/StringBuilder;
    invoke-direct {v2}, Ljava/lang/StringBuilder;-><init>()V
    invoke-virtual {v2, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    move-result-object v2
    
    #str.charAt(i) => v3
    invoke-virtual {p1, v1}, Ljava/lang/String;->charAt(I)C
    move-result v3
    
    # ans += v3 =>v0
    invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;->append(C)Ljava/lang/StringBuilder;
    move-result-object v2
    invoke-virtual {v2}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
    move-result-object v0
    
    # i++
    add-int/lit8 v1, v1, 0x1
    goto :goto_0
    
    # Log.e("ans:", ans);
    :cond_0
    const-string v2, "ans:"
    invoke-static {v2, v0}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I
    return-void
    .end method
    

    switch 语句

    public void encrypt(int flag) {
            String ans = null;
            switch (flag){
                case 0:
                    ans = "ans is 0";
                    break;
                default:
                    ans = "noans";
                    break;
            }
            Log.v("ans:", ans);
        }
    

    对应下面的 smali:

    # public void encrypt(int flag) {
    .method public encrypt(I)V
        .locals 2
        .param p1, "flag"    # I
        .prologue
    
    # String ans = null;
        const/4 v0, 0x0
        .local v0, "ans":Ljava/lang/String;
    
    # switch (flag){
        packed-switch p1, :pswitch_data_0   # pswitch_data_0指定case区域的开头及结尾
    
    # default: ans="noans"
        const-string v0, "noans"
    
    # Log.v("ans:", ans)
        :goto_0
        const-string v1, "ans:"
        invoke-static {v1, v0}, Landroid/util/Log;->v(Ljava/lang/String;Ljava/lang/String;)I
        return-void
    
    # case 0: ans="ans is 0"
        :pswitch_0            # pswitch_<case的值>
        const-string v0, "ans is 0"
        goto :goto_0          # break
        nop
        :pswitch_data_0 #case区域的结束
        .packed-switch 0x0    # 定义case的情况
            :pswitch_0   #case 0
        .end packed-switch
    .end method
    

    根据 switch 语句的不同,case 也有两种方式:

    # packed-switch
    packed-switch p1, :pswitch_data_0
    ...
    :pswitch_data_0
    .packed-switch 0x0
        :pswitch_0
        :pswitch_1
    
    # spase-switch
    sparse-switch p1,:sswitch_data_0
    ...
    sswitch_data_0
    .sparse-switch
        0xa -> : sswitch_0
        0xb -> : sswitch_1 # 字符会转化成数组
    

    try-catch 语句

    public void encrypt(int flag) {
        String ans = null;
        try {
            ans = "ok!";
        } catch (Exception e){
            ans = e.toString();
        }
        Log.d("error", ans);
    }
    

    对应的下面的 smali:

    # public void encrypt(int flag) {
    .method public encrypt(I)V
        .locals 3
        .param p1, "flag"    # I
        .prologue
    
    # String ans = null;
        const/4 v0, 0x0
        .line 20
        .local v0, "ans":Ljava/lang/String;
    
    # try { ans="ok!"; }
        :try_start_0    # 第一个try开始,
        const-string v0, "ok!"
        :try_end_0      # 第一个try结束(主要是可能有多个try)
        .catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0
    
    # Log.d("error", ans);
        :goto_0
        const-string v2, "error"
        invoke-static {v2, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
        return-void
    
    # catch (Exception e){ans = e.toString();}
        :catch_0        #第一个catch
        move-exception v1
        .local v1, "e":Ljava/lang/Exception;
        invoke-virtual {v1}, Ljava/lang/Exception;->toString()Ljava/lang/String;
        move-result-object v0
        goto :goto_0
    .end method
    

    更多资料

    如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

    扫码二维码加入Web技术交流群

    发布评论

    需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
    列表为空,暂无数据
      我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
      原文