本文假定读者对 Java Class 文件格式有一些基本的了解, 建议结合相关书籍进行对照阅读.
Class 文件格式信息
实例代码
- package chapter6;
- public class TestClass {
- private int m;
- public int inc() {
- return m + 1;
- }
- }
使用 JDK1.8 编译成 class 文件, 然后通过 WinHex 打开
魔数(magic)
类型: u4
字节地址: 00000000~00000003
值: 0xCAFEBABE
Class 文件版本
次版本号(minor_version)
类型: u2
字节地址: 00000004~00000005
值: 0x0000
主版本号(major_version)
类型: u2
字节地址: 00000006~00000007
值: 0x0034
将 0x0034 转换为十进制, 计算得到 52, 对应版本号为 JDK 1.8.
常量池
常量池容量计数值(constant_pool_count)
类型: u2
字节地址: 00000008~00000009
值: 0x0016
将 0x0016 转换为十进制, 计算得到 22. 由于容量计数是从 1 开始(如果没有特殊情况, 通常都是从 0 开始), 因此常量池中有 21 项常量, 索引值范围为 1~21.
常量池中每一项常量都是一个表, 表开始的第一位是一个 u1 类型的标志位(tag).
第 1 项常量
tag 类型: u1
tag 字节地址: 0000000A
tag 值: 0x07
查表可知这个常量属于 CONSTANT_Class_info 结构, 代表一个类或者接口的符号引用.
name_index 类型: u2
name_index 字节地址: 0000000B~0000000C
name_index 值: 0x0002
0x0002 指向了常量池中的第 2 项常量.
第 2 项常量
tag 类型: u1
tag 字节地址: 0000000D
tag 值: 0x01
查表可知这个常量属于 CONSTANT_Utf8_info 结构, 代表一个 UTF-8 编码的字符串.
length 类型: u2
length 字节地址: 0000000E~0000000F
length 值: 0x0012
将 0x0012 转换为十进制, 计算得到 18.
bytes 类型: u1
bytes 字节地址: 00000010~00000021(length 表明地址范围为 18 个字节)
bytes 值: 下方图片浅蓝底对应的所有字节内容
通过 WinHex 查看, 对应内容为 chapter6/TestClass, 即类的全限定名.
通过逐个字节对照 ASCII 字符表, 我们同样可以得到内容为 chapter6/TestClass.
获取 ASCII 字符表: 在 Linux 上执行 man ascii, 翻页在 Tables 项可以看到字符表.
查找字符: 先找横坐标, 再找纵坐标, 横竖交叉的位置即为字节对应的字符.
例如 0x63 为 c,0x68 为 h,0x61 为 a,0x70 为 p,0x74 为 t,0x65 为 e,0x72 为 r, 连起来代表单词 chapter.
第 3 项常量
tag 类型: u1
tag 字节地址: 00000022
tag 值: 0x07
这个常量属于 CONSTANT_Class_info 结构, 代表一个类或者接口的符号引用.
name_index 类型: u2
name_index 字节地址: 00000023~00000024
name_index 值: 0x0004
0x0004 指向了常量池中的第 4 项常量.
第 4 项常量
tag 类型: u1
tag 字节地址: 00000025
tag 值: 0x01
这个常量属于 CONSTANT_Utf8_info 结构, 代表一个 UTF-8 编码的字符串.
length 类型: u2
length 字节地址: 00000026~00000027
length 值: 0x0010
将 0x0010 转换为十进制, 计算得到 16.
bytes 类型: u1
bytes 字节地址: 00000028~00000037(length 表明地址范围为 16 个字节)
bytes 值: 下方图片浅蓝底对应的所有字节内容
通过 WinHex 查看, 对应内容为 java/lang/Object, 即类的全限定名.
第 5 项常量
tag 类型: u1
tag 字节地址: 00000038
tag 值: 0x01
这个常量属于 CONSTANT_Utf8_info 结构, 代表一个 UTF-8 编码的字符串.
length 类型: u2
length 字节地址: 00000039~0000003A
length 值: 0x0001
bytes 类型: u1
bytes 字节地址: 0000003B(length 表明地址范围为 1 个字节)
bytes 值: 0x6D
通过 WinHex 查看, 对应内容为实例变量 m.
其他常量可以通过类似的方法进行分析, 但这样一个个分析确实挺辛苦的.
其实, JDK 已经为我们提供了一个 Class 文件字节码工具: javap, 可以让我们较为直观的看到 Class 文件的字节码内容.
执行命令: javap -verbose TestClass.class, 截取常量池部分内容如下:
可以看到, 版本号及前 5 个常量与我们分析的结果是一致的. 所以, 能用 1 行代码搞定的事儿, 就不要用 2 行(浪费笔墨).
常量池最后一个字节: 000000D8
访问标志(access_flags)
类型: u2
字节地址: 000000D9~000000DA
值: 0x0021
查看类或接口访问标志含义表可知, 该类的访问标志为 ACC_PUBLIC(0x0001),ACC_SUPER(0x0020).
另外, 通过类的定义 public class TestClass, 同样可以推断出类的访问标志为 ACC_PUBLIC,ACC_SUPER, 而 ACC_INTERFACE,ACC_ENUM,ACC_FINAL,ACC_ABSTRACT,ACC_ANNOTATION,ACC_SYNTHETIC 都可以排除.
所以, access_flags 应该为 0x0001|0x0020=0x0021, 结果与查看字节码相同.
类索引(this_class)
类型: u2
字节地址: 000000DB~000000DC
值: 0x0001
this_class 指向常量池的第 1 个常量, 基于前面的分析可知:
第 1 个常量的类型为 Class,Class 名称索引指向第 2 个常量.
第 2 个常量类型为 Utf8, 对应内容为 chapter6/TestClass.
因此, 类索引 (this_class) 指向的类为 chapter6/TestClass.
父类索引(super_class)
类型: u2
字节地址: 000000DD~000000DE
值: 0x0003
同样, super_class 指向常量池的第 3 个常量.
第 3 个常量的类型为 Class,Class 名称索引指向第 4 个常量.
第 4 个常量类型为 Utf8, 对应内容为 java/lang/Object.
因此, 父类索引 (super_class) 指向的类为 java/lang/Object.
接口计数器(interfaces_count)
类型: u2
字节地址: 000000DF~000000E0
值: 0x0000
接口计数器值为 0, 说明该类没有实现任何接口.
接口表(interfaces)
无
类索引 (this_class), 父类索引(super_class) 和接口索引 (interfaces) 这三项数据共同确定了当前类以及其继承关系, 相关常量池内容如下:
完整地址范围: 000000DB~000000E0
字段
字段计数器(fields_count)
类型: u2
字节地址: 000000E1~000000E2
值: 0x0001
说明当前类有 1 个字段.
字段表(fields)
访问标志(access_flags)
类型: u2
字节地址: 000000E3~000000E4
值: 0x0002
对应的访问标志为 ACC_PRIVATE.
名称索引(name_index)
类型: u2
字节地址: 000000E5~000000E6
值: 0x0005
对应常量池中的第 5 项常量, 即字段名为 m.
描述符(descriptor_index)
类型: u2
字节地址: 000000E7~000000E8
值: 0x0006
对应常量池中的第 6 项常量, 值为 I, 即 int 类型.
因此, 该字段的定义为 private int m;
属性计数器(attributes_count)
类型: u2
字节地址: 000000E9~000000EA
值: 0x0000
说明该字段没有属性信息.
属性表(attributes)
无.
字段完整地址范围: 000000E1~000000EA
最后是方法和属性, 由于内容复杂度及篇幅原因, 我们下篇再续.
来源: https://juejin.im/post/5bc361fce51d450e3d2d2ede