java字节码,class文件,二进制类型文件,类似PE文件,也有一定的文件结构
官方文档:https://docs.oracle.com/javase/specs/jvms/se15/html/jvms-4.html
classs文件整体结构如下:
ClassFile { |
大体如下图所示
Class文件 结构
魔数
Magic Number,固定为cafebabe
版本号
分为主版本号 Marjor Version 和次版本号Minor Version
u2 minor_version;//Class 的小版本号 |
常量池
常量池占据了 class 文件很大一部分数据,里面存放着各式各样的常量信息,包括数字和字符串常量、类和接口名、字段和方法名等
u2 constant_pool_count;//常量池的数量 |
常量池的数量是 constant_pool_count-1
(常量池计数器是从 1 开始计数的,索引值为 0 代表“不引用任何一个常量池项” )
常量池主要存放两大常量:字面量和符号引用
字面量就是java中最基本的数据类型,
符号引用主要包括:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
java没有像 c 语言那样有”链接”这一步,而是在加载 class 文件的时候动态加载,拿到该符号引用指向的字符串,再使用反射,加载相应的类.
Java 虚拟机规范定义了一种简单的语法来描述字段和方法,可以根据下面的规则生成描述符。
类型描述符。
- 基本类型 byte、short、char、int、long、float 和 double 的描述符是单个字母,分别对应 B、S、C、I、J、F 和 D。注意,long 的描述符是 J 而不是 L。
- 引用类型的描述符是“L+类的完全限定名+分号”
- 数组类型的描述符是“[+数组元素类型描述符”
字段描述符就是字段的类型描述符。
方法描述符格式是:“(按参数顺序的参数类型描述符)+返回值类型描述符”,其中 void 返回值由单个字母 V 表示。
常量池中每一项常量都是一个表,这 14 种表有一个共同的特点:开始的第一位是一个 u1 类型的标志位 -tag 来标识常量的类型,代表当前这个常量属于哪种常量类型.
类型 | 标志(tag) | 描述 |
---|---|---|
CONSTANT_utf8_info | 1 | UTF-8 编码的字符串 |
CONSTANT_Integer_info | 3 | 整形字面量 |
CONSTANT_Float_info | 4 | 浮点型字面量 |
CONSTANT_Long_info | 5 | 长整型字面量 |
CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
CONSTANT_Class_info | 7 | 类或接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型字面量 |
CONSTANT_FieldRef_info | 9 | 字段的符号引用 |
CONSTANT_MethodRef_info | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodRef_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的符号引用 |
CONSTANT_MethodType_info | 16 | 标志方法类型 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
访问标志
u2 access_flags;//Class 的访问标记 |
类访问和属性修饰符:
当前类、父类、接口
u2 this_class;//当前类 |
Java 类的继承关系由类索引、父类索引和接口索引集合三项确定。类索引、父类索引和接口索引集合按照顺序排在访问标志之后,
类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于 Java 语言的单继承,所以父类索引只有一个
接口索引集合用来描述这个类实现了哪些接口,这些被实现的接口将按 implements
(如果这个类本身是接口的话则是extends
) 后的接口顺序从左到右排列在接口索引集合中
字段表
Field
u2 fields_count;//字段数量 |
用于描述接口或类中声明的变量。字段包括类级变量以及实例变量,但不包括局部变量。
字段结构:
- access_flags: 字段的作用域(
public
,private
,protected
修饰符),是实例变量还是类变量(static
修饰符),可否被序列化(transient 修饰符),可变性(final),可见性(volatile 修饰符,是否强制从主内存读写)。 - name_index: 对常量池的引用,表示的字段的名称;
- descriptor_index: 对常量池的引用,表示字段和方法的描述符;
- attributes_count: 一个字段还会拥有一些额外的属性,attributes_count 存放属性的个数;
- attributes[attributes_count]: 存放具体属性具体内容。
字段的 access_flag 的取值:
方法表
Method
u2 methods_count;//方法数量 |
methods_count 表示方法的数量,而 method_info 表示方法表。
method_info(方法表的) 结构:
方法表的 access_flag 取值:
属性表
Attributes
在 Class 文件,字段表,方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。与 Class 文件中其它的数据项目要求的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写 入自己定义的属性信息,Java 虚拟机运行时会忽略掉它不认识的属性—
属性表也是最复杂的
按照用途,有 23 种预定义属性,可以分为三组。
- 第一组属性是实现 Java 虚拟机所必需的,共有 5 种;
- 第二组属性是 Java 类库所必需的,共有 12 种;
- 第三组属性主要提供给工具使用,共有 6 种。(这组属性是可选的,也就是说可以不出现在 class 文件中。如果 class 文件中存在第三组属性,Java 虚拟机实现或者 Java 类库也是可以利用它们的,比如使用 LineNumberTable 属性在异常堆栈中显示行号。)
这里只介绍几个常用的属性:
属性名 | 位置 | 含义 | 分组 |
---|---|---|---|
Deprecated | ClassFile, field_info, method_info | 被声明为 deprecated 的方法和字段 | 3 |
Synthetic | ClassFile, field_info, method_info | 标示方法或字段是由编译器自动生成的 | 2 |
SourceFile | ClassFile | 记录源文件的名称 | 3 |
ConstantValue | field_info | final 关键字定义的常量值 | 1 |
Code | method_info | Java 代码编译成的字节码指令 | 1 |
Exceptions | method_info | 方法抛出的异常 | 1 |
LineNumberTable | Code | Java 源代码的行号与字节码指令的对应关系 | 3 |
LocalVariableTable | Code | 方法的局部变量描述 | 3 |
ContanstValue
ConstantValue 是定长属性,只会出现在 field_info 结构中,用于表示常量表达式的值。其作用是通知虚拟机自动为静态变量赋值。只有被 static 修饰的变量(类变量)才可以使用这项属性。
其结构定义如下:
ConstantValue_attribute { |
下面有三种情况:
- int a1 = 123;
- static int a2 = 123;
- final static int a3 = 123
对于非 static 变量,eg:a1,其赋值是在实例构造器<init>
方法中完成的。
而对于 static 变量,eg:a2,a3,有两种赋值方式。
- 在类构造器
<clinit>
方法中。 - 使用 ConstantValue 属性。
目前 Sun Javac 的选择是:前提都是针对于用 static 修饰的静态变量。如果是用 final static 修饰的话,并且这个变量是基本类型或者 String,那么使用 2 赋值。否则使用 1 赋值。
因此这里的 constantvalue_index 是指向常量池中一个字面量类别(CONSTANT_Integer、CONSTANT_Float、CONSTANT_Long、CONSTANT_Double、CONSTANT_Utf8 五种中的一种)的索引,该常量中保存着变量的值。
Code
Code 是变长属性,只存在于 method_info 结构中。Code 属性中存放字节码等方法相关信息,应该重点关注,其中存放着jvm指令。其结构定义如下:
Code_attribute { |
关于上面的 exception_table,其结构定义如下。
{ |
字节码指令存储在 Code 属性内。Code 属性出现在方法表的属性集合之中,但是并非所有的方法都必须存在 Code 这个属性。譬如接口或者抽象类中的方法就不存在 Code 属性
- attribute_name_index:指向 CONSTANT_Utf8_info 类型常量的索引,这个常量值固定为“Code”,代表了该属性的属性名称。
- attribute_length:代表该属性的长度,包括从 attribute_name_index 开始到 attributes[]数组结束。
- max_stack:代表操作数栈的深度的最大值。在方法执行的任意时刻,操作数栈都不能超过这个深度。
- max_locals:代表了局部变量表所需的存储空间大小。在这里 max_locals 的单位是 Slot,Slot 是虚拟机为局部变量分配内存所使用的最小单位。对于 byte、char、short、int、float、boolean、returnAddress 等长度不超过 32 位的数据,每个局部变量占用一个 Slot,而 double 和 long 这种 64 位的数据则需要两个 Slot 来存放。
- code_length:指示下面的 code 字节码数组的长度。虽然这是一个 u4 类型,理论上最大值可以达到 2^32-1,但是 Java 虚拟机明确规定一个方法中的指令不能超过 65535 条字节码指令,也就是说它实际是使用了 u2 的长度。
- code[code_length]:存放的是 Java 源程序编译后生成的 字节码指令, 之后会详细学习
- exception_table_length:指示下面的异常表数组的长度。
- exception_table[exception_table_length] 关于异常处理。
- attributes_count:指示下面的属性表数组的长度。
- attribute_info attributes[attributes_count]:Code 本身就已经是属性了,在这个属性的字段中还包括一些其它的属性…那么就存在这个表中。
局部变量表中存放的内容
- 方法参数(包括实例方法中隐藏参数 this)
- 显式异常处理器的参数(catch 块所定义的异常)
- 方法体中递归的局部变量
但是这里要注意的是:并非在方法中用到了多少个局部变量,就把这些局部变量所占的 Slot 的数量作为 max_locals 的值,因为局部变量表中的 Slot 是可以重用的,当代码执行超出了某一局部变量的作用域之后,这个 Slot 就可以被其它局部变量所使用了,所以 Javac 编译器会根据变量的作用域来分配 Slot 给各个变量使用,然后计算出 max_locals 的大小
this:
它的实现方法是在 Javac 编译的时候把 this 添加到每个非静态方法的方法参数中,所以在方法内访问的 this 其实是本方法的参数
Exception
这里将的 Exception 属性和 Code 属性是一级的。并不是 Code 属性中的异常属性表。
这里的 Exception 属性的作用是列举方法中通过throws
关键字后面列举的异常。其结构如下:
Exceptions_attribute { |
局部变量表(LocalVariableTable)
字节码偏移量(Bytecode Offset)是指在 Java 字节码指令序列中的某条指令相对于方法字节码起始位置的偏移量,以字节为单位计算。它在 Java 虚拟机中用于指令定位、跳转操作以及调试信息映射,是以Code为基址的偏移。
LocalVariableTable_attribute { |
local_variable_table 局部变量表,其中的每一条数据代表一个变量
也就是一个变量的描述信息如下:
{ |
start_pc: 局部变量生效的起始位置(字节码偏移量)
length:局部变量的长度,这里不是指物理上的长度,而是局部变量作用的指令的长度(从
start_pc
开始的字节数),即局部变量的生命周期name_index:指向常量池中一个
CONSTANT_Utf8_info
项,表示局部变量的名称。descriptor_index:指向常量池中一个
CONSTANT_Utf8_info
项,表示变量的类型描述符index:表示该局部变量在局部变量表中的槽位索引
MethodParameters
MethodParameters是一个用在方法表中的变长属性。MethodParameters的作用是记录方法的各个形参名称和信息。
MethodParameters
属性表主要用于描述 Java 字节码中某些方法参数的元数据。这个属性是在 Java 8 中引入的,用于支持通过字节码存储和检索参数名及其他信息(如是否是最终参数、是否隐式等)。
字段名 | 类型 | 描述 |
---|---|---|
attribute_name_index |
u2 |
常量池中指向字符串"MethodParameters" 的索引。 |
attribute_length |
u4 |
属性内容的长度(不包括attribute_name_index 和attribute_length 本身)。 |
parameters_count |
u1 |
方法参数的数量。 |
parameters |
parameter_info[] |
包含每个参数的元数据。 |
作用:反编译时还原方法参数名称
其他
InnerClasses
作用:描述内部类和嵌套类的信息。
字段名 | 类型 | 描述 |
---|---|---|
number_of_classes |
u2 |
内部类的数量。 |
classes |
class_info[] |
内部类信息数组。 |
class_info 结构:
字段名 | 类型 | 描述 |
---|---|---|
inner_class_info_index |
u2 |
内部类的索引。 |
outer_class_info_index |
u2 |
外部类的索引。 |
inner_name_index |
u2 |
内部类的简单名称索引。 |
inner_class_access_flags |
u2 |
内部类的访问标志。 |
LineNumberTable
作用:用于调试,表示字节码指令与源码行号的映射。
字段名 | 类型 | 描述 |
---|---|---|
line_number_table_length |
u2 |
表的长度。 |
line_number_table |
line_number_info[] |
映射关系数组。 |
line_number_info 结构:
字段名 | 类型 | 描述 |
---|---|---|
start_pc |
u2 |
字节码指令的起始地址。 |
line_number |
u2 |
源代码的行号。 |
SourceFile
作用:指示 .class
文件来源的源文件名。
字段名 | 类型 | 描述 |
---|---|---|
sourcefile_index |
u2 |
常量池中源文件名的索引。 |
StackMapTable
作用:用于验证器快速验证方法字节码的类型安全性(自 Java 6 起引入)。
字段名 | 类型 | 描述 |
---|---|---|
number_of_entries |
u2 |
表的长度。 |
entries |
stack_map_frame[] |
栈帧数组,用于描述字节码执行时的栈和局部变量状态。 |
Signature
作用:提供泛型和方法签名信息(比 descriptor
更详细)。
字段名 | 类型 | 描述 |
---|---|---|
signature_index |
u2 |
指向常量池中签名字符串的索引。 |
使用场景:
- 描述泛型方法或类,如
List<T>
或Map<K, V>
。
BootstrapMethods
作用:支持动态调用(invokedynamic
指令)的引导方法表。
字段名 | 类型 | 描述 |
---|---|---|
num_bootstrap_methods |
u2 |
引导方法数量。 |
bootstrap_methods |
bootstrap_method_info[] |
引导方法信息数组。 |
bootstrap_method_info 结构:
字段名 | 类型 | 描述 |
---|---|---|
bootstrap_method_ref |
u2 |
指向常量池中方法句柄的索引。 |
num_bootstrap_arguments |
u2 |
引导方法的参数数量。 |
bootstrap_arguments |
u2[] |
参数在常量池中的索引数组。 |
当然,这里只是列举了一部分属性表,还有一些像模块化等相关的还没提及
Class 文件解析
使用go 编写 代码对 class 文件进行解析
reader 读取 字节流,用来读取不同的类型
type ClassReader struct { |
下面是主要的代码,其他结构体和 读取的方法忽略
type ClassFile struct { |
上面代码参考项目:jvm.go
项目地址:https://github.com/zxh0/jvm.go
解析class 和解析jvm指令,即可实现反编译和编辑class字节码,后续看自己能不能实现
Reference
https://javaguide.cn/java/jvm/class-file-structure.html