有趣的 Class 文件结构!!!

    技术2022-08-01  100

    写在前面

    本文作为阅读了周志明作者的 <<深入理解Java虚拟机>> 的读书笔记,同时,也结合了 SE 8 的 JAVA 虚拟机规范。

    Class 文件是一组以 8 位字节为基础单位的二进制流,各个数据项目严格按照顺序紧紧地排列在 Class 文件之中,中间没有添加任何分隔符。

    根据 JVM 规定,Class 文件格式采用一种类似于C语言结构体的伪结构体来存储,这种伪结构中只有两种数据类型:无符号数和表。

    无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值,或者按照 UTF-8 编码构成字符串值。

    表是由多个无符号数或其他作为数据项构成的复合数据类型,所有的表都习惯以 “_info" 结尾。

    类文件结构

    一个类文件包含一个单一的 ClassFile 结构:

    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]; }

    这是使用 WinHex 打开的一个 class 文件,后续各数据项均在其中能找到:

    各个数据项的具体含义如下:

    magic:提供识别类文件格式的魔数; 它的值是 0xCAFEBABE。魔数一般是用来识别文件的,因为依赖后缀名的识别是不安全的,后缀名可以随意改动。对应图中的前 4 个字节,相信你已经看到了。

    minor_version, major_version: Class 文件的次、主版本号。主版本从 45 开始,所以图中的 第六个和第七个字节对应的是十六进制的 0x0034,也就是转换为十进制后的 52,我用的 Java 8。

    constant_pool_count:值等于constant_pool表中的条目数加1。如果constant_pool索引大于零且小于constant_pool_count,则认为它是有效的。也就是说它是从 1 而不是 0 开始的。空出 0 是有特殊考虑的,是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目” 的意思,这种情况就可以把索引值置为 0 来表示。图中对应的是 0x0021,即十进制的 33 ,这就代表常量池中有 33 项常量,索引值为 1~33。

    constant_pool[]:这是一个表结构,表示在类文件结构及其子结构中引用的各种字符串常量、类和接口名、字段名和其它常量。书中将常量池中的常量分为两大类:字面量和符号引用。

    字面量就是我们常生活的文本字符串,被声明为 final 的常量值等。而符号引用则属于编译原理方法的概念,包括了下面三类常量:

    类和接口的全限定名字段的名称和描述符方法的名称和描述符

    虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析并翻译到具体的内存地址中。文档中这样描述到:”Java虚拟机指令不依赖于类、接口、类实例或数组的运行时布局。相反,指令引用constant_pool表中的符号信息。“。

    常量池中的每一项常量都是一个表,共有 14 种结构不同的表结构数据(Java 8),这 11 种表都有一个共同的特点,就是表开始的第一位是一个 u1 类型的标志位,代表当前这个常量属于哪种常量类型。通用的格式如下:

    cp_info { u1 tag; u1 info[]; }

    tags 如下:

    书中提到一点,由于 Class 文件中方法、字段等都需要引用 CONSTANT_Utf8_info 型常量来描述名称,所以 CONSTANT_Utf8_info 型常量的最大长度也就是 Java 中方法和字段名的最大长度,即 u2 类型能表达的最大值为 65535。所以 Java 程序中如果定义了超过 64 KB 英文字符的变量或方法名,将无法编译。

    我们对应的图中第一项是 0x0A,十进制的 11,对应表中的 CONSTANT_InterfaceMethodref 类型,查询该类型的表结构如下:

    CONSTANT_InterfaceMethodref_info { u1 tag; // 即类型对应的 tag 值(11) u2 class_index; // class_index项的值必须是constant_pool表的有效索引。它表示具有字段或方法作为成员的类或接口类型 u2 name_and_type_index; // 这个constant_pool条目指示字段或方法的名称和描述符。 }

    class_index 对应的是 0x0009 ,即 9,name_and_type_index 对应的是 0x0016,即常量池中的第22项。所以我们只要找到常量池中对应的第 9 项和第 22 项即可,不过这得借助工具了;

    使用 javap -verbose Test.class 命令帮助我们打印常量池的内容: 可以看见,我们分析的没错,常量第一项索引了第九项和第22项的内容。

    access_flags:标记掩码,用于表示对这个类或接口的访问权限和属性,每个标志定义如下:

    因为前面常量池的缘故,我们已经很难找到这个标志位在哪里了,但通过上面 javap 命令的输出,我们可以看到 flags 为 ACC_PUBLIC ,ACC_SUPER,所以它对应的值应该为 0x0021,这样,我们又能在 class 文件中找到位置了。

    this_class、super_class、interfaces_count、interfaces[]:类索引、父类索引,接口索引集合。Class 将由这三个文件来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,接口索引集合用来描述这个类实现了哪个接口。

    在图中分别对应 8, 9 ,0 (十进制),这代表常量池中索引,通过 javap 命令找到 8,9 分别为 “Test" , “java/lang/Object”,这是对的,看来上一步,我们的重新定位也没有错。

    fields_count、fields[]:由这个类或接口类型声明的所有字段,包括类变量和实例变量。表的结构如下:

    field_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }

    由于我上文所写的类并没有任何的字段,所以这里也都为 0 了。读者可以自己去试试,这很有趣~

    methods_count、methods[]:方法表集合。表的结构如下:

    method_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; // 此方法的其它属性 }

    对应我们所写的则是 00 02 00 01 00 0E 00 0F 00 01,代表着 2 个方法, access_flags 为 ACC_PUBLIC(这在文档中能找到对应关系),name_index 为 14,可以看出这里依然为索引,找到常量池中的 14 为 <init> (简单名称,这里为构造方法,构造方法在编译时会被改为 ),descriptor_index 为 15,即 ‘()V’ (描述符:方法参数返回值的格式),属性计数为 1,不过,属性为表结构,先看它的结构图:

    attribute_info { u2 attribute_name_index; u4 attribute_length; u1 info[attribute_length]; }

    往后对应的则是 00 10 00 00 00 1D,即属性名称索引为 16,即 Code(代表 Java 代码编译成的字节码指令),属性的长度为 29,代表着我们需要往后读出 29 个字节:00 01 00 01 00 00 00 05 2A B7 ...

    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]; }

    所以,可以看出:max_stack 为 1,max_locals 为 1,这指的是为方法默认传参 this 。code_length 为 5 ,即往后读取五个字节为 2A B7 00 01 B1,查阅 jvm 指令可知,依次为 ‘aload_0 invokespecial nop aconst_null return’ 指令;一切都联系起来了。

    尽管 code_length 理论上最大可以达到 2 的 32 次方减一,但是 虚拟机规范中限制了一个方法不允许超过 65535 条字节码指令,如果超过限制,Javac 编译器就会拒绝编译。exception_table 是用来记录异常处理程序的。

    attributes_count、attributes[]:

    对于所有属性,attribute_name_index必须是类常量池中的有效无符号16位索引。attribute_name_index中的constant_pool条目必须是CONSTANT_Utf8_info结构,表示属性的名称。attribute_length项的值表示后续信息的字节长度。长度不包括包含attribute_name_index和attribute_length项的初始6个字节。属性的种类很多,上面只列出了一种 Code,这也是字节码文件中,最具拓展性的一个地方。

    总结

    第一次完整的去解析类文件结构,对字节码又有了更深的认识,后面需要解读的话,只要查阅文档,就能明白了。也可以看出,其实底层限制了很多,比如,方法和字段名的最大长度是多少?这都是由 Class 文件的数据结构限制了。也能明白 JVM 指令其实是一个字节表示的,所以这就限定了 JVM 的指令集不能太多。

    尽管,这些我们很少触碰到这些知识面,但学海无涯,当贪婪则贪婪!

    参考博文


    JVM 规范 :ClassFile Structure

    我与风来


    如果你觉得我的文章对你有所帮助的话,欢迎关注我的公众号。赞! 认认真真学习,做思想的产出者,而不是文字的搬运工。错误之处,还望指出!

    Processed: 0.009, SQL: 9