少女祈祷中...

java字节码,class文件,二进制类型文件,类似PE文件,也有一定的文件结构

官方文档:https://docs.oracle.com/javase/specs/jvms/se15/html/jvms-4.html

classs文件整体结构如下:

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

大体如下图所示

Class文件 结构

魔数

Magic Number,固定为cafebabe

版本号

分为主版本号 Marjor Version 和次版本号Minor Version

u2             minor_version;//Class 的小版本号
u2 major_version;//Class 的大版本号

常量池

常量池占据了 class 文件很大一部分数据,里面存放着各式各样的常量信息,包括数字和字符串常量、类和接口名、字段和方法名等

u2             constant_pool_count;//常量池的数量
cp_info constant_pool[constant_pool_count-1];//常量池

常量池的数量是 constant_pool_count-1​(常量池计数器是从 1 开始计数的,索引值为 0 代表“不引用任何一个常量池项” )

常量池主要存放两大常量:字面量和符号引用

字面量就是java中最基本的数据类型,

符号引用主要包括:

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

java没有像 c 语言那样有”链接”这一步,而是在加载 class 文件的时候动态加载,拿到该符号引用指向的字符串,再使用反射,加载相应的类.

Java 虚拟机规范定义了一种简单的语法来描述字段和方法,可以根据下面的规则生成描述符。

  1. 类型描述符。

    • 基本类型 byte、short、char、int、long、float 和 double 的描述符是单个字母,分别对应 B、S、C、I、J、F 和 D。注意,long 的描述符是 J 而不是 L。
    • 引用类型的描述符是“L+类的完全限定名+分号”
    • 数组类型的描述符是“[+数组元素类型描述符”
  2. 字段描述符就是字段的类型描述符。

  3. 方法描述符格式是:“(按参数顺序的参数类型描述符)+返回值类型描述符”,其中 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;//当前类
u2 super_class;//父类
u2 interfaces_count;//接口数量
u2 interfaces[interfaces_count];//一个类可以实现多个接口

Java 类的继承关系由类索引、父类索引和接口索引集合三项确定。类索引、父类索引和接口索引集合按照顺序排在访问标志之后,

类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于 Java 语言的单继承,所以父类索引只有一个

接口索引集合用来描述这个类实现了哪些接口,这些被实现的接口将按 implements (如果这个类本身是接口的话则是extends) 后的接口顺序从左到右排列在接口索引集合中

字段表

Field

u2             fields_count;//字段数量
field_info fields[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;//方法数量
method_info methods[methods_count];//一个类可以有个多个方法

methods_count 表示方法的数量,而 method_info 表示方法表。

method_info(方法表的) 结构:

方法表的 access_flag 取值:

属性表

Attributes

在 Class 文件,字段表,方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。与 Class 文件中其它的数据项目要求的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写 入自己定义的属性信息,Java 虚拟机运行时会忽略掉它不认识的属性—

属性表也是最复杂的

按照用途,有 23 种预定义属性,可以分为三组。

  1. 第一组属性是实现 Java 虚拟机所必需的,共有 5 种;
  2. 第二组属性是 Java 类库所必需的,共有 12 种;
  3. 第三组属性主要提供给工具使用,共有 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 {
u2 attribute_name_index;
u4 attribute_length;
u2 constantvalue_index;
}

下面有三种情况:

  • int a1 = 123;
  • static int a2 = 123;
  • final static int a3 = 123

对于非 static 变量,eg:a1,其赋值是在实例构造器<init>​方法中完成的。

而对于 static 变量,eg:a2,a3,有两种赋值方式。

  1. 在类构造器<clinit>​方法中。
  2. 使用 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 {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}

关于上面的 exception_table,其结构定义如下。

{ 
u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
}

字节码指令存储在 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 {
u2 attribute_name_index;
u4 attribute_length;
u2 number_of_exceptions;
u2 exception_index_table[number_of_exceptions];
}

局部变量表(LocalVariableTable)

字节码偏移量(Bytecode Offset)是指在 Java 字节码指令序列中的某条指令相对于方法字节码起始位置的偏移量,以字节为单位计算。它在 Java 虚拟机中用于指令定位、跳转操作以及调试信息映射,是以Code为基址的偏移。

LocalVariableTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 local_variable_table_length;
{ u2 start_pc;
u2 length;
u2 name_index;
u2 descriptor_index;
u2 index;
} local_variable_table[local_variable_table_length];
}

local_variable_table 局部变量表,其中的每一条数据代表一个变量

也就是一个变量的描述信息如下:

{   
u2 start_pc; //起始偏移
u2 length; //长度
u2 name_index; //名称
u2 descriptor_index;//类型描述符
u2 index;
}
  • 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 {
BytesReader
cf *ClassFile
}

func NewClassReader(data []byte) ClassReader {
br := NewBytesReader(data, binary.BigEndian)
return ClassReader{BytesReader: br}
}

func (reader *ClassReader) readUint16s() []uint16 {
n := reader.ReadUint16()
s := make([]uint16, n)
for i := range s {
s[i] = reader.ReadUint16()
}
return s
}

// readFn: func(reader *ClassReader) XXX
func (reader *ClassReader) readTable(readFn interface{}) interface{} {
n := int(reader.ReadUint16())

itemType := reflect.TypeOf(readFn).Out(0)
sliceType := reflect.SliceOf(itemType)
s := reflect.MakeSlice(sliceType, n, n) // make([]x, n, n)

readFnVal := reflect.ValueOf(readFn)
args := []reflect.Value{reflect.ValueOf(reader)}

for i := 0; i < n; i++ {
x := readFnVal.Call(args)[0]
s.Index(i).Set(x) // s[i] = x
}

return s.Interface()
}

type BytesReader struct {
byteOrder binary.ByteOrder
data []byte
position int
}

func NewBytesReader(data []byte, byteOrder binary.ByteOrder) BytesReader {
return BytesReader{
byteOrder: byteOrder,
data: data,
position: 0,
}
}

func (reader *BytesReader) Position() int {
return reader.position
}

func (reader *BytesReader) ReadUint8() uint8 {
i := reader.data[reader.position]
reader.position++
return i
}

func (reader *BytesReader) ReadUint16() uint16 {
i := reader.byteOrder.Uint16(reader.data[reader.position:])
reader.position += 2
return i
}

func (reader *BytesReader) ReadUint32() uint32 {
i := reader.byteOrder.Uint32(reader.data[reader.position:])
reader.position += 4
return i
}

func (reader *BytesReader) ReadUint64() uint64 {
i := reader.byteOrder.Uint64(reader.data[reader.position:])
reader.position += 8
return i
}

func (reader *BytesReader) ReadBytes(n int) []byte {
bytes := reader.data[reader.position : reader.position+n]
reader.position += n
return bytes
}

func Parse(classData []byte) (cf *ClassFile, err error) {
defer func() {
if r := recover(); r != nil {
var ok bool
err, ok = r.(error)
if !ok {
err = fmt.Errorf("%v", r)
}
}
}()

cr := NewClassReader(classData)
cf = &ClassFile{}
cf.Read(&cr)
return
}

下面是主要的代码,其他结构体和 读取的方法忽略

type ClassFile struct {
//magic uint32
MinorVersion uint16 //次版本
MajorVersion uint16 //主版本
ConstantPoolCount uint16 // 常量池大小
ConstantPool []ConstantInfo //常量池
AccessFlags uint16 //类访问控制符
ThisClass uint16 //类名
SuperClass uint16 //父类名
InterfacesCount uint16 //接口数量
Interfaces []uint16 // 类实现的接口
FieldsCount uint16 //类属性数量
Fields []MemberInfo //类属性表
MethodsCount uint16 //方法数量
Methods []MemberInfo //方法表
AttributesCount uint16 //属性数量
AttributeTable // 属性表
}

func (cf ClassFile) ToByteCode() []byte {
return make([]byte, 0)
}

func (cf *ClassFile) Read(reader *ClassReader) {
reader.cf = cf
//读取魔数
cf.readAndCheckMagic(reader)
//读取版本号
cf.readAndCheckVersions(reader)
//读取常量池
cf.ConstantPool = readConstantPool(reader)
cf.ConstantPoolCount = uint16(len(cf.ConstantPool))

cf.AccessFlags = reader.ReadUint16()
cf.ThisClass = reader.ReadUint16()
cf.SuperClass = reader.ReadUint16()
cf.Interfaces = reader.readUint16s()
cf.InterfacesCount = uint16(len(cf.Interfaces))
//读取field 表
cf.Fields = readMembers(reader)
cf.FieldsCount = uint16(len(cf.Fields))
//读取方法表
cf.Methods = readMembers(reader)
cf.MethodsCount = uint16(len(cf.Methods))
//读取属性表
cf.AttributeTable = readAttributes(reader)
cf.AttributesCount = uint16(len(cf.AttributeTable))
}

// store 指令 存储临时变量名 , //暂时与其他变量一样,改变常量池
func (cf *ClassFile) AddConstInfo(info ConstantInfo) int {
cf.ConstantPoolCount++
cf.ConstantPool = append(cf.ConstantPool, info)
return len(cf.ConstantPool) - 1
}

func (cf *ClassFile) readAndCheckMagic(reader *ClassReader) {
magic := reader.ReadUint32()
if magic != 0xCAFEBABE {
panic("Bad magic!") // TODO
}
}

func (cf *ClassFile) readAndCheckVersions(reader *ClassReader) {
cf.MinorVersion = reader.ReadUint16()
cf.MajorVersion = reader.ReadUint16()

switch cf.MajorVersion {
case 45:
return
case 46, 47, 48, 49, 50, 51, 52,
53, 54, 55, 56, 57:
if cf.MinorVersion == 0 {
return
}
}
panic("java.lang.UnsupportedClassVersionError!")
}

func (cf *ClassFile) GetThisClassName() string {
return cf.GetClassName(cf.ThisClass)
}
func (cf *ClassFile) GetSuperClassName() string {
return cf.GetClassName(cf.SuperClass)
}
func (cf *ClassFile) GetInterfaceNames() []string {
return cf.GetClassNames(cf.Interfaces)
}

func (cf *ClassFile) GetNameAndType(cpIndex uint16) (name, _type string) {
if cpIndex > 0 {
ntInfo := cf.GetConstantInfo(cpIndex).(ConstantNameAndTypeInfo)
name = cf.GetUTF8(ntInfo.NameIndex)
_type = cf.GetUTF8(ntInfo.DescriptorIndex)
}
return
}

func (cf *ClassFile) GetClassName(cpIndex uint16) string {
if cpIndex == 0 {
return ""
}
classInfo := cf.GetConstantInfo(cpIndex).(ConstantClassInfo)
return cf.GetUTF8(classInfo.NameIndex)
}
func (cf *ClassFile) GetPackageName(cpIndex uint16) string {
if cpIndex == 0 {
return ""
}
pkgInfo := cf.GetConstantInfo(cpIndex).(ConstantPackageInfo)
return cf.GetUTF8(pkgInfo.NameIndex)
}
func (cf *ClassFile) GetModuleName(cpIndex uint16) string {
if cpIndex == 0 {
return ""
}
modInfo := cf.GetConstantInfo(cpIndex).(ConstantModuleInfo)
return cf.GetUTF8(modInfo.NameIndex)
}

func (cf *ClassFile) GetClassNames(cpIndexes []uint16) []string {
ss := make([]string, len(cpIndexes))
for i, cpIndex := range cpIndexes {
ss[i] = cf.GetClassName(cpIndex)
}
return ss
}
func (cf *ClassFile) GetModuleNames(cpIndexes []uint16) []string {
ss := make([]string, len(cpIndexes))
for i, cpIndex := range cpIndexes {
ss[i] = cf.GetModuleName(cpIndex)
}
return ss
}

func (cf *ClassFile) GetRawUTF8(cpIndex uint16) string {
if cpIndex == 0 {
return ""
}
rawBytes := cf.GetConstantInfo(cpIndex).([]byte)
return string(rawBytes)
}

// GetUTF8 从常量池中读取字符串
func (cf *ClassFile) GetUTF8(cpIndex uint16) string {
if cpIndex == 0 {
return ""
}
bytes := cf.GetConstantInfo(cpIndex).([]byte)
return DecodeMUTF8(bytes)
}

func (cf *ClassFile) GetConstantInfo(cpIndex uint16) ConstantInfo {
if cpInfo := cf.ConstantPool[cpIndex]; cpInfo == nil {
panic(fmt.Errorf("invalid constant pool index: %d", cpIndex))
} else {
return cpInfo
}
}

上面代码参考项目:jvm.go

项目地址:https://github.com/zxh0/jvm.go

解析class 和解析jvm指令,即可实现反编译和编辑class字节码,后续看自己能不能实现

Reference

https://javaguide.cn/java/jvm/class-file-structure.html

https://zachaxy.github.io/2017/05/09/%E6%89%8B%E5%86%99JVM%E7%B3%BB%E5%88%97-6-%E5%88%86%E6%9E%90class%E6%96%87%E4%BB%B6-%E5%B1%9E%E6%80%A7%E8%A1%A8