解析Java Class 文件过程
前言:
身为一个java程序员,怎么能不了解jvm呢,倘若想学习jvm,那就又必须要了解class文件,class之于虚拟机,就如鱼之于水,虚拟机因为class而有了生命。《深入理解java虚拟机》中花了一整个章节来讲解class文件,可是看完后,一直都还是迷迷糊糊,似懂非懂。正好前段时间看见一本书很不错:《自己动手写java虚拟机》,作者利用go语言实现了一个简单的jvm,虽然没有完整实现jvm的所有功能,但是对于一些对jvm稍感兴趣的人来说,可读性还是很高的。作者讲解的很详细,每个过程都分为了一章,其中一部分就是讲解如何解析class文件。
这本书不太厚,很快就读完了,读完后,收获颇丰。但是纸上得来终觉浅,绝知此事要躬行,我便尝试着自己解析class文件。go语言虽然很优秀,但是终究不熟练,尤其是不太习惯其把类型放在变量之后的语法,还是老老实实用java吧。
话不多说,先贴出项目地址:https://github.com/halfstackdeveloper/classreader
class文件
什么是class文件?
java之所以能够实现跨平台,便在于其编译阶段不是将代码直接编译为平台相关的机器语言,而是先编译成二进制形式的java字节码,放在class文件之中,虚拟机再加载class文件,解析出程序运行所需的内容。每个类都会被编译成一个单独的class文件,内部类也会作为一个独立的类,生成自己的class。
基本结构
随便找到一个class文件,用sublime text打开是这样的:
是不是一脸懵逼,不过java虚拟机规范中给出了class文件的基本格式,只要按照这个格式去解析就可以了:
classfile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; }
classfile中的字段类型有u1、u2、u4,这是什么类型呢?其实很简单,就是分别表示1个字节,2个字节和4个字节。
开头四个字节为:magic,是用来唯一标识文件格式的,一般被称作magic number(魔数),这样虚拟机才能识别出所加载的文件是否是class格式,class文件的魔数为cafebabe。不只是class文件,基本上大部分文件都有魔数,用来标识自己的格式。
接下来的部分主要是class文件的一些信息,如常量池、类访问标志、父类、接口信息、字段、方法等,具体的信息可参考《java虚拟机规范》。
解析
字段类型
上面说到classfile中的字段类型有u1、u2、u4,分别表示1个字节,2个字节和4个字节的无符号整数。java中short、int、long分别为2、4、8个字节的有符号整数,去掉符号位,刚好可以用来表示u1、u2、u4。
public class u1 { public static short read(inputstream inputstream) { byte[] bytes = new byte[1]; try { inputstream.read(bytes); } catch (ioexception e) { e.printstacktrace(); } short value = (short) (bytes[0] & 0xff); return value; } } public class u2 { public static int read(inputstream inputstream) { byte[] bytes = new byte[2]; try { inputstream.read(bytes); } catch (ioexception e) { e.printstacktrace(); } int num = 0; for (int i= 0; i < bytes.length; i++) { num <<= 8; num |= (bytes[i] & 0xff); } return num; } } public class u4 { public static long read(inputstream inputstream) { byte[] bytes = new byte[4]; try { inputstream.read(bytes); } catch (ioexception e) { e.printstacktrace(); } long num = 0; for (int i= 0; i < bytes.length; i++) { num <<= 8; num |= (bytes[i] & 0xff); } return num; } }
常量池
定义好字段类型后,我们就可以读取class文件了,首先是读取魔数之类的基本信息,这部分很简单:
fileinputstream inputstream = new fileinputstream(file); classfile classfile = new classfile(); classfile.magic = u4.read(inputstream); classfile.minorversion = u2.read(inputstream); classfile.majorversion = u2.read(inputstream);
这部分只是热热身,接下来的大头在于常量池。解析常量池之前,我们先来解释一下常量池是什么。
常量池,顾名思义,存放常量的资源池,这里的常量指的是字面量和符号引用。字面量指的是一些字符串资源,而符号引用分为三类:类符号引用、方法符号引用和字段符号引用。通过将资源放在常量池中,其他项就可以直接定义成常量池中的索引了,避免了空间的浪费,不只是class文件,android可执行文件dex也是同样如此,将字符串资源等放在dexdata中,其他项通过索引定位资源。java虚拟机规范给出了常量池中每一项的格式:
cp_info { u1 tag; u1 info[]; }
上面的这个格式只是一个通用格式,常量池中真正包含的数据有14种格式,每种格式的tag值不同,具体如下所示:
由于格式太多,文章中只挑选一部分讲解:
这里首先读取常量池的大小,初始化常量池:
//解析常量池 int constant_pool_count = u2.read(inputstream); constantpool constantpool = new constantpool(constant_pool_count); constantpool.read(inputstream);
接下来再逐个读取每项内容,并存储到数组cpinfo中,这里需要注意的是,cpinfo[]下标从1开始,0无效,且真正的常量池大小为constant_pool_count-1。
public class constantpool { public int constant_pool_count; public constantinfo[] cpinfo; public constantpool(int count) { constant_pool_count = count; cpinfo = new constantinfo[constant_pool_count]; } public void read(inputstream inputstream) { for (int i = 1; i < constant_pool_count; i++) { short tag = u1.read(inputstream); constantinfo constantinfo = constantinfo.getconstantinfo(tag); constantinfo.read(inputstream); cpinfo[i] = constantinfo; if (tag == constantinfo.constant_double || tag == constantinfo.constant_long) { i++; } } } }
我们先来看看constant_utf8格式,这一项里面存放的是mutf-8编码的字符串:
constant_utf8_info { u1 tag; u2 length; u1 bytes[length]; }
那么如何读取这一项呢?
public class constantutf8 extends constantinfo { public string value; @override public void read(inputstream inputstream) { int length = u2.read(inputstream); byte[] bytes = new byte[length]; try { inputstream.read(bytes); } catch (ioexception e) { e.printstacktrace(); } try { value = readutf8(bytes); } catch (utfdataformatexception e) { e.printstacktrace(); } } private string readutf8(byte[] bytearr) throws utfdataformatexception { //copy from java.io.datainputstream.readutf() } }
很简单,首先读取这一项的字节数组长度,接着调用readutf8(),将字节数组转化为string字符串。
再来看看constant_class这一项,这一项存储的是类或者接口的符号引用:
constant_class_info { u1 tag; u2 name_index; }
注意这里的name_index并不是直接的字符串,而是指向常量池中cpinfo数组的name_index项,且cpinfo[name_index]一定是constant_utf8格式。
public class constantclass extends constantinfo { public int nameindex; @override public void read(inputstream inputstream) { nameindex = u2.read(inputstream); } }
常量池解析完毕后,就可以供后面的数据使用了,比方说classfile中的this_class指向的就是常量池中格式为constant_class的某一项,那么我们就可以读取出类名:
int classindex = u2.read(inputstream); constantclass clazz = (constantclass) constantpool.cpinfo[classindex]; constantutf8 classname = (constantutf8) constantpool.cpinfo[clazz.nameindex]; classfile.classname = classname.value; system.out.print("classname:" + classfile.classname + "\n");
字节码指令
解析常量池之后还需要接着解析一些类信息,如父类、接口类、字段等,但是相信大家最好奇的还是java指令的存储,大家都知道,我们平时写的java代码会被编译成java字节码,那么这些字节码到底存储在哪呢?别急,讲解指令之前,我们先来了解下classfile中的method_info,其格式如下:
method_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }
method_info里主要是一些方法信息:如访问标志、方法名索引、方法描述符索引及属性数组。这里要强调的是属性数组,因为字节码指令就存储在这个属性数组里。属性有很多种,比如说异常表就是一个属性,而存储字节码指令的属性为code属性,看这名字也知道是用来存储代码的了。属性的通用格式为:
attribute_info { u2 attribute_name_index; u4 attribute_length; u1 info[attribute_length]; }
根据attribute_name_index可以从常量池中拿到属性名,再根据属性名就可以判断属性种类了。
code属性的具体格式为:
code_attribute { u2 attribute_name_index; u4 attribute_length; u2 max_stack; u2 max_locals; u4 code_length; u1 code[code_length]; u2 exception_table_length; { u2 start_pc; u2 end_pc; u2 handler_pc; u2 catch_type; } exception_table[exception_table_length]; u2 attributes_count; attribute_info attributes[attributes_count]; }
其中code数组里存储就是字节码指令,那么如何解析呢?每条指令在code[]中都是一个字节,我们平时javap命令反编译看到的指令其实是助记符,只是方便阅读字节码使用的,jvm有一张字节码与助记符的对照表,根据对照表,就可以将指令翻译为可读的助记符了。这里我也是在网上随便找了一个对照表,保存到本地txt文件中,并在使用时解析成hashmap。代码很简单,就不贴了,可以参考我代码中instructiontable.java。
接下来我们就可以解析字节码了:
for (int j = 0; j < methodinfo.attributescount; j++) { if (methodinfo.attributes[j] instanceof codeattribute) { codeattribute codeattribute = (codeattribute) methodinfo.attributes[j]; for (int m = 0; m < codeattribute.codelength; m++) { short code = codeattribute.code[m]; system.out.print(instructiontable.getinstruction(code) + "\n"); } } }
运行
整个项目终于写完了,接下来就来看看效果如何,随便找一个class文件解析运行:
哈哈,是不是很赞!
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
推荐阅读
-
解析Java Class 文件过程
-
Java生成和解析XML格式文件和字符串的实例代码
-
Java解析Excel文件并把数据存入数据库
-
【java项目实战】dom4j解析xml文件,连接Oracle数据库 博客分类: 【JavaScript】 xml编程dom4j
-
使用dom4j来解析xml文件或xml字符串 博客分类: Java EE dom4jxml解析xml文件xml字符串
-
java使用dom4j解析xml配置文件实现抽象工厂反射示例
-
java 解析由String类型拼接的XML文件方法
-
java解析dbf之通过javadbf包生成和读取dbf文件
-
java dom4j解析xml文件代码实例分享
-
java Class文件内部结构解析过程详解