From:https://blog.csdn.net/sinat_18268881/article/details/55832757

Dex文件格式详解:https://www.jianshu.com/p/f7f0a712ddfe

dex文件解析(第三篇):https://blog.csdn.net/tabactivity/article/details/78950379

深入理解DEX文件格式( 有个 Python 读 dex 文件 ):https://mybeibei.net/archives/1103

一文读懂 DEX 文件格式解析:https://cloud.tencent.com/developer/article/1663852

Android逆向之旅—解析编译之后的Dex文件格式:http://www.520monkey.com/archives/579

图解Dex文件结构及解析要点:https://blog.csdn.net/beyond702/article/details/52460721

从 Android 运行时出发,打造我们的脱壳神器:https://blog.csdn.net/earbao/article/details/51516116

http://www.droidsec.cn/从android运行时出发,打造我们的脱壳神器/

参考资料:《Android软件安全与逆向分析》.非虫

  1. DexFile.h:https://www.androidos.net.cn/android/9.0.0_r8/xref/dalvik/libdex/DexFile.h
  2. DexFile.cpp:https://www.androidos.net.cn/android/9.0.0_r8/xref/dalvik/libdex/DexFile.cpp

0x00■  构造 DEX 文件

什么是 dex 文件?

DEX 文件就是 Android Dalvik 虚拟机运行的程序,关于 DEX 文件的结构的重要性我就不多说了。

dex  是 Android 系统的可执行文件,包含应用程序的全部操作指令以及运行时数据。

简单的说,就是优化后的 android版.exe。每个apk安装包里都有。相对于PC上的 java 虚拟机能运行.class;android上的 Davlik 虚拟机能运行.dex。

为何要研究 dex 格式 ?

因为 dex 里面包含了所有 app 代码,利用反编译工具可以获取 java 源码。理解并修改 dex 文件,就能更好的 逆向 APK。

由于dalvik是一种针对嵌入式设备而特殊设计的java虚拟机,所以dex文件与标准的class文件在结构设计上有着本质的区别

当 java 程序编译成 class 后,还需要使用 dx 工具将所有的 class 文件整合到一个 dex 文件,目的是其中各个类能够共享数据,在一定程度上降低了冗余,同时也是文件结构更加经凑,实验表明,dex 文件是传统 jar 文件大小的 50% 左右

可以看见:dex 将原来 class 每个文件都有的共有信息合成一体,这样减少了 class 的冗余

数据结构

类型 含义
u1 unit8_t,1字节无符号数
u2 unit16_t,2字节无符号数
u4 unit32_t,4字节无符号数
u8 unit64_t,8字节无符号数
sleb128 有符号LEB128,可变长度1~5
uleb128 无符号LEB128,
uleb128p1 无符号LEB128值加1,

其中 u1~u8 很好理解,表示 1 到 8 个字节的无符号数,后面三个是dex特有的数据类型,

