现在的位置: 首页 > 综合 > 正文

Dalvik 调试分析

2012年07月15日 ⁄ 综合 ⁄ 共 8080字 ⁄ 字号 评论关闭

前言:dalvik 是 Android 的重要组成部分 ,掌握其运行机制对理解整个Android系统有着相当之大的帮助。本文将介绍GDB单步调试和Dexdump工具的使用,期望为探索dalvik打下一定的基础。

 

1. Dalvik 之编译

为了能够更方便的调试dalvik,我们需要编译一个在X86上运行的dalvik和相关工具。编译步骤如下:

  1. 首先进入到Android 源码根目录
  2. source build/envsetup.sh (不是网上有些文章写的只输入 build/envsetup.sh)
  3. lunch 2  在此之后可以看到TARGET_PRODUCT 为sim。TARGET_ARCH为x86
  4. make  或者 make dalvikvm 和 make dexdump   (make 为编译所有程序,比较耗时,有时甚至某些模块编译不过,如为节省时间,可使用make dalvikvm直接编译dalvik, make dexdump直接编译dexdump)

 

2. Gdb调试dalvik之准备工作

在用gdb启动dalvik时,需要设置一些环境,比较繁琐,这里创建一个脚本来简化这些过程,脚本名为grund.sh,放于Android源码根目录。下面为脚本内容。

 

#!/bin/sh

base=`pwd`

root=$base/out/debug/host/linux-x86/pr/sim/system

export ANDROID_ROOT=$root

bootpath=$root/framework

export BOOTCLASSPATH=$bootpath/core.jar:$bootpath/ext.jar:$bootpath/framework.jar:$bootpath/android.police.jar

export ANDROID_DATA=/tmp/dalvik_test

mkdir -p $ANDROID_DATA/dalvik-cache

exec gdb $root/bin/dalvikvm 

 

3. Gdb 调试 dalvik

  1. 准备一个简单的java 程序,如hello.java,编译后将hello.jar拷贝至Android 源码根目录。 (hello.java与makefile见附录)
  2. 进入到Android 源码根目录
  3. ./grund.sh  (执行上述脚本,之后会看到gdb提示符)
  4. 在gdb提示符后输入 “set args –cp hello.jar hello”
  5. 这个时候就可以设置断点,单步跟踪了!如有对gdb不熟的同学,请google之。 main()函数为入口函数,先在main.c 212行设置断点(在gdb提示符后输入 “b 212”)
  6. 在gdb提示符后输入”r”, OK,我们会看到dalvik被gdb启动执行,然后停于212行,执行JNI_CreateJavaVM函数前先看看gDvm的内容(输入p gDvm)。 然后执行JNI_CreateJavaVM函数(输入“n”),再看看gDvm的内容。对比执行前后的变化,可大概知道JNI_CreateJavaVM函数所做的事情。
  7. main.c 249 行代码用于加载hello.class,在249行设置断点。在此中断后,看下slashClass的内容(输入“p slashClass“),slashClass正是”hello”字符串。接下来单步进入执行之(输入”s”),然后查看下函数调用栈(输入”bt”)。可知现在正在执行的是jni.c 中的FindClass函数。通过此方法,可知函数指针指向的是何函数。
  8. main.c 255行代码用于取得hello.java 中main函数编译后的字节码。类似于g步骤,可知此时执行的函数为jni.c中的GetStaticMethodID函数
  9. main.c 273行代码执行main函数编译后的字节码,类似于g步骤,可知此时执行的函数为jni.c 中的2681行。此处为宏定义,不容易找到。但通过gdb调试,可以准确的定位。如此时继续运行程序,”hello world” 就会出现在我们的眼前!

 

此处只做简单分析,为抛砖引玉用,各位同学可以藉此探究自己感兴趣的内容。在接下来的文章中会详细分析class加载与字节码的执行。

 

4.  dexdump查看jar文件

 

Dexdump 可执行文件放于out目录下, 可使用”find out/ -name dexdump”命令来找到dexdump。

“dexdump –f  hello.jar” 命令可打印jar文件的头部信息。

“dexdump –d hello.jar” 可打印所编译的字节码。

 

头部信息如下:

Opened 'hello.jar', DEX version '035'

DEX file header:

magic               : 'dex

035'

checksum            : f2f85a9c

signature           : 0404...7831

file_size           : 740

header_size         : 112

link_size           : 0

link_off            : 0 (0x000000)

string_ids_size     : 14

string_ids_off      : 112 (0x000070)

type_ids_size       : 7

type_ids_off        : 168 (0x0000a8)

field_ids_size      : 1

field_ids_off       : 232 (0x0000e8)

method_ids_size     : 4

method_ids_off      : 240 (0x0000f0)

class_defs_size     : 1

class_defs_off      : 272 (0x000110)

data_size           : 436

data_off            : 304 (0x000130)

 

 

其中string_ids,type_ids,field_ids,method_ids, class_defs皆可理解为索引。通过这些索引,可以查到真正的数据存放位置。Data_off为真正的数据存放位置。

 

 

