大家好,又见面了,我是你们的朋友全栈君。
目录
正式写之前先说两句废话,这篇笔记是我去年的时候创建的,当时是写了一部分,后来因为乱七八糟的事情太忙了,结果放到草稿箱里给忘记了,昨天回过头去复习这部分的内容偶然间发现了它,还是个没完成的它,大写的尴尬啊,所以急忙给补上了,此处鄙视一下自己!
下面进入今天的正题——解析class文件和dex文件,做个笔记,方便总结和回顾。
能够被JVM识别,加载并执行的文件格式,说白了就是一种文件格式,像mp4、doc、txt这种文件格式一样,只不过class文件中存储的是应用程序,并且有很多语言都可以生成class文件,并不是只有Java语言,比如:Scala、Python、Small等等都可以生成class字节码来被JVM识别并且执行。
两种方式,一种是通过IDE开发工具自动build,另一种是通过javac,即java compiler编译命令手动执行生成class文件。
生成了class之后,执行方式同样也是两种,一种是通过点击IDE的Run执行,另一种是通过java命令手动执行。
这里来看一下如何手动生成class文件?
首先电脑上基本的Java开发环境要配置完成,包括环境变量的配置,有了这个基础才能具体实战。
首先我在自己电脑E盘中hotfix文件夹下准备了一个.java的源文件,然后打开电脑控制台,查找一下这个文件是否存在,如下图:
然后我们进入到hotfix目录下面查看里面的文件,并且打开这个文件,这时候系统会调起电脑中的其他程序加载对应的文件:
可以看到就一个Hello.java的源文件,然后里面就打印一句话:Hello Jarchie。下面我们来手动执行javac -Hello.java命令生成class文件,然后继续执行java Hello命令,执行这个应用程序,观察结果,确实打印出了代码中的内容,如下图所示:
记录一个类文件的所有信息,记住是所有!包括一个类的名称、类中所有的方法、类中所有的变量等等,class文件中所包含的信息,远远多于java源代码中所能看到的信息。举个例子,为什么在一个类中并没有定义this,super这样的关键字,但是我们却可以使用这些关键字来调用我们父类的方法或者调用当前类的变量,那是因为在生成class字节码文件的时候,java虚拟机帮我们记录了它的当前类this关键字和父类super关键字,所以可以这样使用。
从整体上看,首先它是一种8位字节的二进制流文件,这一点与大部分文件都一样,比如音视频文件都是二进制流。其次它的各个数据按顺序紧密的排列,没有丝毫的间隙,不像有些文件,为了读取上的方便,会做一些填充,比如每80个字节定为一行,那么紧密排列的好处就是可以减少class文件的体积,让JVM在加载class文件的时候更加快速。最后,每个类或接口都单独占据一个class文件,这样做的好处是每个类或者接口都可以独自管理自己内部的内容,而无需相互交叉,这是class文件在宏观上的三个特点。
上图中呢就是class文件中的所有字段,下面来具体说说每个字段的主要作用。
magic:无符号四字节类型,主要作用加密段,类似于文件的md5加密,主要是给虚拟机用来判断当前class文件是否被篡改过。
minor_version:当前class文件最小可以被哪个版本的jdk加载,就是它最小适配的jdk版本。
major_version:当前class文件是由哪个版本的jdk生成的。
constant_pool_count:当前class文件中常量池的数量,通常来说都是一个常量池。
constant_pool:真正的常量池,类型cp_info即结构体类型。先来看三个比较好理解的,即CONSTANT_Integer_info、CONSTANT_Long_info、CONSTANT_String_info,分别存储class文件中所有的Integer类型、Long类型、String类型,当然还有short、byte等类型也有对应的字段去存储,这里就不再一一列举了。再来看三个比较复杂的,即CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info,这三个字段分别记录了类相关的信息、类中成员变量和类中方法相关的信息,其中CONSTANT_Class_info不仅记录了当前类的一些信息,还记录了当前类所引用到的类的信息,另外CONSTANT_Fieldref_info、CONSTANT_Methodref_info中记录的并不是对应类型的真正信息,而是存储的索引地址,这些索引最终指向的又是对应的CONSTANT_Integer_info这些,所以所有内容实际上都是存储在常量池中的Integer_info、String_info这些字段里面的。
access_flags:作用域标志,比如public类型或者private类型,它的取值范围如下表所示:
this_class&super_class:JVM默认填充当前类以及当前类的父类。
interfaces_count&interfaces:class文件继承的接口数量和接口,只记录当前类直接继承的接口,不会记录间接继承的接口。
fields_count&fields:class文件包含的所有成员变量及数量,其中fields是结构体类型,表明它内部还包含了其他内容,主要是每个成员变量的name、所属的类以及类型。
methods_count&methods:class文件中所有的方法相关的内容,其中methods是结构体类型,内部包含每个方法的name、access_flag作用域、所属类以及类型。
attribute_count&attributes:记录类的一些属性相关的内容,上面这些字段没有包含到的内容都会放到attribute中,比如类上面的注解。
总体上看,一个class文件中定义了这么多字段,并且这些字段里面可能又包含字段,就像我们的JSON文件一样,是一层套一层的,通过这些字段的详细定义,我们的Java虚拟机就可以找到每个class文件中的任何内容。
讲了这么多class字节的字段以及他们的作用,是不是还是没什么感觉,很虚无缥缈枯燥乏味的知识点是不是都不太想看,那么下面我们就具体的来查看一下class文件中的内容,看一下它的内部结构,眼见为实嘛。我们就以上面生成的Hello.class为例,来具体看一下,来验证一下我们的这些理论知识。
首先介绍一款软件——010 Editor,它是主要用来查看二进制文件的,不仅可以查看class文件,后面还能查看dex文件。
先来整体上看一下,如下图所示,把这个结构体缩起来以后,选中的内容表明整个class文件由这些二进制的数据组成。
展开struct将结构体打开后,具体来看一下,如下图所示:
先以第一个字段为例说明一下怎么看,首先来看下方struct部分展开后会出现一个列表,如果选中第一个字段magic(红线圈出的位置),上方十六进制区域会显示出这个字段所在的位置,然后下方开始位置为0h,也就是说它是从第0行第0列开始,大小为4h,意思是跨度为4列,这里的4是十六进制。按照这个说明,我给大家选中了一个CONSTANT_Methodref,它是记录了class字节中的一个方法,然后展开可以看到这里面有几个字段,首先是有一个标志,然后一个class_index,表明这个方法是属于哪个类的,第三个字段表明这个方法的名字和方法的类型,注意这里都是index,所以这两个字段都是一个指针,根据指针可以查找到具体指向的内容。这里面的内容有很多,最好就是下载一下这个软件,自己对应着去查看一下,下面都是constant_pool,这里就不再一一的看了,翻到最下面,可以看到我们的access_flags,这里显示是public类型,再往下看还能看到this、super、方法、成员变量这些内容,从这里也能看出,都是论证了我们之前的理论知识的。如下图所示:
1、内存占用大,不合适移动端:每一个class文件包括很多的常量池,以及所有的field、method,而我们一个应用中有成百上千个类,是非常常见的,如果单纯的使用class字节码这种文件去存储类的信息的话,那么在移动端是不现实的,因为移动端的内存是非常少的。
2、堆栈的加载模式,加载速度慢;
3、文件IO操作多,类查找和加载慢:每个class文件只存储了一个java源文件中的所有的信息,每次去加载一个新的class的时候,都要去执行一遍加载查询,所以相对来说查找较慢。
class文件还有一些其他的缺点,正是因为这些缺点,所以导致移动开发不适合使用class字节码文件,所以接下来来看一下dex文件是如何规避这些缺点的,并且还做了哪些优化。
能够被DVM(Dalvik Virtual Machine,是Google专门为Android平台开发的虚拟机,运行在Android运行时库中)识别,加载并执行的文件格式。同样的它不仅能由Java源文件生成,还能由C/C++生成。这也说明了Android编程不仅可以使用Java语言,也可以使用C/C++来编写Android应用程序。
两种方式:第一种是通过IDE自动帮我们build生成,第二种是手动通过dx命令去生成dex文件。做过Android逆向的人肯定都知道,我们去反编译一个apk时,首先会先把apk解压缩,然后会发现里面会有一个classes.dex文件,然后可以通过dex2jar将.dex文件转化为.jar文件,然后再通过jd-gui这个工具就可以查看源代码了。这里就是简单的说一下,主要是想说如何快速看到dex文件。那么我们该如何手动通过dx命令生成dex文件呢?
第一步:需要配置一个环境变量,找到android sdk目录,然后找到build-tools下面具体的某个版本的文件夹打开,里面可以看到dx工具,然后把这个目录复制下来,这就是要配置的环境变量的目录,具体配置环境变量的方式这里就不详细说了,不会的自行谷歌或者百度。
第二步:创建一个Hello.java文件,我在里面随便打印了一句话”Welcome to jarchie520@gmail.com”,代码很简单:
然后通过命令行 javac -target 1.6 -source 1.6 Hello.java 指定了jdk版本编译,这里为了防止某些高版本的手机上跑不起来,那通过这一步就生成了Hello.class文件。
第三步:通过 dx –dex –output [输出的dex文件名 ] [要执行的class文件名] 这个命令就可以生成dex文件了,给大家举个栗子如下图:
第一步:要执行dex文件首先需要一台手机,我这里将我的华为手机连上了我的电脑,打开开发者模式,并开启USB调试。
第二步:将我们生成的Hello.dex文件push到手机中,打开控制台,通过adb push [需要push的文件名] [push到的位置] 命令将文件push到了我手机的SD卡上。
第三步:通过adb shell 命令进入手机的控制台,这里我的是进入到了我的这部华为手机的控制台。
第四步:通过 dalvikvm -cp /sdcard/Hello.dex Hello命令执行dex文件,这里-cp是指定dex文件的路径,Hello是要执行的class name,这样就可以看到执行结果了,跟我们代码里写的一样是不是,这里也给大家截了个图:
记录整个工程中所有类文件的信息,记住是整个工程!
文件头 | header | 文件头 |
---|---|---|
索引区 | string_ids | 字符串的索引 |
type_ids | 类型的索引 | |
proto_ids | 方法原型的索引 | |
field_ids | 域的索引 | |
method_ids | 方法的索引 | |
数据区 | class_defs | 类的定义区 |
data | 数据区 | |
link_data | 链接数据区 |
上面的表格中表明了dex文件格式有哪些区域,从表中可以看到主要分为三个部分,第一部分是文件头header,主要记录了dex文件的信息以及所有字段的大致分布。第二部分是索引区,主要包含了中间的一些字段,这部分完全定义了整个dex文件所有的类、方法、存储的位置等。第三部分是数据区,分为普通数据区和链接数据区,链接数据区主要是对一些动态链接库so的指向。
dex文件头的各个字段的具体名称及说明信息见下图,它主要是对dex文件的整体做了介绍,介绍了它的大小、各个区段及对应的起始位置和偏移量等:
下面我们打开010 Editor软件来具体查看一下dex文件的结构:
我们看到结果中显示的结构体名称和我们表格中的区域是一致的,最上面是文件头,将文件头展开就是图片中展示的那些内容,文件头下面就是全部的索引区,每个结构体代表了不同的索引区,比如第一个就是字符串索引,它记录了整个应用中所有的字符串,可以看到它是从70h开始的,它的大小是38h,在上图中我也用蓝色选中了这部分内容,具体的查看方法和hello.class文件是类似的,大家可以自己对照着将结构体展开详细的看一下,这里就不再一一展开详述了,因为内容确实比较多,总的来说微观结构和整体结构中说的是一一对应的。对于数据区的内容没有单独列出来,其实就是value对应的这部分内容,即通过索引就可以到数据区找到它对应的值。
从下面这张图就能够很清楚的看出它们的异同了:
写到这里算是粗略的写完了,中间可能有的地方说的并不是很清楚,说实话我理解的也并不是太深入,有什么不懂的大家留言讨论吧,天也不早了,人也很少了,大家洗洗睡吧!
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/153544.html原文链接:https://javaforall.cn