«

Android逆向 - Dalvik可执行格式与字节码规范

ljierui 发布于 阅读:35 技术杂谈


Dalvik虚拟机

Dalvik虚拟机的特点

Dalivk虚拟机与Java虚拟机的区别

Dalivk字节码与Java字节码对比

1、编写Java代码

public class Hello {
    public int foo(int a, int b) {
        return (a + b) * (a - b);
    }

    public static void main(String[] args) {
        Hello hello = new Hello();
        System.out.println(hello.foo(5, 3));
    }
}

2、编译成class : javac hello.java
3、把class文件通过Android SDK工具转码成Dex文件格式 : d8 --output=d8out .\Hello.class
4、查看Java字节码 : javap -c -classpath . Hello
5、查看Dalvik字节码: dexdump.exe -d .\classes.dex

Java字节码
解析:

  1. 下面的foo()函数 占8个字节,代码中每条指令都占一个字节,并且没有参数。
  2. Java虚拟机的指令集也被称为零地址形式的指令集,零地址形式指令的源参数和目标参数都是隐含的,通过Java虚拟机提供的数据结构求值栈来传递。
  3. 对Java程序来说,每个线程在执行时都有一个PC计数器和一个Java栈。
  4. 看foo()函数, 左边的偏移量就是程序执行每行代码时PC寄存器的值
  5. iload是load指令中的一条,i是指令前缀,表示操作类型是int; load表示将局部变量存入Java栈。(类似指令:lload(long类型),fload(float类型),dload(double类型)入栈)
  6. 下划线右边的数字表示,要操作的是那个局部变量,索引值从0开始计数,例如iload_1表示使第2个int类型的局部变量入栈,而这个局部变量都是存放在局部变量区foo()函数中的第2个参数
  7. 下面的指令:iload_2 取第3个参数;iadd从栈顶弹出两个int类型的值并求它们的和;iload_1iload_2分别再次把第2个参数和第3个参数压入栈;isub从栈顶弹出两个int类型的值并求它们的差;imul从栈顶弹出两个int类型的值并求它们的积;ireturn返回一个int类型的值。

    Compiled from "Hello.java"
    public class Hello {
    public Hello();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
    
    public int foo(int, int);
    Code:
       0: iload_1
       1: iload_2
       2: iadd
       3: iload_1
       4: iload_2
       5: isub
       6: imul
       7: ireturn
    
    public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class Hello
       3: dup
       4: invokespecial #3                  // Method "<init>":()V
       7: astore_1
       8: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      11: aload_1
      12: iconst_5
      13: iconst_3
      14: invokevirtual #5                  // Method foo:(II)I
      17: invokevirtual #6                  // Method java/io/PrintStream.println:(I)V
      20: return
    }

