Dalvik可执行格式和字节码规范

<<Android软件安全权威指南>>笔记

第三章 Dalvik可执行格式与字节码规范

早期Android是Dalvik虚拟机,Android4.4以后,引入ART(Android Runtime)虚拟机

JIT(Just-In-Time)即时编译,AOT(Ahead-Of-Time)预先编译

ART提供对Dalvik的可执行格式与字节码的支持

Dalvik虚拟机

特点

专有DEX(Dalvik Executable)可执行文件格式

常量池32位索引

基于寄存器架构,完整指令系统

Dalvik虚拟机与Java虚拟机区别
字节码

Java源文件 -> Java字节码保存在class文件中 -> Java虚拟机执行

Java源文件 -> Java字节码保存在class文件中 -> Dalvik字节码保存在DEX可执行文件 -> Dalvik虚拟机执行

常量池

dx重新排列Java类文件,消除冗余信息,dx将java文件转换为DEX文件

压缩常量池,重复字符串和常量只会出现一次

虚拟机架构

Java虚拟机基于栈,Dalvik虚拟机基于寄存器

实例

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));
    }
}

编译文件,指定版本

javac -source 1.7 -target 1.7 Hello.java

生成DEX文件

dx --dex --output=Hello.dex Hello.class

javap反编译Hello.class,查看foo函数的Java字节码

javap -c -classpath . Hello

得到

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

java字节码共占8字节,每条指令没有参数

Java虚拟机指令集为零地址形式的指令集,源参数和目标参数都是隐含的,Java虚拟机提供求值栈的数据结构。

Java程序每一个线程在执行时都有一个PC计数器和一个Java栈

PC计数器以字节为单位记录当前运行位置与方法开头的偏移量,PC计数器只对当前方法有效,Java虚拟机通过PC值来取指令执行。

Java栈记录Java方法调用的活动记录,以帧frame为单位保存线程的运行状态,每调用一个新方法分配新栈帧压栈,方法返回则弹出撤出对应栈帧。

每个栈帧包括:局部变量区、操作数栈、其他

java虚拟机
最左边是PC中的偏移量,最多支持0xff条指令

iload_1为例分析

i是前缀,可以是ffloat,llong,ddouble

load是指令,表示将局部变量存入java栈

_1数字表示对哪个局部变量进行操作,从0开始计数

使用dexdump,查看foo的Dalvik字节码

dexdump -d Hello.dex

得到

  Virtual methods   -
    #0              : (in LHello;)
      name          : 'foo'
      type          : '(II)I'
      access        : 0x0001 (PUBLIC)
      code          -
      registers     : 5
      ins           : 3
      outs          : 0
      insns size    : 6 16-bit code units
000198:                                        |[000198] Hello.foo:(II)I
0001a8: 9000 0304                              |0000: add-int v0, v3, v4
0001ac: 9101 0304                              |0002: sub-int v1, v3, v4
0001b0: b210                                   |0004: mul-int/2addr v0, v1
0001b2: 0f00                                   |0005: return v0
      catches       : (none)
      positions     :
        0x0000 line=3
      locals        :
        0x0000 - 0x0006 reg=2 this LHello;

Dalvik指令简洁,只用了4条

Dalvik虚拟机运行也为每一个线程维护一个PC和一个调用栈,不同的是,该调用栈维护一个寄存器列表,寄存器的数量在方法结构体的registers字段给出,根据这个值创建一个虚拟寄存器列表

Dalvik虚拟机

虚拟机的执行流程

Android系统从下到上为Linux内核,函数库,Android运行时环境,框架,应用

Android系统启动后,立即执行init进程,完成设备初始化工作,再读取init.rc文件启动系统中重要外部程序Zygote(我查字典后才知道Zygote是受精卵的意思…还真的挺贴切的)

Zygote是Android系统中所有进程的孵化器进程。

Zygote启动后,初始化Dalvik虚拟机,再启动system_server进程进入Zygote模式,通过socket等待命令下达。

执行一个Android应用程序的时候,system_server进程通过Binder IPC方式将命令发送给Zygote,收到命令后,通过fork自身创建一个Dalvik虚拟机实例来执行应用的入口函数,完成程序自启动。