字节码如下:

    #1              : (in Lhello;)

      name          : 'main'

      type          : '([Ljava/lang/String;)V'

      access        : 0x0009 (PUBLIC STATIC)

      code          -

      registers     : 3

      ins           : 1

      outs          : 2

      insns size    : 10 16-bit code units

000148:                                        |[000148] hello.main:([Ljava/lang/String;)V

000158: 6200 0000                              |0000: sget-object v0, Ljava/lang/System;.out:Ljava/io/PrintStream; // field@0000

00015c: 1a01 0900                              |0002: const-string v1, "hello world" // string@0009

000160: 6e20 0200 1000                         |0004: invoke-virtual {v0, v1}, Ljava/io/PrintStream;.println:(Ljava/lang/String;)V // method@0002

000166: 2a00 0000 0000                         |0007: goto/32 #00000000

      catches       : (none)

      positions     :

        0x0000 line=4

        0x0007 line=5

      locals        :

 

  Virtual methods   -

  source_file_idx   : 10 (hello.java)

 

 

附录:

Hello.java :

public class hello{

      public static void main(String args[]) {

                  System.out.println("hello world");

                  while(true) {}

      }

}

 

Makefile:

ANDROID_SRC_DIR := /android/platform_sim

 

android_dir_dx = $(ANDROID_SRC_DIR)/out/host/linux-x86/bin/dx

 

all:

      javac hello.java

      $(android_dir_dx) --dex --output=hello.jar hello.class

clean:

      @rm *.jar *.class

 

Java 源代码经过编译后会生成后缀为class的文件,也即字节码文件。然后在Android中使用dx工具将其转换为后缀为jar 的dex类型文件。Dalvik 虚拟机负责解释并执行编译后的字节码。在解释执行字节码之前,当然要读取文件,分析文件的内容,得到字节码,然后才能解释执行之。在整个的加载过程中,最为重要的就是对Class的加载 – Class包含Method,Method 又包含code。通过对Class的加载,我们即可获得所需执行的字节码。

 

本文从dexfile 文件分析及Class加载中的数据结构入手,结合主要流程,对整个加载过程进行分析,期望对大家有所帮助。

 

 

1. DexFile 在内存中的映射

 

在Android 系统中,java源文件会被编译为后缀为jar的dex类型文件,在代码中称为dexfile。在加载Class之前,必先读取相应的jar文件。通常我们使用read函数来读取文件中的内容。但在Dalvik中使用mmap函数,和read不同,mmap函数会将dex文件映射到内存中,这样通过普通的内存读取操作即可访问dex file 中的内容。

 

Dexfile 的文件格式如下图所示,主要有三部分组成:头部,索引,数据。通过头部可知索引的位置和数目,可知数据区的起始位置。其中classDefsOff指定了ClassDef在文件的起始位置,dataOff指定了数据在文件的起始位置,ClassDef即可理解为Class的索引。通过读取ClassDef可获知Class的基本信息,其中classDataOff指定了Class数据在数据区的位置。

 

在将dexfile文件映射到内存后,即会调用dexFileParse函数对其分析,分析的结果存放于名为DexFile 的数据结构中。DexFile中的baseAddr指向映射区的起始位置,pClassDefs指向ClassDefs(即class索引)的起始位置。由于在查找class时,都是使用class的名字进行查找,为了加快查找速度,创建了一个hash表。在hash表中对class名字进行hash,并生成index。这些操作都是在对文件解析时所完成的,这样虽然在加载过程中比较耗时,但是在运行过程中确可节省大量查找时间。

 

 

 

 

 

 

2. ClassObject -  Class在加载后的表现形式

 

在对文件解析完成后就要加载Class的具体内容了!在Dalvik中,由ClassObject 这个数据结构负责存放加载的信息。如下图所示,加载过程会在内存中alloc几个区域,分别存放directMethods, virtualMethods, sfields, ifields。这些信息正是从dex 文件的数据区中读取。首先会读取Class的详细信息,从中获知directMethod, virtualMethod, sfield, ifield等的信息,然后再读取。下图为加载完成后的示意。 这里并未介绍加载的每个细节,感兴趣的同学可通过此二图自行分析。

还请大家注意的是在ClassObject结构中有个名为super的成员。通过super成员来指向它的超类。

 

 

 

3. findClassNoInit – 负责加载Class并生成相应ClassObject的函数。

 

第二节介绍了加载后的数据结构,本节会分析负责加载工作的函数- findClassNoInit。 请注意在获取Class索引时,会分为基本类库文件和用户类文件两种情况。在Dalvik分析之准备篇中,grund.sh中有一语句   “export BOOTCLASSPATH=$bootpath/core.jar:$bootpath/ext.jar:$bootpath/framework.jar:$bootpath/android.police.jar”
。 这条语句指定了Dalvik所需的基本库文件,如果没有此语句,Dalvik在启动过程中就会报错退出。

 

LoadClassFromDex 函数先会读取Class的具体数据(从ClassDataoff处),然后按图索骥,分别加载 directMethod, virtualMethod,ifield,sfield。

 

作为业界TOP公司出品的东东,当然要关注执行效率了^_^。首先,在加载后我们要将其缓存起来,以便以后使用方便。其次,在查找过程中,如果我们顺序查找的话,当然是很慢的拉。这当然是俺们senior engineer所不能容忍的,所以gDvm.loadedClasses这个Hash表就隆重出场了。什么,这位同学说没有几个Class,至于这么大动干戈么。让我们看看,通过在准备篇中介绍的方法,我们在main.c 249行设置断点,这个时候基本库已加载完毕。当程序停下时,我们看看gDvm的值,可以看到numLoadedClasses这个成员的值为212
!也即意味着这个时候我们什么也没做,用户类也未加载,Dalvik 已加载的Class数目已达到212。

 

dvmLinkClass, 好长,但是其最终好像会再次调用findClassNoInit。嗯,也是可以理解的嘛。如果一个子类需要调用超类的函数,那它当然要先加载超类了,可能的话,甚至会加载超类的超类^_^。  

 

 

 

 

眼见为虚,实践出真知,拿gdb调试下。

在findClassNoInit函数处设置断点(在gdb提示符后输入”b findClassNoInit”),在gdb提示符后连续几次执行”c”和”bt”。可出现如下信息,可以看到在函数调用栈上可以多次看到findClassNoInit的身影。

 

(gdb) bt

#0  findClassNoInit (descriptor=0xfef4c7f4 "??????%", loader=0x0, pDvmDex=0x0)

    at dalvik/vm/oo/Class.c:1373

#1  0xf6fc4d53 in dvmFindClassNoInit (descriptor=0xf5046a63 "Ljava/lang/Object;", loader=0x0)

    at dalvik/vm/oo/Class.c:1194

#2  0xf6fc6c0a in dvmResolveClass (referrer=0xf5837400, classIdx=290,

    fromUnverifiedConstant=false) at dalvik/vm/oo/Resolve.c:94

