如何实现一个 Java Class 解析器

2017 年 10 月 16 日 ImportNew

(点击上方公众号,可快速关注)


来源:tinylcy,

tinylcy.me/2017/02/12/如何实现一个Java-Class解析器/

如有好文章投稿,请点击 → 这里了解详情


最近在写一个私人项目,名字叫做ClassAnalyzer,ClassAnalyzer的目的是能让我们对Java Class文件的设计与结构能够有一个深入的理解。主体框架与基本功能已经完成,还有一些细节功能日后再增加。实际上JDK已经提供了命令行工具javap来反编译Class文件,但本篇文章将阐明我实现解析器的思路。


Class文件


作为类或者接口信息的载体,每个Class文件都完整的定义了一个类。为了使Java程序可以“编写一次,处处运行”,Java虚拟机规范对Class文件进行了严格的规定。构成Class文件的基本数据单位是字节,这些字节之间不存在任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,单个字节无法表示的数据由多个连续的字节来表示。


根据Java虚拟机规范,Class文件采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。Java虚拟机规范定义了u1、u2、u4和u8来分别表示1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者是字符串。表是由多个无符号数或者其它表作为数据项构成的复合数据类型,表用于描述有层次关系的复合结构的数据,因此整个Class文件本质上就是一张表。在ClassAnalyzer中,byte、short、int和long分别对应u1、u2、u4和u8数据类型,Class文件被描述为如下Java类。


public class ClassFile {

    public U4 magic;                            // magic

    public U2 minorVersion;                     // minor_version

    public U2 majorVersion;                     // major_version

    public U2 constantPoolCount;                // constant_pool_count

    public ConstantPoolInfo[] cpInfo;           // cp_info

    public U2 accessFlags;                      // access_flags

    public U2 thisClass;                        // this_class

    public U2 superClass;                       // super_class

    public U2 interfacesCount;                  // interfaces_count

    public U2[] interfaces;                     // interfaces

    public U2 fieldsCount;                      // fields_count

    public FieldInfo[] fields;                  // fields

    public U2 methodsCount;                     // methods_count

    public MethodInfo[] methods;                // methods

    public U2 attributesCount;                  // attributes_count

    public BasicAttributeInfo[] attributes;     // attributes

}


如何解析


组成Class文件的各个数据项中,例如魔数、Class文件的版本、访问标志、类索引和父类索引等数据项,它们在每个Class文件中都占用固定数量的字节,在解析时只需要读取相应数量的字节。除此之外,需要灵活处理的主要包括4部分:常量池、字段表集合、方法表集合和属性表集合。字段和方法都可以具备自己的属性,Class本身也有相应的属性,因此,在解析字段表集合和方法表集合的同时也包含了属性表集合的解析。


常量池占据了Class文件很大一部分的数据,用于存储所有的常量信息,包括数字和字符串常量、类名、接口名、字段名和方法名等。Java虚拟机规范定义了多种常量类型,每一种常量类型都有自己的结构。常量池本身是一个表,在解析时有几点需要注意。


  • 每个常量类型都通过一个u1类型的tag来标识。

  • 表头给出的常量池大小(constantPoolCount)比实际大1,例如,如果constantPoolCount等于47,那么常量池中有46项常量。

  • 常量池的索引范围从1开始,例如,如果constantPoolCount等于47,那么常量池的索引范围为1 ~ 46。设计者将第0项空出来的目的是用于表达“不引用任何一个常量池项目”。

  • 如果一个CONSTANT_Long_info或CONSTANT_Double_info结构的项在常量池中的索引为n,则常量池中下一个有效的项的索引为n+2,此时常量池中索引为n+1的项有效但必须被认为不可用。

  • CONSTANT_Utf8_info型常量的结构中包含一个u1类型的tag、一个u2类型的length和由length个u1类型组成的bytes,这length字节的连续数据是一个使用MUTF-8(Modified UTF-8)编码的字符串。MUTF-8与UTF-8并不兼容,主要区别有两点:一是null字符会被编码成2字节(0xC0和0×80);二是补充字符是按照UTF-16拆分为代理对分别编码的,相关细节可以看这里(变种UTF-8)。


属性表用于描述某些场景专有的信息,Class文件、字段表和方法表都有相应的属性表集合。Java虚拟机规范定义了多种属性,ClassAnalyzer目前实现了对常用属性的解析。与常量类型的数据项不同,属性并没有一个tag来标识属性的类型,但是每个属性都包含有一个u2类型的attribute_name_index,attribute_name_index指向常量池中的一个CONSTANT_Utf8_info类型的常量,该常量包含着属性的名称。在解析属性时,ClassAnalyzer正是通过attribute_name_index指向的常量对应的属性名称来得知属性的类型。