Zygote

Zygote三种创建进程的方法

  • fork()创建一个Zygote进程,这种方法实际上不会被调用
  • forkAndSpecialize()创建一个非Zygote进程
  • forkSystemServer()创建一个系统服务进程

系统服务进程终止后子进程也终止。Zygote进程可以再分为其他进程,非Zygote进程不能再分。

进程fork后,执行交给Dalvik虚拟机

先通过loadClassFromDex()函数装载类,每个类成功解析后,都会获得运行时环境中的一个ClassObject类型的数据结构存储,虚拟机使用gDvm.loadedClasses全局散列表来存储和查询所有装载进来的类。接下来字节码验证器使用dvmVerifyCodeFlow()函数对装入的代码进行校验,虚拟机调用FindClass()函数查找并装载main方法类,最后虚拟机调用dvmInterpret()函数来初始化解释器并执行字节码流

虚拟机执行方式

即时编译JIT,又叫动态编译,在运行时将字节码翻译为机器码使程序执行速度加快。

主流JIT包括两种字节码编译方式

  • method方式:以函数方法为单位编译
  • trace方式:以trace为单位进行编译

trace方式:在函数中,只有少数代码是顺序执行的,多数代码有好几条执行路径,其中一些路径很少执行,称为冷路径,执行频繁的称为热路径,使用trace编译能快速获取热路径的代码。

Dalvik虚拟机默认采用trace方式编译代码,同时支持JIT

Dalvik语言基础

Dalvik汇编语言

基于寄存器的设计,方法在内存创建以后拥有固定大小的栈帧,栈帧空间取决于方法中寄存器数目,运行时数据和代码在DEX文件中

指令流以16位无符号整型为存储单元

Dalvik指令格式

指令的位描述和指令格式标识

位描述

  • 每16位的字用空格分开
  • 每个字母表示4位,每个字母按顺序从高字节到低字节排序,每4位用|分隔
  • 顺序采用大写字母A-Z表示4位操作码,op表示8位
  • ϕ\phi表示字段所有位的值为0

A|G|op BBBB F|E|D|C为例,空格将其分为3个部分,每个部分16位

第一个部分A|G|op,高8位由A和G组成,低字节op。

第二个部分BBBB表示一个16位的偏移量

第三个部分F|E|D|C分别表示寄存器参数

指令格式标识

DEX反汇编工具

目前主流DEX文件反汇编工具:Android官方dexdump,第三方baksmali

两种反编译代码结构大致相同,寄存器命名有不同,dexdump使用的是以v开头的寄存器,baksmali使用的是以v和p开头的寄存器。

Dalvik寄存器

基于寄存器架构,设计之初为ARM架构,Dalvik将部分寄存器映射到ARM寄存器上,还有一部分通过调用栈进行模拟。

Dalvik使用的寄存器都是32位的,64位类型用相邻两个寄存器表示

语法格式为op vAAAA, vBBBB,则最大值为216-1,即65535,从0开始,取值为v0-v65535

每个函数在头部用.registers指令指定所使用的寄存器数目,当虚拟机执行到这个函数的时候,会根据寄存器数目分配适当的栈空间。

虚拟机通过处理字节码对寄存器进行的读写操作实际上都是对栈空间进行读写操作。

fp为ARM寄存器栈帧寄存器,取idx的寄存器值

寄存器命名法

两种寄存器表示方法:v命名法和p命名法

假设一个函数使用M个寄存器,函数有N个参数。则参数使用最后的N个寄存器,局部变量用从v0开始的M-N个寄存器。

以foo为例子,一共使用5个寄存器,2个显式的整型参数。

该函数是Hello类的非静态方法,调用时会传入一个隐式的Hello对象引用。所以实际上有三个参数,局部变量使用前两个寄存器,参数使用后三个寄存器。

# virtual methods
.method public foo(II)I
    .registers 5

    .prologue
    .line 3
    add-int v0, p1, p2

    sub-int v1, p1, p2

    mul-int/2addr v0, v1

    return v0
.end method

v命名法是所有的寄存器都是v+数字。