#3  0xf6fc3476 in dvmLinkClass (clazz=0xf5837400, classesResolved=false)

    at dalvik/vm/oo/Class.c:2537

#4  0xf6fc1b67 in findClassNoInit (descriptor=0xf6ff0df6 "Ljava/lang/Class;", loader=0x0,

    pDvmDex=0xa04c720) at dalvik/vm/oo/Class.c:1489

 

 

现在从另一个角度去观察。在class.c 2575行设置断点,然后等待程序停下。看下clazz的内容。

(gdb) p clazz->super->descriptor

$6 = 0xf5046a63 "Ljava/lang/Object;"

(gdb) p clazz->descriptor

$7 = 0xf5046121 "Ljava/lang/Class;"

 

 

4. 基本类库文件的加载

 

先在findClassNoInit函数处设置断点,然后运行程序,等待程序的停下。

 

(gdb) b findClassNoInit

Breakpoint 2 at 0xf6fc13e0: file dalvik/vm/oo/Class.c, line 1373.

(gdb) c

Continuing.

 

看看谁是第一个加载的Class,及其调用关系。

 

(gdb) bt

#0  findClassNoInit (descriptor=0x0, loader=0x0, pDvmDex=0x0) at dalvik/vm/oo/Class.c:1373

#1  0xf6fc32a1 in dvmLinkClass (clazz=0xf5837350, classesResolved=false)

    at dalvik/vm/oo/Class.c:2491

#2  0xf6fc1b67 in findClassNoInit (descriptor=0xf6ff1ded "Ljava/lang/Thread;", loader=0x0,

    pDvmDex=0xa04c720) at dalvik/vm/oo/Class.c:1489

#3  0xf6f92692 in dvmThreadObjStartup () at dalvik/vm/Thread.c:328

#4  0xf6f800e6 in dvmStartup (argc=2, argv=0xa041190, ignoreUnrecognized=false, pEnv=0xa0411a0)

    at dalvik/vm/Init.c:1155

#5  0xf6f8b8e3 in JNI_CreateJavaVM (p_vm=0xf6ff0df6, p_env=0xf6ff0df6, vm_args=0xfef4d0b0)

    at dalvik/vm/Jni.c:4198

#6  0x08048893 in main (argc=3, argv=0xfef4d168) at dalvik/dalvikvm/Main.c:212

 

函数调用顺序清晰可见:main -> JNI_CreateJavaVM-> dvmStartup-> dvmThreadObjStartup-> dvmFindSystemClassNoInit-> findClassNoInit 观察仔细的同学可能会问在调用栈中没有看到dvmFindSystemClassNoInit啊,为何你这样写啊?我估计编译器将其作为inline优化了,导致gdb看不到有dvmFindSystemClassNoInit的栈。从回溯栈中也可清晰的看到

 

 

5. 用户类文件的加载

用户类文件的加载颇为曲折,它会先加载一个Class。然后这个Class去负责用户类文件的加载,当然了,这个Class又会通过JNI的方式去曲折调用到findClassNoInit。这个就留给大家自己分析了, 其实楼主自己也不是很明白为什么要这么大费周折^_^。

 

 

 

 

 

抱歉!评论已关闭.