不理解的可以参考这里( https://blog.csdn.net/zklth/article/details/7978362 )

更详细的参考:(深入到源码解析leb128数据类型)[http://i.woblog.cn/2016/07/23/leb128-format/]

下面开练。。。。。

建议:不要只看,跟着做。看再多遍不如自己亲自实践一遍来的可靠,别问我为什么知道。泪崩ing.....

下面会自己构造一个 dex 文件,因为自己构造的比较简单,分析起来比较容易。等你简单的会了,难的自然也就懂了。

首先,编写一个简单的 Java 程序,如下:

public class HelloWorld {  int a = 0;  static String b = "HelloDalvik";  public int getNumber(int i, int j) {  int e = 3;  return e + i + j;  }  public static void main(String[] args) {  int c = 1;  int d = 2;  HelloWorld helloWorld = new HelloWorld();  String sayNumber = String.valueOf(helloWorld.getNumber(c, d));  System.out.println("HelloDex!" + sayNumber);  }
}  

然后将其编译成 dex 文件:打开命令行,进入 HelloWorld.class 所在文件夹下,执行命令:

javac HelloWorld.java

接下来会出现一个HelloWorld.class文件,然后继续执行命令( dx 工具需要安装Android SDK才能有的工具 ):

dx --dex --output=HelloWorld.dex HelloWorld.class

就会出现 HelloWorld.dex 文件了。

在当前工作路径下 , 编译方法如下 :

1. 编译成 java class 文件,执行命令 : javac Hello.java  。编译完成后 ,目录下生成 Hello.class 文件 。可以使用命令 java Hello 来测试下 ,会输出代码中的 “Hello, Android!” 的字符串 。

2. 编译成 dex 文件
编译工具在 Android SDK 的路径如下 ,其中 19.0.1 是Android SDK build_tools 的版本 ,请按照在本地安装的 build_tools 版本来 。建议该路径加载到 PATH 路径下 ,否则引用 dx 工具时需要使用绝对路径 :./build-tools/19.0.1/dx
执行命令 : dx –dex –output=Hello.dex Hello.class
编译正常会生成 Hello.dex 文件 。

3. 使用 ADB 运行测试
        测试命令和输出结果如下 :
        $ adb root
        $ adb push Hello.dex /sdcard/
        $ adb shell
        root@maguro:/ # dalvikvm -cp /sdcard/Hello.dex Hello

4. 重要说明

(1) 测试环境使用真机和 Android 虚拟机都可以的 。核心的命令是
dalvikvm -cp /sdcard/Hello.dex Hello
-cp 是 class path 的缩写 ,后面的 Hello 是要运行的 Class 的名称 。网上有描述说输入 dalvikvm –help
可以看到 dalvikvm 的帮助文档 ,但是在 Android4.4 的官方模拟器和自己的手机上测试都提示找不到
Class 路径 ,在Android 老的版本 ( 4.3 ) 上测试还是有输出的 。

(2) 因为命令在执行时 , dalvikvm 会在 /data/dalvik-cache/ 目录下创建 .dex 文件 ,因此要求 ADB 的
执行 Shell 对目录 /data/dalvik-cache/ 有读、写和执行的权限 ,否则无法达到预期效果 。

这时,我们需要下载一个十六进位文本编辑器,因为用它可以解析二进制文件,我们用它打开 dex 文件就会全部以十六进制的数进行展现了。

这里推荐 010Editor,下载地址:https://www.sweetscape.com/010editor/(收费软件,可以免费试用30天)。

下载完成之后,我们可以用它打开dex文件了,打开之后,你的界面应该是这样的:

一下子看到这些东西,是不是立马懵逼了,正常,我刚开始看的时候也是,这什么玩意儿啊!其实,这就是二进制流文件中的内容,010Editor把它转化成了16进制的内容,以方便我们阅读的。

0x01■  DEX文件结构总览

一张图搞懂dex

( 可以 右键 ---> 在新标签页中打开图片 ,就可以看清图片)

不要慌,下面我跟你解释,这些东西我们虽然看了懵逼,但是 Dalvik 虚拟机不会,因为它就是解析这些东西的,这些东西虽然看起来头大,但是它是有自己的格式标准的。dex文件的结构如下图所示:

这就是 dex 的文件格式了,下面我们从最上面的 Header 说起,Header 中存储了什么内容呢?下面我们还得来一张图:

从宏观上来说 dex 的文件结果很简单,实际上是由多个不同结构的数据体以首尾相接的方式拼接而成。如下图:

数据名称 解释
header dex文件头部,记录整个dex文件的相关属性
string_ids 字符串数据索引,记录了每个字符串在数据区的偏移量
type_ids 类似数据索引,记录了每个类型的字符串索引
proto_ids 原型数据索引,记录了方法声明的字符串,返回类型字符串,参数列表
field_ids 字段数据索引,记录了所属类,类型以及方法名
method_ids 类方法索引,记录方法所属类名,方法声明以及方法名等信息
class_defs 类定义数据索引,记录指定类各类信息,包括接口,超类,类数据偏移量
data 数据区,保存了各个类的真是数据
link_data 连接数据区

/dalvik/libdex/DexFile.h  定义如下:

struct DexFile {const DexHeader*    pHeader;const DexStringId*  pStringIds;const DexTypeId*    pTypeIds;const DexFieldId*   pFieldIds;const DexMethodId*  pMethodIds;const DexProtoId*   pProtoIds;const DexClassDef*  pClassDefs;const DexLink*      pLinkData;
}

注意:其中一些定义的字段是在内存中并没有存到真实的 dex 文件中

header 简单记录了dex文件的一些基本信息,以及大致的数据分布。长度固定为0x70,其中每一项信息所占用的内存空间也是固定的,好处是虚拟机在处理 dex 时不用考虑 dex 文件的多样性

字段名称 偏移值 长度 说明
magic 0x0 8 魔数字段,值为"dex\n035\0"
checksum 0x8 4 校验码
signature 0xc 20 sha-1签名
file_size 0x20 4 dex文件总长度
header_size 0x24 4 文件头长度,009版本=0x5c,035版本=0x70
endian_tag 0x28 4 标示字节顺序的常量
link_size 0x2c 4 链接段的大小,如果为0就是静态链接
link_off 0x30 4 链接段的开始位置
map_off 0x34 4 map数据基址
string_ids_size 0x38 4 字符串列表中字符串个数
string_ids_off 0x3c 4 字符串列表基址
type_ids_size 0x40 4 类列表里的类型个数
type_ids_off 0x44 4 类列表基址
proto_ids_size 0x48 4 原型列表里面的原型个数
proto_ids_off 0x4c 4 原型列表基址
field_ids_size 0x50 4 字段个数
field_ids_off 0x54 4 字段列表基址
method_ids_size 0x58 4 方法个数
method_ids_off 0x5c 4 方法列表基址
class_defs_size 0x60 4 类定义标中类的个数
class_defs_off 0x64 4 类定义列表基址
data_size 0x68 4 数据段的大小,必须4k对齐
data_off 0x6c 4 数据段基址

/dalvik/libdex/DexFile.h  定义如下:

struct DexHeader {u1  magic[8];           /* includes version number */u4  checksum;           /* adler32 checksum */u1  signature[kSHA1DigestLen]; /* SHA-1 hash */u4  fileSize;           /* length of entire file */u4  headerSize;         /* offset to start of next section */u4  endianTag;u4  linkSize;u4  linkOff;u4  mapOff;u4  stringIdsSize;u4  stringIdsOff;u4  typeIdsSize;u4  typeIdsOff;u4  protoIdsSize;u4  protoIdsOff;u4  fieldIdsSize;u4  fieldIdsOff;u4  methodIdsSize;u4  methodIdsOff;u4  classDefsSize;u4  classDefsOff;u4  dataSize;u4  dataOff;
};

我们可以用:hexdump -c classes.dex 查看 dex 单字节显示的结果,如下:

0000000   d   e   x  \n   0   3   5  \0 022 217   ?   w   z   ? 031 221
0000010   ?  \f   ?   ?   ?   ?   ?   ? 217 235 200   z   ? 030   I   ?
0000020   ? 003  \0  \0   p  \0  \0  \0   x   V   4 022  \0  \0  \0  \0
0000030  \0  \0  \0  \0   ? 002  \0  \0 024  \0  \0  \0   p  \0  \0  \0
0000040  \b  \0  \0  \0   ?  \0  \0  \0 005  \0  \0  \0   ?  \0  \0  \0
0000050 001  \0  \0  \0 034 001  \0  \0 005  \0  \0  \0   $ 001  \0  \0
0000060 001  \0  \0  \0   L 001  \0  \0   8 002  \0  \0   l 001  \0  \0
0000070   l 001  \0  \0   t 001  \0  \0 201 001  \0  \0 204 001  \0  \0
0000080 222 001  \0  \0 226 001  \0  \0   ? 001  \0  \0   ? 001  \0  \0
0000090   ? 001  \0  \0   ? 001  \0  \0 004 002  \0  \0  \a 002  \0  \0
00000a0  \v 002  \0  \0     002  \0  \0   ( 002  \0  \0   . 002  \0  \0
00000b0   4 002  \0  \0   9 002  \0  \0   B 002  \0  \0   L 002  \0  \0
00000c0 003  \0  \0  \0 005  \0  \0  \0 006  \0  \0  \0  \a  \0  \0  \0
00000d0  \b  \0  \0  \0  \t  \0  \0  \0  \n  \0  \0  \0  \f  \0  \0  \0
00000e0 002  \0  \0  \0 003  \0  \0  \0  \0  \0  \0  \0 004  \0  \0  \0
00000f0 004  \0  \0  \0   x 002  \0  \0  \n  \0  \0  \0 006  \0  \0  \0
0000100  \0  \0  \0  \0  \v  \0  \0  \0 006  \0  \0  \0   x 002  \0  \0
0000110  \v  \0  \0  \0 006  \0  \0  \0   p 002  \0  \0 005  \0 001  \0
0000120 020  \0  \0  \0  \0  \0 004  \0 017  \0  \0  \0 001  \0 003  \0
0000130 021  \0  \0  \0 004  \0 002  \0  \0  \0  \0  \0 004  \0 001  \0
0000140  \r  \0  \0  \0 004  \0  \0  \0 022  \0  \0  \0  \0  \0  \0  \0
0000150 001  \0  \0  \0 002  \0  \0  \0  \0  \0  \0  \0   ?   ?   ?   ?
0000160  \0  \0  \0  \0   ? 002  \0  \0  \0  \0  \0  \0 006   <   i   n
0000170   i   t   >  \0  \v   H   e   l   l   o       W   o   r   l   d
0000180  \0 001   L  \0  \f   L   H   e   l   l   o   W   o   r   l   d
0000190   ;  \0 002   L   L  \0 025   L   j   a   v   a   /   i   o   /
00001a0   P   r   i   n   t   S   t   r   e   a   m   ;  \0 022   L   j
00001b0   a   v   a   /   l   a   n   g   /   O   b   j   e   c   t   ;
00001c0  \0 022   L   j   a   v   a   /   l   a   n   g   /   S   t   r
00001d0   i   n   g   ;  \0 031   L   j   a   v   a   /   l   a   n   g
00001e0   /   S   t   r   i   n   g   B   u   i   l   d   e   r   ;  \0
00001f0 022   L   j   a   v   a   /   l   a   n   g   /   S   y   s   t
0000200   e   m   ;  \0 001   V  \0 002   V   L  \0 023   [   L   j   a
0000210   v   a   /   l   a   n   g   /   S   t   r   i   n   g   ;  \0
0000220 006   a   p   p   e   n   d  \0 004   a   r   g   s  \0 004   m
0000230   a   i   n  \0 003   o   u   t  \0  \a   p   r   i   n   t   l
0000240   n  \0  \b   t   o   S   t   r   i   n   g  \0 016   ?   ? 231
0000250   ? 230   ?   ?   ? 200   ?   ?   ?   ? 211 213   ? 206 231   ?
0000260 232 204   s   m   a   l   i   ?   ? 236   ?   ? 213  \0  \0  \0
0000270 001  \0  \0  \0  \a  \0  \0  \0 001  \0  \0  \0 003  \0  \0  \0
0000280  \0  \0  \0  \0  \0  \0  \0  \0  \0 001 017  \a  \0  \0  \0  \0
0000290  \v  \0 001  \0 002  \0  \0  \0 210 002  \0  \0   (  \0  \0  \0
00002a0   b  \0  \0  \0  \0  \0  \0  \0  \0  \0 022   2 023 003   ?   ?
00002b0 030 004  \0  \0 001  \0  \0  \0  \0  \0 034 005 003  \0 001   &
00002c0   "  \a 004  \0   p 020 002  \0  \a  \0 032  \b 023  \0   n
00002d0 003  \0 207  \0  \f  \a   n 020 004  \0  \a  \0  \f  \t   n
00002e0 001  \0 220  \0 032 001 001  \0   n     001  \0 020  \0 016  \0
00002f0  \0  \0 001  \0  \0  \t 220 005 016  \0  \0  \0  \0  \0  \0  \0
0000300 001  \0  \0  \0  \0  \0  \0  \0 001  \0  \0  \0 024  \0  \0  \0
0000310   p  \0  \0  \0 002  \0  \0  \0  \b  \0  \0  \0   ?  \0  \0  \0
0000320 003  \0  \0  \0 005  \0  \0  \0   ?  \0  \0  \0 004  \0  \0  \0
0000330 001  \0  \0  \0 034 001  \0  \0 005  \0  \0  \0 005  \0  \0  \0
0000340   $ 001  \0  \0 006  \0  \0  \0 001  \0  \0  \0   L 001  \0  \0
0000350 002      \0  \0 024  \0  \0  \0   l 001  \0  \0 001 020  \0  \0
0000360 002  \0  \0  \0   p 002  \0  \0 003 020  \0  \0 002  \0  \0  \0
0000370 200 002  \0  \0 003      \0  \0 001  \0  \0  \0 210 002  \0  \0
0000380 001      \0  \0 001  \0  \0  \0 220 002  \0  \0  \0      \0  \0
0000390 001  \0  \0  \0   ? 002  \0  \0  \0 020  \0  \0 001  \0  \0  \0
00003a0   ? 002  \0  \0
00003a4

还可以用 -C 显示 16 进制 和 ASCII码:hexdump -C classes.dex

00000000  64 65 78 0a 30 33 35 00  12 8f b1 77 7a e9 19 91  |dex.035....wz...|
00000010  f2 0c ff ce a0 ce aa cd  8f 9d 80 7a ac 18 49 bf  |...........z..I.|
00000020  a4 03 00 00 70 00 00 00  78 56 34 12 00 00 00 00  |....p...xV4.....|
00000030  00 00 00 00 f8 02 00 00  14 00 00 00 70 00 00 00  |............p...|
00000040  08 00 00 00 c0 00 00 00  05 00 00 00 e0 00 00 00  |................|
00000050  01 00 00 00 1c 01 00 00  05 00 00 00 24 01 00 00  |............$...|
00000060  01 00 00 00 4c 01 00 00  38 02 00 00 6c 01 00 00  |....L...8...l...|
00000070  6c 01 00 00 74 01 00 00  81 01 00 00 84 01 00 00  |l...t...........|
00000080  92 01 00 00 96 01 00 00  ad 01 00 00 c1 01 00 00  |................|
00000090  d5 01 00 00 f0 01 00 00  04 02 00 00 07 02 00 00  |................|
000000a0  0b 02 00 00 20 02 00 00  28 02 00 00 2e 02 00 00  |.... ...(.......|
000000b0  34 02 00 00 39 02 00 00  42 02 00 00 4c 02 00 00  |4...9...B...L...|
000000c0  03 00 00 00 05 00 00 00  06 00 00 00 07 00 00 00  |................|
000000d0  08 00 00 00 09 00 00 00  0a 00 00 00 0c 00 00 00  |................|
000000e0  02 00 00 00 03 00 00 00  00 00 00 00 04 00 00 00  |................|
000000f0  04 00 00 00 78 02 00 00  0a 00 00 00 06 00 00 00  |....x...........|
00000100  00 00 00 00 0b 00 00 00  06 00 00 00 78 02 00 00  |............x...|
00000110  0b 00 00 00 06 00 00 00  70 02 00 00 05 00 01 00  |........p.......|
00000120  10 00 00 00 00 00 04 00  0f 00 00 00 01 00 03 00  |................|
00000130  11 00 00 00 04 00 02 00  00 00 00 00 04 00 01 00  |................|
00000140  0d 00 00 00 04 00 00 00  12 00 00 00 00 00 00 00  |................|
00000150  01 00 00 00 02 00 00 00  00 00 00 00 ff ff ff ff  |................|
00000160  00 00 00 00 f0 02 00 00  00 00 00 00 06 3c 69 6e  |.............<in|
00000170  69 74 3e 00 0b 48 65 6c  6c 6f 20 57 6f 72 6c 64  |it>..Hello World|
00000180  00 01 4c 00 0c 4c 48 65  6c 6c 6f 57 6f 72 6c 64  |..L..LHelloWorld|
00000190  3b 00 02 4c 4c 00 15 4c  6a 61 76 61 2f 69 6f 2f  |;..LL..Ljava/io/|
000001a0  50 72 69 6e 74 53 74 72  65 61 6d 3b 00 12 4c 6a  |PrintStream;..Lj|
000001b0  61 76 61 2f 6c 61 6e 67  2f 4f 62 6a 65 63 74 3b  |ava/lang/Object;|
000001c0  00 12 4c 6a 61 76 61 2f  6c 61 6e 67 2f 53 74 72  |..Ljava/lang/Str|
000001d0  69 6e 67 3b 00 19 4c 6a  61 76 61 2f 6c 61 6e 67  |ing;..Ljava/lang|
000001e0  2f 53 74 72 69 6e 67 42  75 69 6c 64 65 72 3b 00  |/StringBuilder;.|
000001f0  12 4c 6a 61 76 61 2f 6c  61 6e 67 2f 53 79 73 74  |.Ljava/lang/Syst|
00000200  65 6d 3b 00 01 56 00 02  56 4c 00 13 5b 4c 6a 61  |em;..V..VL..[Lja|
00000210  76 61 2f 6c 61 6e 67 2f  53 74 72 69 6e 67 3b 00  |va/lang/String;.|
00000220  06 61 70 70 65 6e 64 00  04 61 72 67 73 00 04 6d  |.append..args..m|
00000230  61 69 6e 00 03 6f 75 74  00 07 70 72 69 6e 74 6c  |ain..out..printl|
00000240  6e 00 08 74 6f 53 74 72  69 6e 67 00 0e e8 bf 99  |n..toString.....|
00000250  e6 98 af e4 b8 80 e4 b8  aa e6 89 8b e5 86 99 e7  |................|
00000260  9a 84 73 6d 61 6c 69 e5  ae 9e e4 be 8b 00 00 00  |..smali.........|
00000270  01 00 00 00 07 00 00 00  01 00 00 00 03 00 00 00  |................|
00000280  00 00 00 00 00 00 00 00  00 01 0f 07 00 00 00 00  |................|
00000290  0b 00 01 00 02 00 00 00  88 02 00 00 28 00 00 00  |............(...|
000002a0  62 00 00 00 00 00 00 00  00 00 12 32 13 03 ff ff  |b..........2....|
000002b0  18 04 00 00 01 00 00 00  00 00 1c 05 03 00 01 26  |...............&|
000002c0  22 07 04 00 70 10 02 00  07 00 1a 08 13 00 6e 20  |"...p.........n |
000002d0  03 00 87 00 0c 07 6e 10  04 00 07 00 0c 09 6e 20  |......n.......n |
000002e0  01 00 90 00 1a 01 01 00  6e 20 01 00 10 00 0e 00  |........n ......|
000002f0  00 00 01 00 00 09 90 05  0e 00 00 00 00 00 00 00  |................|
00000300  01 00 00 00 00 00 00 00  01 00 00 00 14 00 00 00  |................|
00000310  70 00 00 00 02 00 00 00  08 00 00 00 c0 00 00 00  |p...............|
00000320  03 00 00 00 05 00 00 00  e0 00 00 00 04 00 00 00  |................|
00000330  01 00 00 00 1c 01 00 00  05 00 00 00 05 00 00 00  |................|
00000340  24 01 00 00 06 00 00 00  01 00 00 00 4c 01 00 00  |$...........L...|
00000350  02 20 00 00 14 00 00 00  6c 01 00 00 01 10 00 00  |. ......l.......|
00000360  02 00 00 00 70 02 00 00  03 10 00 00 02 00 00 00  |....p...........|
00000370  80 02 00 00 03 20 00 00  01 00 00 00 88 02 00 00  |..... ..........|
00000380  01 20 00 00 01 00 00 00  90 02 00 00 00 20 00 00  |. ........... ..|
00000390  01 00 00 00 f0 02 00 00  00 10 00 00 01 00 00 00  |................|
000003a0  f8 02 00 00                                       |....|
000003a4

0x02■  DEX文件结构解析

先看下就行,不用着急,下面我们一步一步来,首先点击你的010Editor的这里:

对,就是箭头指的那里,点击之后,你会发现上面的有一片区域成了选中的颜色,这部分里面存储的就是Header中的数据了,下面我们根据Header的数据图以此来进行分析。

首先,我们看到DexHeader中每个数据前面有个u1或者u4,这个是什么意思呢?它们其实就是代表1个或者4个字节的无符号数。下面我们依次根据Header中的数据段进行解释。

1. 从第一个看起,magic[8];它代表dex中的文件标识,一般被称为魔数。是用来识别dex这种文件的,它可以判断当前的dex文件是否有效,可以看到它用了8个1字节的无符号数来表示,我们在010Editor中可以看到也就是“64 65 78 0A 30 33 35 00 ”这8个字节,这些字节都是用16进制表示的,用16进制表示的话,两个数代表一个字节(一个字节等于8位,一个16进制的数能表示4位)。这8个字节用ASCII码表转化一下可以转化为:dex.035(点击这里可以进行十六进制转ASCII,你可以试试:其中,'.' 不是转化来的)。目前,dex的魔数固定为dex.035。

2. 第二个是,checksum;  它是dex文件的校验和,通过它可以判断dex文件是否被损坏或者被篡改。它占用4个字节,也就是“5D 9D F9 59”。这里提醒一下,在010Editor中,其实可以分别识别我们在DexHeader中看到的这些字段的,你可以点一下这里:

你可以看到这个header列表展开了,其实我们分析下来就和它这个结构是一样的,你可以先看下,我们现在分析到了checksum中了,你可以看到后面对应的值是“59 F9 9D 5D”。咦?这好像和上面的字节不是一一对应的啊。对的,你可以发现它是反着写的。这是由于dex文件中采用的是小字节序的编码方式,也就是低位上存储的就是低字节内容,所以它们应该要反一下。

3. 第三个到了 signature[kSHA1DigestLen] 了,signature字段用于检验dex文件,其实就是把整个dex文件用SHA-1签名得到的一个值。这里占用20个字节,你可以自己点010Editor看一看。

4. 第四个 fileSize ;表示整个文件的大小,占用4个字节。

5. 第五个 headerSize ;表示 DexHeader 头结构的大小,占用4个字节。

可以看到它一共占用了112个字节,112对应的16进制数为70h。

6. 第6个是 endianTag ;代表 字节序标记,用于指定dex运行环境的cpu,预设值为0x12345678,对应在101Editor中为“78 56 34 12”(小字节序)。

7. 接下来两个分别是 linkSize 和 u4  linkOff ;这两个字段,它们分别指定了链接段的大小和文件偏移,通常情况下它们都为0。linkSize 为 0 的话表示静态链接。

8. 再下来就是 mapOff 字段了,它指定了DexMapList的文件偏移,这里我们先不过多介绍它,你可以看一下它的值为“14 04 00 00”,它其实对应的16进制数就是414h(别忘了小字节序),我们可以在414h的位置看一下它在哪里:

其实就是dex文件最后一部分内容。关于这部分内容里面是什么,我们先不说,继续往下看。

9. stringIdsSizestringIdsOff 字段:这两个字段指定了dex文件中所有用到的字符串的个数和位置偏移,我们先看stringIdsSize,它的值为:“1C 00 00 00”,16进制的1C也就是十进制的28,也就是说我们这个dex文件中一共有28个字符串,然后stringIdsOff为:“70 00 00 00”,代表字符串的偏移位置为70h,这下我们找到70h的地方:

这下我们就要先介绍一下DexStringId这个结构了,图中从70h开始,所有被选中的都是DexStringId这种数据结构的内容,DexStringId代表的是字符串的位置偏移,每个DexStringId占用4个字节,也就是说它里面存的还不是真正的字符串,它们只是存储了真正字符串的偏移位置。

下面我们先分析几个看看,

① 取第一个“B2 02 00 00”,它代表的位置偏移是2B2h,我们先找到这个位置:

可以发现我一共选中了10个字节,这10个字节就表示了一个字符串。下面我们看一下dex文件中的字符串是如何表示的。dex中的字符串采用了一种叫做MUTF-8这样的编码,它是经过传统的UTF-8编码修改的。在MTUF-8中,它的头部存放的是由uleb128编码的字符的个数。(至于uleb128编码是什么编码,这里我不详细展开说,有兴趣的可以搜索看看。)

也就是说在“08 3C 63 6C 69 6E 69 74 3E 00”这些字节中,第一个08指定的是后面需要用到的编码的个数,也就是8个,即“ 3C 63 6C 69 6E 69 74 3E”这8个,但是我们为什么一共选中了10个字节呢,因为最后一个空字符“0”表示的是字符串的结尾,字符个数没有把它算进去。下面我们来看看“ 3C 63 6C 69 6E 69 74 3E”这8个字符代表了什么字符串:

依旧可以点这里查询ASCII ( http://www.ab126.com/goju/1711.html )。(要说明的一点是,这里凑巧这几个uleb128编码的字符都用了1个字节,所以我们可以这样进行查询,uleb128编码标准用的是1~5个字节, 这里只是恰好都是一个字节)。也就是说上面的70h开始的第一个DexStringId指向的其实是字符串“<clinit>”(但是貌似我们的代码中没有用到这个字符串啊,先不用管,我们接着分析)。再看到这里:

② 刚刚我们分析到“B2 02 00 00”所指向的真实字符串了,下面我们接着再分析一个,我们直接分析第三个,不分析第二个了。第三个为“C4 02 00 00”,对应的位置也就是2C4h,我们找到它:

看这里,这就是2C4h的位置了。我们首先看第一个字符,它的值为0Bh,也就是十进制的11,也就是说接下来的11个字符代表了它的字符串,我们依旧是查看接下来11个字符代表的是什么,经过查询整理:

依旧可以点这里查询ASCII ( http://www.ab126.com/goju/1711.html )。上面就是“HelloDalvik”这个字符串,可以看看我们的代码,我们确实用了一个这样的字符串,bingo。下面剩下的字符串就不分析了。经过整理,可以整理出我们一共用到的28个字符串为:

ok,字符串这里告一段落,下面我们继续看DexHeader的下面的字段。头好晕~乎乎

噢,读了,还不能结束呢,你现在可以看一下最开始发的那张dex结构图了:

看到了吧,我们这半天分析的stringIdsSize 和 stringIdsOff字段指向的位置就是上面那个箭头指向的位置,它们里面存储的是真实字符串的位置偏移,它们都存储在data区域。(先透露一下,后面我们要分析的几个也和stringIdsSize 与stringIdsOff字段类似,它们里面存储的基本都是位置偏移,并不是真正的数据,真正的数据都在data区域)

好,我们继续。

10. 继续看DexHeader图,我们现在该typeIdsSizetypeIdsOff了。它们代表什么呢?它们代表的是类的类型的数量和位置偏移,也是都占4个字节,下面我们看它们的值

可以看到,typeIdsSize的值为9h,也就是我们dex文件中用到的类的类型一共有9个,位置偏移在E0h位置,下面我们找到这个位置

看到了吧,我选中的位置就是了。这里我们又得介绍一种数据结构了,因为这里的数据也是一种数据结构的数据组成的。那就是DexTypeId,也就是说选中的内容都是DexTypeId这种数据,这种数据结构中只有一个变量,如下所示:

struct DexTypeId{u4 descriptorIdx;   /*指向DexStringId列表的索引*/
}

看到了吧,这就是DexTypeId数据结构,它里面只有一个数据descriptorIdx,它的值的内容是DexStringId列表的索引。还记得DexStringId是什么吗?在上面我们分析字符串时,字符串的偏移位置就是由DexStringId这种数据结构描述的,也就是说descriptorIdx指向的是所有的DexStringId组成的列表的索引。上面我们整理出了所有的字符串,你可以翻上去看看图。然后我们看这里一共是9个类的类型代表的都是什么。先看第一个“05 00 00 00”,也就是05h,即十进位的5。然后我们在上面所有整理出的字符串看看5索引的是什么?翻上去可以看到是“I”。接下来我们依次整理这些类的类型,也可以得到类的类型的列表

看到了吧,这就是我们dex文件中所有用到的类的类型。比如“I”代表的就是int,LHelloWorld代表的就是HelloWorld,Ljava/io/PrintStream代表的就是java.io.PrintStream。后面的几个先就不说了。我们接着往下分析。

11. 这下到了protoIdsSizeprotoIdsOff了,它们代表的是dex文件中方法原型的个数和位置偏移。我们先看它们的值

如上图就是它们的值了,protoIdsSize的值为十进制的7,说明有7个方法原型,然后位置偏移为104h,我们找到这个位置

看到了吧,这里就是了。对,下面又有新的数据结构了。这下一个数据结构不能满足这块的内容了,我们先看第一个数据结构,DexProtoId

struct DexProtoId{u4 shortyIdx;          /*指向DexStringId列表的索引*/u4 returnTypeIdx;     /*指向DexTypeId列表的索引*/u4 parametersOff;       /*指向DexTypeList的位置偏移*/
}

可以看到,这个数据结构由三个变量组成。第一个shortyIdx它指向的是我们上面分析的DexStringId列表的索引,代表的是方法声明字符串。第二个returnTypeIdx它指向的是 我们上边分析的DexTypeId列表的索引,代表的是方法返回类型字符串。第三个parametersOff指向的是DexTypeList的位置索引,这又是一个新的数据结构了,先说一下这里面 存储的是方法的参数列表。可以看到这三个参数,有方法声明字符串,有返回类型,有方法的参数列表,这基本上就确定了我们一个方法的大体内容。

我们接着看看DexTypeList这个数据结构,看看参数列表是如何存储的。

struct DexTypeList{u4 size;      /*DexTypeItem的个数*/DexTypeItem list[1];  /*DexTypeItem结构*/
}

看到了嘛,它有两个参数,其中第一个size说的是DexTypeItem的个数,那DexTypeItem又是啥咧?它又是一种数据结构。我们继续看看

struct DexTypeItem{u2 typeIdx;               /*指向DexTypeId列表的索引*/
}

恩,还好,里面就一个参数。也比较简单,就是一个指向DexTypeId列表的索引,也就是代表参数列表中某一个具体的参数的位置。

分析完这几个数据结构了,下面我们具体地分析一个类吧。别走神,我们该从上图的104h开始了。

在104h这里,由于 都是DexProtoId这种数据结构的数据,一个DexProtoId一共占用12个字节。所以,我们取前12个字节进行分析。“06 00 00 00,00 00 00 00,94 02 00 00”,这就是那12个字节了。首先“06 00 00 00”代表的是shortyIdx,它的值是指向DexStringId列表的索引,我们找到DexStringId列表中第6个对应的值,也就是III,说明这个方法中声明字符串为三个int。接着,“00 00 00 00”代表的是returnTypeIdx,它的值指向的是DexTypeId列表的索引,我们找到对应的值,也就是I,说明这个方法的返回值是int类型的。最后,我们看“94 02 00 00”,它代表的是DexTypeList的位置偏移,它的值为294h,我们找到这个位置

这里是DexTypeList结构,首先看前4个字节,代表的是DexTypeItem的个数,“02 00 00 00 ”也就是2,说明接下来有2个DexTypeItem的数据,每个DexTypeItem占用2个字节,也就是两个都是“00 00”,它们的值是DexTypeId列表的索引,我们去找一下,发现0对应的是I,也就是说它的两个参数都是int型的。因此这个方法的声明我们也就确定了。也就是int(int,int),可以看看我们的源代码,getNumber方法确实是这样的。好,第一个方法就这样分析完了,下面我们依旧是将这些方法的声明整理成列表,后面可能有数据会指向它们的索引。

终于又完了一个。我们准备继续下面的。累了就先去听听歌吧,歇一歇再看 -_-

12. fieldIdsSizefieldIdsOff字段。这两个字段指向的是dex文件中字段名的信息。我们看到这里

可以看到,fieldIdsSize为3h,说明共有3个字段。fieldIdsOff为158h,说明偏移为158h,我们继续看到158h这里

咳咳,又该新的数据结构了,再忍一忍,接下来的数据结构是DexFieldId,我们看下

struct DexFieldId{u2 classIdx;       /*类的类型,指向DexTypeId列表的索引*/u2 typeIdx;     /*字段类型,指向DexTypeId列表的索引*/u4 nameIdx;     /*字段名,指向DexStringId列表的索引*/
}

可以看到,这三个数据都是指向的索引值,具体的就不说了,看后面的备注就是。我们依旧是分析一下第一个字段,“01 00 ,00 00,13 00 00 00”,类的类型为DexTypeId列表的索引1,也就是HelloWorld,字段的类型为DexTypeId列表中的索引0,也就是int,字段名为DexStringId列表中的索引13h,即十进制的19,找一下,是a,也就是说我们这个字段就确认了,即int HelloWorld.a。这不就是我们在HelloWorld.java文件里定义的变量a嘛。然后我们依次把我们所有的3个字段都列出来:

〇int HelloWorld.a , ①java.lang.String HelloWorld.b ,②java.io.PrintStream java.lang.System.out

ok,先告一段落。继续分析下一个

13. methodIdsSizemethodIdsOff字段。这俩字段指明了方法所在的类、方法的声明以及方法名。我们看看

先是,methodIdsSize,为Ah,即十进制的10,说明共有10个方法。methodIdsOff,为170h,说明它们的位置偏移在170h。我们看到这里

对对对,又是新的数据结构,不过这个和上个一样简单,请看DexMethodId

struct DexMethodId{u2 classIdx;      /*类的类型,指向DexTypeId列表的索引*/u2 protoIdx;        /*声明类型,指向DexProtoId列表的索引*/u4 nameIdx;        /*方法名,指向DexStringId列表的索引*/
}

对吧,这个也简单,三个数据也都是指向对应的结构的索引值。我们直接分析一下第一个数据,“01 00, 04 00, 00 00 00 00”,首先,classIdx,为1,对应DexTypeId列表的索引1,也就是HelloWorld;其次,protoIdx,为4,对应DexProtoId列表中的索引4,也就是void();最后,nameIdx,为0,对应DexStringId列表中的索引0,也就是<clinit>。因此,第一个数据就出来了,即void HelloWorld.<clinit>() 。后面的不进行分析了,我们依旧是把其余的9个方法列出来

好了,这个就算分析完了。下面真正开始我们的重头戏了。先缓一缓再继续吧。

14. classDefsSizeclassDefsOff字段。这两个字段指明的是dex文件中类的定义的相关信息。我们先找到它们的位置。

classDefsSize字段,为1,也就是只有一个类定义,classDefsOff,为1C0h,我们找到它的偏移位置。

这里就是了,到了这里,你现在应该也知道又有新的数据结构了。对的,接下来的数据结构是DexClassDef,请看

struct DexClassDef{u4 classIdx;      /*类的类型,指向DexTypeId列表的索引*/u4 accessFlags;     /*访问标志*/u4 superclassIdx;   /*父类类型,指向DexTypeId列表的索引*/u4 interfacesOff;   /*接口,指向DexTypeList的偏移*/u4 sourceFileIdx; /*源文件名,指向DexStringId列表的索引*/u4 annotationsOff;    /*注解,指向DexAnnotationsDirectoryItem结构*/u4 classDataOff;   /*指向DexClassData结构的偏移*/u4 staticValuesOff;  /*指向DexEncodedArray结构的偏移*/
}

不多说了,我们直接根据结构开始分析吧,反正就只有一个类定义。classIdx为1,对应DexTypeId列表的索引1,找到是HelloWorld,确实是我们源程序中的类的类型。accessFlags为1,它是类的访问标志,对应的值是一个以ACC_开头的枚举值,1对应的是 ACC_PUBLIC,你可以在010Editor中看一下,说明我们的类是public的。superclassIdx的值为3,找到DexTypeId列表中的索引3,对应的是java.lang.object,说明我们的类的父类类型是Object的。interfaceOff指向的是DexTypeList结构,我们这里是0说明没有接口。如果有接口的话直接对应到DexTypeList,就和之前我们分析的一样了,这里不多解释,有兴趣的可以写一个有接口的类验证下。再下来sourceFileIdx指向的是DexStringId列表的索引,代表源文件名,我们这里位4,找一下对应到了字符串"HelloWorld.java",说明我们类程序的源文件名为HelloWorld.java。annotationsOff字段指向注解目录接口,根据类型不同会有注解类、注解方法、注解字段与注解参数,我们这里的值为0,说明没有注解,这里也不过多解释,有兴趣可以自己试试。

接下来是classDataOff了,它指向的是DexClassData结构的位置偏移,DexClassData中存储的是类的数据部分,我们开始详细分析一下它,首先,还是先找到偏移位置3F8h

接着,我们看看DexClassData数据结构

struct DexClassData{DexClassDataHeader           header;         /*指定字段与方法的个数*/DexField*             staticFields;       /*静态字段,DexField结构*/DexField*         instanceFields;  /*实例字段,DexField结构*/DexMethod*            directMethods;      /*直接方法,DexMethod结构*/DexMethod*           virtualMethods;     /*虚方法,DexMethod结构*/
}

可以看到,在DexClassData结构中又引入了三种结构,我们一起写出来看一下吧

struct DexClassDataHeader{u4 staticFieldsSize;   /*静态字段个数*/u4 instanceFieldsSize;    /*实例字段个数*/u4 directMethodsSize; /*直接方法个数*/u4 virtualMethodsSize;  /*虚方法个数*/
}struct DexField{u4 fieldIdx;       /*指向DexFieldId的索引*/u4 accessFlags;      /*访问标志*/
}struct DexMethod{u4 methodIdx;     /*指向DexMethodId的索引*/u4 accessFlags;     /*访问标志*/u4 codeOff;     /*指向DexCode结构的偏移*/
}/*指向DexFieldId的索引*/u4 accessFlags;     /*访问标志*/
}struct DexMethod{u4 methodIdx;     /*指向DexMethodId的索引*/u4 accessFlags;     /*访问标志*/u4 codeOff;     /*指向DexCode结构的偏移*/
}

代码中的注释写的也都很清楚了,我们就不多说了。但是请注意,在这些结构中的u4不是指的占用4个字节,而是指它们是uleb128类型(占用1~5个字节)的数据。关于uleb128还是不多说,想了解的可以自己查查看。

好,接下来开始分析,对于DexClassData,第一个为DexClassDataHeader,我们找到相应的位置,第一个staticFieldsSize其实只占用了一个字节,即01h就是它的值,也就是说共有一个静态字段,接下来instanceFieldsSize,directMethodsSize,virtualMethodsSize也都是只占用了一个字节,即实例字段的个数为1,直接方法的个数为3,虚方法的个数为1。(这里只是凑巧它们几个都占用一个字节,并不一定是只占用一个字节,这关于到uleb128数据类型,具体可以自己了解下)。

然后接下来就是staticFields了,它对应的数据结构为DexField,可以看到,第一个fieldIdx,是指向DexFieldId的索引,值为1,找到对应的索引值为java.lang.String HelloWorld.b。第二个accessFlags,值为8,对应的ACC_开头的数据为ACC_STATIC(可以在010Editor中对应查看一下),说明我们这个静态字段为:static java.lang.String HelloWorld.b。可以对应我们的源代码看一下,我们确实定义了一个static的b变量。

接着看instanceFields,它和staticFields对应的数据结构是一样的,我们直接分析,第一个fieldIdx,值为0,对应的DexField的索引值为int HelloWorld.a。第二个accessFlags,值为0,对应的ACC_开头的数据为空,就是什么也没有。说明我们这个实例字段为:int HelloWorld.a。可以对应我们的源码 看看,我们确实定义了一个a实例变量。

再接着,根据directMethodsSize,有3个直接方法,我们先看第一个,它对应的数据结构是DexMethod,首先methodIdx指向的是DexMethodId的索引,值为0,找到对应的索引值为void HelloWorld.<clinit>()。然后accessFlages为......为......为....我的个天!我以为就这样能蒙混过关了,没想到还真碰到一个uleb128数据不是占用一个字节的,这个accessFlags对应的值占用了三个字节,“88 80 04”,为什么?因为是按照uleb128格式的数据读出来的(还是自己去查查吧,这个坑先不填了,其实这种数据也不麻烦,就是前面字节上的最高位指定了是否需要下一个字节上的内容)。“88 80 04”对应的ACC_开头的数据为 ACC_STATIC ACC_CONSTRUCTOR,表明这个方法是静态的,并且是构造方法。最后,看看codeOff,它对应了DexCode结构的偏移,DexCode中存放了方法的指令集等信息,也就是真正的代码了。我们暂且不分析DexCode,就先看看它的偏移位置为“E0 03”,这个等于多少呢?uleb128转化为16进制数结果为:1E0h。也就是DexCode存放在偏移位置1E0h的位置上。

具体的DexCode我们就先不分析了,因为它里面存放的一些指令局需要根据相关资料一一查找,有兴趣的自己可以找资料看看。剩下的两个直接方法我们也不分析了。

接下来,我们看根据virtualMethodsSize,有1个虚方法,我们直接看。首先methodIdx的值为2,对应的DexMethodId的索引值为int HelloWorld.getNumber(int, int)。然后accessFlags为1,对应的值为ACC_PUBLIC,表明这是一个public类。codeOff为“FC 04”,对应的位置为27Ch,这里就不上图了,自己找找吧。

好了,我们整个DEX文件的结构就这样从DexHeader开始基本分析完了,好累啊,不过这样分析一遍,对DEX文件的格式会有更深刻的认识。总是看别人的真不如自己来一遍来的实在!

0x03■  参考资料

参考资料:

《Android软件安全与逆向分析》.非虫

一篇文章带你搞懂 DEX 文件的结构相关推荐

  1. 一篇文章带你搞懂网络层(网际层)-- 地址篇

    网络层(Network Layer)是OSI模型中的第三层(TCP/IP模型中的网际层),提供路由和寻址的功能,使两终端系统能够互连且决定最佳路径,并具有一定的拥塞控制和流量控制的能力.相当于发送邮件 ...

  2. 一篇文章带你搞懂微信小程序的开发过程

    点击上方"前端进阶学习交流",进行关注 回复"前端"即可获赠前端相关学习资料 今 日 鸡 汤 只解沙场为国死,何须马革裹尸还. 大家好,我进阶学习者. 前言 小 ...

  3. 谷歌SEO很复杂?一篇文章带你搞懂它(外贸人必读)

    这篇文章是对谷歌SEO流程的一个梳理,此文会用通俗易懂的语言告诉你做Google SEO必须知道的常识.建议和谷歌优化的方法思路. 任何关于谷歌SEO的疑问,可到此文⬇️留言,免费咨询: Google ...

  4. 一篇文章带你搞懂慢SQL以及优化的策略

    文章目录 一.什么是慢SQL ? 二.为什么要对慢SQL进行优化? 三.数据库性能 1. 最大数据量 2. 最大并发数 3. 查询耗时0.5秒 4. 具体实施 四.数据库表的设计 1. 数据类型 2. ...

  5. 一篇文章带你搞懂前端面试技巧及进阶路线

    大家好,我是若川.最近有很多朋友给我后台留言: 自己投了不少简历,但是收到的面试邀请却特别少: 好不容易收到了大厂的面试邀请,但由于对面试流程不清楚,准备的特别不充分,结果也挂了: 对于面试官的问题, ...

  6. 一篇文章带你搞懂数据链路层

    数据链路层,简称链路层.两个主机之间的数据传输,总是在一段一段的链路上面传送的,也就是说,在两个相邻结点之间(主机与路由器之间 或者 两个路由器之间)传送数据是直接传送的(点对点).这时,就需要使用专 ...

  7. 一篇文章带你搞懂应用层(万维网篇)

    每个应用层协议都是为了解决某一类应用问题,而问题的解决又往往是通过位于不同主机中的多个应用进程之间的通信和协同工作来完成的.应用层的具体内容就是规定应用进程在通信时所遵循的协议.应用层的许多协议都是基 ...

  8. 一篇文章带你搞懂Redis(超级无敌最最最详细版本)(命令大全)(真·收藏必备)

    (本文近两万字,阅读时间可能较久,建议收藏以便查询使用) 目录 Redis诞生背景功能简介 Redis的下载与安装 Redis键的基本操作 Redis键名查询 Redis键的类型查询 Redis键的重 ...

  9. 一篇文章带你搞懂JS对象的自我销毁

    在日常的JS组件开发中,往往会有一些较为复杂的DOM操作及事件监听,尤其是在处理UI层面的widgets时候更为明显.常常会花很多精力在对象的init上,而当组件需要被移除时则仅仅是把所在DOM草草的 ...

最新文章

  1. Linux学习日记:第二天
  2. 魔兽服务器状态页面,谁动了我的服务器 WOW大服务器优势何时展现
  3. wordpress html音乐,WordPress引用百度Ting音乐方法
  4. 转】未指定 INSTANCESHAREDWOWDIR 命令行值。如果指定INSTANCESHAREDDIR 值,则必须指定该值 ....
  5. 进程和线程的深入理解
  6. 如何对大数据进行数据分析
  7. 电脑耳机没声音怎么设置?Windows系统适用
  8. C# 自制微信登录窗口,100%还原,数据库(SQL Server)
  9. VTK读取序列DCM格式医学图像
  10. 全闪存存储的数据库加速场景应用
  11. 该不该从大学退学的讨论
  12. 如何设置无线路由连接无线wifi
  13. 安卓内存监控工具,2021年Android面试心得,系列教学
  14. 自动阅读是如何赚取收益的
  15. 软通python编程题_软通动力笔试(带参考答案)
  16. LeetCode_素数筛_中等_204.计数质数
  17. 3dmax:基于CAD图纸进行3dmax室内建模基础步骤攻略
  18. Python爬虫入门实战,图文详细教学,一看就懂
  19. Java 3D——基本图形功能及源代码
  20. 虹膜识别 讲的特别好

热门文章

  1. 最全Java架构师130面试题:微服务、高并发、大数据、缓存等中间件
  2. 技术动态 | 去中心化知识图谱协作平台建设实践
  3. docker的简单操作和端口映射
  4. 百度作业帮-产品分析
  5. Android自动化测试探索
  6. Android官方开发文档Training系列课程中文版:高效显示位图之管理位图内存
  7. tensorflow--GPU
  8. SinglepassTextCluster项目:基于single-pass算法思想的自动文本聚类组件
  9. Python总结:保留小数点任意位round函数不够精确
  10. Django - 模板相关