p命名法是局部变量使用的寄存器为v+数字,参数使用的寄存器为p+数字。可以通过前缀判断寄存器是局部变量寄存器还是参数寄存器。

Dalvik字节码
类型

基本类型和引用类型,这两种类型表示Java语音的全部类型。

Java中的对象和数组都属于引用对象,其他Java类型为基本类型。

Dalvik字节码类型描述符

语法 含义
v void
Z boolean
B byte
S short
C char
I int
J long
F float
D double
L Java类类型
[ 数组类型

Dalvik寄存器每个都是32位的,对于长度小于等于32位的类型,只用一个寄存器就可以存放该类型的值,对JD等64位类型,使用相邻两个寄存器存储。

L表示Java类型中的任何类,Java代码中表示为package.name.ObhectName,在Dalvik汇编代码中以Lpackage/name/ObjectName;表示

[表示基本类型数组,多个表示多维数组,例如[[I。最大维数为255

L[可以同时使用

方法

Dalvik用方法名,类型参数,返回值描述一个方法。例如

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

括号(III)为方法的参数,这里意思为三个int类型,Z表示返回值类型,这里为boolean类型

一个更复杂的例子

method(I[[IILjava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;

转换为Java代码为

String method(int, int[][], int, String, Object[]);

baksmali生成的方法代码以.method指令开始,以.end method结束

#为注释

字段

字段的格式

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

类型Lpackage/name/ObjectName;,字段名FiledName,字段类型Ljava/lang/String;

baksmali生成字段代码以.filed开头

Dalvik指令集

Android4.4以前,可以在dalvik/libdex/DexOpcodes.h中找到完整Dalvik指令集

ART主导系统,可以在art/runtime/dexinstructionlist.h中找到系统支持的完整的指令集定义。

Dalvik指令集使用单字节的指令助记符

指令类型

参数采用从dest到src的方式

为一些字节码添加名称后缀消除歧义

  • 32位常规类型的字节码未添加任何后缀
  • 64位常规类型的字节码添加-wide后缀
  • 特殊类型的字节码,具体类型后缀,除了基本类型外,还有-string-class-object
  • 斜杠后缀
  • 宽度值中的每个字母表示4位的宽度

例子:move-wide/from16 vAA, vBBBB

move为基础字节码,-wide为名称后缀,表示指令操作的数据宽度为64位,from16为字节码后缀,表示源为一个16位寄存器引用变量,vAA为dest寄存器,vBBBB为src寄存器

空操作指令

nop,值为00,通常用于对齐代码,无操作

数据操作指令

原型为move dest, src,有不同后缀

返回指令

指函数运行结束时运行的最后一条指令,基础字节码为return

数据定义指令

定义程序用到的常量,字符串,类等类型,基础字节码为const

锁指令

多线程程序对同一对象的操作中

monitor-enter vAA,为指定对象获取锁

monitor-exit vAA,释放指定对象的锁

实例操作指令

包括类型转换,检查,创建等

check-cast vAA, type@BBBB,用于将vAA寄存器中对象引用转换为指定的类型,失败抛出ClassCastException异常。

instance-of vA, vB, type@CCCC,用于判断vB寄存器中的对象引用是否可以转换为指定类型,如果可以vA赋值1,否则赋值0

new-instance vAA, type@BBBB,用于构造一个指定类型对象的新实例,对象引用赋值给vAA寄存器,type指定类型不能是数组

数组操作指令

获取数组长度,新建数组,数组赋值,数组元素取值与赋值

array-length vA, vB,获取给定vB中数组长度,值赋给vA

new-array vA, vB, type@CCCC,用于构造指定类型type@CCCC和大小vB的数组,值赋给vA

filled-new-array {vC, vD, vE, vF, vG}, type@BBBB,用于构造指定type类型和vA大小的数组并填充数组内容,vA为隐含使用的

filled-new-array/range {vCCCC, ..., vNNNN}, type@BBBB,该指令与上一条功能相同,使用range指定取值范围

arrayop vAA, vBB, vCC,用于对vBB寄存器指定的数组元素进行取值和赋值,vCC中为索引,vAA为取得值或赋的值,,读取用aget,赋值用aput

异常指令

throw vAA,抛出vAA中指定类型的异常

跳转指令

三种跳转:无条件跳转goto,分支跳转switch,条件跳转if

packed-switch vAA, +BBBBBBBB,vAA为switch分支判断的值,BBBBBBBB指向一个packed-switch-payload格式偏移表。

if-test vA, vB, +CCCC条件跳转,比较vA和vB的值,比较结果满足则跳转到CCCC指定的偏移处,test有好几种类型

eq:==, ne:!=, lt:<, ge:>=, gt:>, le:<=
eqz:==0, nez:!=0, ltz:<0, gez:>=0, gtz:>0, lez:<=0
比较指令

对两个寄存器的值,浮点或者长整型进行比较,格式为cmpkind vAA, vBB, vCC,其中vBB和vCC是比较的两个寄存器,比较的结果放在vAA中。共有5条比较指令

cmpl-float比较单精度浮点数,vBB>vCC则vAA=-1,vBB=vCC则vAA=0,vBB<vCC则vAA=1

cmpg-float

cmpl-double

cmpg-double

cmp-long

字段操作指令

对对象实例的字段进行读写

普通字段:iinstanceop vA, vB, fileld@CCCC,前缀为i

静态字段:sstaticop vAA, field@BBBB,前缀为s

后面加上字段类型后缀,例如iget-bytesget-wide

方法调用指令

调用类实例的方法,基础指令为invoke

invoke-kind {vC, vD, vE, vF, vG}, meth@BBBB

invoke-kind/range {vCCCC,...,vNNN}, meth@BBBB

方法类型不同,有5种

invoke-virtual实例的虚方法

invoke-super实例的父类方法

invoke-direct实例的直接方法

invoke-static实例的静态方法

invoke-interface实例的接口方法

返回值必须用move-result*指令获取

数据转换指令

将一种类型的数值转换为另一种类型的数值,格式unop vA, vB,vB中放需要转换的数据,转换结果放在vA中。

neg-[type]求补

not-[type]求反

XXX-to-XXX一个类型到另一个类型

数据运算指令

算术运算指令和逻辑运算指令,加减乘除模移位,与或非异或

数据运算指令有4类

binop vAA, vBB, vCC将寄存器vBB和vCC进行运算,结果放在vAA

binop/2addr vA, vB将vA与vB进行运算,结果在vA

binop/lit16 vA, vB, #+CCCCvB与常量CCCC进行运算,结果在vA

binop/lit8 vAA, vBB, #+CC同上

运算+数据类型

add, sub, mul, div, rem(就是mod), and, or, xor, shl, shr(>>), ushr(>>>)

以上就是所以支持指令,Android4.0以后扩充了一部分指令,助记符后添加jumbo

Dalvik指令练习

编写smali文件

新建HelloWorld.smali文件,模版框架如下

.class public LHelloWorld;
.super Ljava/lang/Object;
.method public static main([Ljava/lang/String;)V
    .registers 4
    .prologue
    return-void
.end method

添加指令,在.prologue后添加

.class public LHelloWorld;
.super Ljava/lang/Object;
.method public static main([Ljava/lang/String;)V
    .registers 4
    .prologue
    nop
    nop
    nop
    nop
    #数据定义
    const/16 v0, 0x8
    const/4 v1, 0x5
    const/4 v2, 0x3
    #数组操作
    new-array v0, v0, [I
    array-length v1, v0
    #实例操作
    new-instance v1, Ljava/lang/StringBuilder;
    #方法调用
    invoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V
    #跳转指令
    if-nez v0, :cond_0
    goto :goto_0
    :cond_0
    #数据转换
    int-to-float v2, v2
    #数据运算
    add-float v2, v2, v2
    #比较指令
    cmpl-float v0, v2, v2
    #字段操作
    sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
    const-string v1, "Hello world"
    #方法调用
    invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
    :goto_0
    return-void
.end method
编译smali文件
java -jar smali.jar a HelloWorld.smali

会得到out.dex文件

测试运行

启动Android运行环境,cmd执行

adb push out.dex /sdcard/

将文件输入sdcard目录中

adb shell dalvikvm -cp /sdcard/out.dex HelloWorld

会看到输出Hello World

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章