Dalvik字节码
解析:

  1. 相比Java字节码, Dalvik字节码只用了4个步骤就完成了这个操作。
  2. add-int 将v2,v3 寄存器中的值相加,将结果保存到v0寄存器中;v2,v3表示foo()函数的第1个参数和第2个参数
  3. sub-int 将v2,v3 寄存器中的值相减, 将结果保存到第一个寄存器中,就是v2中(2addr 和sub-int v1 的区别,后者要多一个寄存器)
  4. mul-int 将v0,v2寄存器中的值相乘,结果保存在v0寄存器中
  5. return 返回v0 寄存器的值
  6. Dalvik 虚拟机在运行时也会为每个线程维护一个PC计数器和一个调用栈,与Java不同,这个调用栈维护了一个寄存器列表,寄存器数量会在方法结构体registers中给出。

    Opened '.\classes.dex', DEX version '035'
    Class #0            -
    Class descriptor  : 'LHello;'
    Access flags      : 0x0001 (PUBLIC)
    Superclass        : 'Ljava/lang/Object;'
    Interfaces        -
    Static fields     -
    Instance fields   -
    Direct methods    -
    #0              : (in LHello;)
      name          : '<init>'
      type          : '()V'
      access        : 0x10001 (PUBLIC CONSTRUCTOR)
      code          -
      registers     : 1
      ins           : 1
      outs          : 1
      insns size    : 4 16-bit code units
    00016c:                                        |[00016c] Hello.<init>:()V
    00017c: 7010 0400 0000                         |0000: invoke-direct {v0}, Ljava/lang/Object;.<init>:()V // method@0004
    000182: 0e00                                   |0003: return-void
      catches       : (none)
      positions     :
        0x0000 line=1
      locals        :
        0x0000 - 0x0004 reg=0 this LHello;
    
    #1              : (in LHello;)
      name          : 'main'
      type          : '([Ljava/lang/String;)V'
      access        : 0x0009 (PUBLIC STATIC)
      code          -
      registers     : 4
      ins           : 1
      outs          : 3
      insns size    : 17 16-bit code units
    000184:                                        |[000184] Hello.main:([Ljava/lang/String;)V
    000194: 2203 0100                              |0000: new-instance v3, LHello; // type@0001
    000198: 7010 0000 0300                         |0002: invoke-direct {v3}, LHello;.<init>:()V // method@0000
    00019e: 6200 0000                              |0005: sget-object v0, Ljava/lang/System;.out:Ljava/io/PrintStream; // field@0000
    0001a2: 1251                                   |0007: const/4 v1, #int 5 // #5
    0001a4: 1232                                   |0008: const/4 v2, #int 3 // #3
    0001a6: 6e30 0100 1302                         |0009: invoke-virtual {v3, v1, v2}, LHello;.foo:(II)I // method@0001
    0001ac: 0a03                                   |000c: move-result v3
    0001ae: 6e20 0300 3000                         |000d: invoke-virtual {v0, v3}, Ljava/io/PrintStream;.println:(I)V // method@0003
    0001b4: 0e00                                   |0010: return-void
      catches       : (none)
      positions     :
        0x0000 line=7
        0x0005 line=8
        0x0010 line=9
      locals        :
        0x0000 - 0x0011 reg=3 (null) [Ljava/lang/String;
    
    Virtual methods   -
    #0              : (in LHello;)
      name          : 'foo'
      type          : '(II)I'
      access        : 0x0001 (PUBLIC)
      code          -
      registers     : 4
      ins           : 3
      outs          : 0
      insns size    : 6 16-bit code units
    000150:                                        |[000150] Hello.foo:(II)I
    000160: 9000 0203                              |0000: add-int v0, v2, v3
    000164: b132                                   |0002: sub-int/2addr v2, v3
    000166: 9200 0002                              |0003: mul-int v0, v0, v2
    00016a: 0f00                                   |0005: return v0
      catches       : (none)
      positions     :
        0x0000 line=3
      locals        :
        0x0000 - 0x0006 reg=1 this LHello;
        0x0000 - 0x0006 reg=2 (null) I
        0x0000 - 0x0006 reg=3 (null) I
    
    source_file_idx   : 1 (Hello.java)

    虚拟机的执行流程

    1. Android 系统由Linux内核、函数库、Android运行时、应用程序框架及应用程序组成,Android系统结构采用了分层的思想,Dalvik虚拟机属于Android运行时环境,它与一些核心库一起承担了Android应用程序的运行工作
    2. Android 系统启动并加载内核后,会立即执行init进程,在读取init.rc文件并启动系统中重要外部程序zygote.
    3. Zygote是Android系统中所有进程的孵化器进程,它启动后,会初始化Dalvik虚拟机,再启动system_server进程并进入Zygote模式,通过socket等候命令的下达。
    4. 执行程序时,system_server进程通过Binder IPC方式将命令放送给Zygote,它收到命令后通过fork其自身创建一个Dalvik虚拟机的实例来执行应用程序的入口函数,从而启动程序。

虚拟机的执行方式

  1. 即使编译又称为动态编译, 是通过在运行时将字节码翻译为机器码使程序的执行速度加快。
  2. 主流的JIT包括两种字节码编译方式:
    2.1、method方式: 以函数或方法为单位进行编译
    2.2、trace方式:以trace为单位进行编译;指能快速获取热路径的代码,从而采用更短的方式编译

Android逆向