字段表用于描述类或者接口中声明的变量,字段包括类级变量以及实例级变量。字段表的结构包含一个u2类型的access_flags、一个u2类型的name_index、一个u2类型的descriptor_index、一个u2类型的attributes_count和attributes_count个attribute_info类型的attributes。我们已经介绍了属性表的解析,attributes的解析方式与属性表的解析方式一致。


Class的文件方法表采用了和字段表相同的存储格式,只是access_flags对应的含义有所不同。方法表包含着一个重要的属性:Code属性。Code属性存储了Java代码编译成的字节码指令,在ClassAnalyzer中,Code对应的Java类如下所示(仅列出了类属性)。


public class Code extends BasicAttributeInfo {

    private short maxStack;

    private short maxLocals;

    private long codeLength;

    private byte[] code;

    private short exceptionTableLength;

    private ExceptionInfo[] exceptionTable;

    private short attributesCount;

    private BasicAttributeInfo[] attributes;

    ...

    private class ExceptionInfo {

        public short startPc;

        public short endPc;

        public short handlerPc;

        public short catchType;

        ...

    }

}


在Code属性中,codeLength和code分别用于存储字节码长度和字节码指令,每条指令即一个字节(u1类型)。在虚拟机执行时,通过读取code中的一个个字节码,并将字节码翻译成相应的指令。另外,虽然codeLength是一个u4类型的值,但是实际上一个方法不允许超过65535条字节码指令。


代码实现


ClassAnalyzer的源码已放在了GitHub上。在ClassAnalyzer的README中,我以一个类的Class文件为例,对该Class文件的每个字节进行了分析,希望对大家的理解有所帮助。


https://github.com/tinylcy/ClassAnalyzer


参考


  • 深入理解Java虚拟机


看完本文有收获?请转发分享给更多人

关注「ImportNew」,提升Java技能

登录查看更多
0

相关内容

【Manning新书】现代Java实战,592页pdf
专知会员服务
98+阅读 · 2020年5月22日
【实用书】Python技术手册,第三版767页pdf
专知会员服务
229+阅读 · 2020年5月21日
干净的数据:数据清洗入门与实践,204页pdf
专知会员服务
160+阅读 · 2020年5月14日
【实用书】Python爬虫Web抓取数据,第二版,306页pdf
专知会员服务
115+阅读 · 2020年5月10日
【新书】Java企业微服务,Enterprise Java Microservices,272页pdf
用 Python 开发 Excel 宏脚本的神器
私募工场
26+阅读 · 2019年9月8日
Github标星4w+,如何用Python实现所有算法
七月在线实验室
5+阅读 · 2019年5月21日
100行Python代码,轻松搞定神经网络
大数据文摘
4+阅读 · 2019年5月2日
去哪儿网开源DNS管理系统OpenDnsdb
运维帮
21+阅读 · 2019年1月22日
一文教你构建图书推荐系统【附代码】
机器学习算法与Python学习
10+阅读 · 2018年9月16日
干货 | Python 爬虫的工具列表大全
机器学习算法与Python学习
10+阅读 · 2018年4月13日
Python 杠上 Java、C/C++,赢面有几成?
CSDN
6+阅读 · 2018年4月12日
【关关的刷题日记60】Leetcode 437. Path Sum III
Deep Co-Training for Semi-Supervised Image Segmentation
Arxiv
3+阅读 · 2019年3月1日
Arxiv
5+阅读 · 2018年3月6日
Arxiv
8+阅读 · 2018年2月23日
VIP会员
相关VIP内容
【Manning新书】现代Java实战,592页pdf
专知会员服务
98+阅读 · 2020年5月22日
【实用书】Python技术手册,第三版767页pdf
专知会员服务
229+阅读 · 2020年5月21日
干净的数据:数据清洗入门与实践,204页pdf
专知会员服务
160+阅读 · 2020年5月14日
【实用书】Python爬虫Web抓取数据,第二版,306页pdf
专知会员服务
115+阅读 · 2020年5月10日
【新书】Java企业微服务,Enterprise Java Microservices,272页pdf
相关资讯
用 Python 开发 Excel 宏脚本的神器
私募工场
26+阅读 · 2019年9月8日
Github标星4w+,如何用Python实现所有算法
七月在线实验室
5+阅读 · 2019年5月21日
100行Python代码,轻松搞定神经网络
大数据文摘
4+阅读 · 2019年5月2日
去哪儿网开源DNS管理系统OpenDnsdb
运维帮
21+阅读 · 2019年1月22日
一文教你构建图书推荐系统【附代码】
机器学习算法与Python学习
10+阅读 · 2018年9月16日
干货 | Python 爬虫的工具列表大全
机器学习算法与Python学习
10+阅读 · 2018年4月13日
Python 杠上 Java、C/C++,赢面有几成?
CSDN
6+阅读 · 2018年4月12日
【关关的刷题日记60】Leetcode 437. Path Sum III
相关论文
Top
微信扫码咨询专知VIP会员