# 前言
在这个分类中的前三篇简单的记录了一下 JVM 的运作机制,JVM 的内存划分和内存管理和 JVM 对内存区域的 GC,本篇简单的记录一下类文件结构这块的内容,这篇记录学习完准备再回去再战 Java 反序列化的学习,本篇中所有代码都在 Clown_java/src/main/java/JavaJVM at master・clown-q/Clown_java (github.com)
# 类文件结构
Java 选择了将源代码编译为与操作系统和机器指令无关的中立格式,并且通过对应的 JVM 读取和运行这些编译文件来使得代码能够跨平台
# class 文件
前面通过 Javap 命令来反编译查看字节码文件,还是前面用过的代码
public class TestJVM { | |
public int test(){ | |
int a = 10; | |
int b = 20; | |
int c = a+b; | |
return c; | |
} | |
} |
这里编译后的文件可以使用 winhex 或者 010Editor 打开
这里可以看到的是,整个文件全部都是一个字节一个字节分组,从左上角向下逐行读取,这里还用 javap 命令来拿到反编译后的内容
PS E:\study_java> javap -v E:/study_java/target/classes/JavaJVM/TestJVM.class | |
Classfile /E:/study_java/target/classes/JavaJVM/TestJVM.class | |
Last modified 2023年8月25日; size 401 bytes | |
MD5 checksum 4580edf2ce93eb9e1d963f33771e5da9 | |
Compiled from "TestJVM.java" | |
public class JavaJVM.TestJVM | |
minor version: 0 | |
major version: 52 | |
flags: (0x0021) ACC_PUBLIC, ACC_SUPER | |
this_class: #2 // JavaJVM/TestJVM | |
super_class: #3 // java/lang/Object | |
interfaces: 0, fields: 0, methods: 2, attributes: 1 | |
Constant pool: | |
#1 = Methodref #3.#19 // java/lang/Object."<init>":()V | |
#2 = Class #20 // JavaJVM/TestJVM | |
#3 = Class #21 // java/lang/Object | |
#4 = Utf8 <init> | |
#5 = Utf8 ()V | |
#6 = Utf8 Code | |
#7 = Utf8 LineNumberTable | |
#8 = Utf8 LocalVariableTable | |
#9 = Utf8 this | |
#10 = Utf8 LJavaJVM/TestJVM; | |
#11 = Utf8 test | |
#12 = Utf8 ()I | |
#13 = Utf8 a | |
#14 = Utf8 I | |
#15 = Utf8 b | |
#16 = Utf8 c | |
#17 = Utf8 SourceFile | |
#18 = Utf8 TestJVM.java | |
#19 = NameAndType #4:#5 // "<init>":()V | |
#20 = Utf8 JavaJVM/TestJVM | |
#21 = Utf8 java/lang/Object | |
{ | |
public JavaJVM.TestJVM(); | |
descriptor: ()V | |
flags: (0x0001) ACC_PUBLIC | |
Code: | |
stack=1, locals=1, args_size=1 | |
0: aload_0 | |
1: invokespecial #1 // Method java/lang/Object."<init>":()V | |
4: return | |
LineNumberTable: | |
line 9: 0 | |
LocalVariableTable: | |
Start Length Slot Name Signature | |
0 5 0 this LJavaJVM/TestJVM; | |
public int test(); | |
descriptor: ()I | |
flags: (0x0001) ACC_PUBLIC | |
Code: | |
stack=2, locals=4, args_size=1 | |
0: bipush 10 | |
2: istore_1 | |
3: bipush 20 | |
5: istore_2 | |
6: iload_1 | |
7: iload_2 | |
8: iadd | |
9: istore_3 | |
10: iload_3 | |
11: ireturn | |
LineNumberTable: | |
line 11: 0 | |
line 12: 3 | |
line 13: 6 | |
line 14: 10 | |
LocalVariableTable: | |
Start Length Slot Name Signature | |
0 12 0 this LJavaJVM/TestJVM; | |
3 9 1 a I | |
6 6 2 b I | |
10 2 3 c I | |
} | |
SourceFile: "TestJVM.java" |
这种反编译后的内容前面的文章中已经记录过多次了,这里就不在主句分析,它采用了一种微结构体来存放数据,其中只有两种数据类型
- 无符号数属于基本数据类型,以 u1、u2、u4、u8 分别代表 1 个字节、2 个字节、4 个字节与 8 个字节的无符号数。无符号数通常可以用来描述数字、引用索引、数量值或按照 UTF-8 编码构成的字符串值
- 表是由多个无符号数或其他表作为数据项组成的复合数据结构。表的命名通常以 "_info" 结尾。表用于描述有层次关系的复合结构的数据。整个 Class 文件也可以视为一张表
无符号数
类型 | 名称 | 数量 |
---|---|---|
u4 | magic | 1 |
u2 | minor_version | 1 |
u2 | major_version | 1 |
u2 | constant_pool_count | 1 |
cp_info | constant_pool | constant_pool_count-1 |
u2 | access_flags | 1 |
u2 | this_class | 1 |
u2 | super_class | 1 |
u2 | interfaces_count | 1 |
u2 | interfaces | interfaces_count |
u2 | fields_count | 1 |
field_info | fields | fields_count |
u2 | methods_count | 1 |
method_info | methods | methods_count |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
无符号数类型介绍
这里还是看 winhex 中的内容
这里前四个字节是文件头,标识这是一个 class 文件,在 Linux 系统中就是通过这个来识别文件类型
这里可以看到,在 Linux 的 file 命令中还给出了版本(52 是 JDK8,53 是 JDK9),这个信息其实就是上面的后八个字节表示的
这里次版本号是 0(现在基本不再使用),主版本号是 16 进制的 34,转换过来后其实就是 52,这个版本号的作用实际上就是来判断当前的运行环境是否兼容
java 中低版本编译文件是可以在高版本中运行的
然后再紧接着就是常量池中的数据了
这两个字节存放的是常量池中常量的数量
这两个字节存放的是 16 进制,比如这个 16 就是 22,但是是从 1 开始计算的,所以这里有 21 个常量
对应的常量池数据,这里直接看 javap 反编译得到的数据
这里安装一下 IDEA 上的插件 jclasslib 来看一下
安装完成后,选中字节码文件,然后点击视图选择工具
然后就可以在右边看到
这里可以看到下面也是列出了方法等信息
值得注意的是,如果这里有静态代码块
就会存在这个 clinit 方法
# 字节码指令
在上面一张图中,可以看到 clinit 的字节码,这里来看一下 test 方法的字节码指令
0 bipush 10 //将常量值10(即一个字节的整数)推送到操作数栈上 | |
2 istore_1 //将操作数栈顶的整数值存储到局部变量1中 | |
3 bipush 20 //将常量值20推送到操作数栈上 | |
5 istore_2 //将操作数栈顶的整数值存储到局部变量2中 | |
6 iload_1 //将局部变量1的值加载到操作数栈上 | |
7 iload_2 //将局部变量2的值加载到操作数栈上 | |
8 iadd //从操作数栈中弹出两个整数值,将它们相加,然后将结果推送回操作数栈 | |
9 istore_3 //将操作数栈顶的整数值存储到局部变量3中 | |
10 iload_3 //将局部变量3的值加载到操作数栈上 | |
11 ireturn //从当前方法返回整数值,该值位于操作数栈的顶部 |
上面我给出了每个字节码指令的对应操作数栈上的操作,其实这块在这个分类的前前面的随笔中也有解释
# 字节码编程
在 java 中也可以直接创建一个字节码文件来像省去编译的过程。有点像是手搓汇编,现在是手搓字节码?在 Java 中内置的 ASM 框架就支持字节码编程
public class ASMTest { | |
public static void main(String[] args) { | |
//ClassWriter.COMPUTE_MAXS 自动计算操作数栈和局部变量表大小,需要手动触发 | |
// 如果是参数是 0,不会自动计算需要自己手动指定 | |
// 如果是 ClassWriter.COMPUTE_FRAMES,会自动计算操作数栈和局部临时变量表大小,并且会自动计算 StackMapFrames | |
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS); | |
} | |
} |
这里使用到 ClassWriter 来编辑类的字节码文件
public class ASMTest { | |
public static void main(String[] args) { | |
//ClassWriter.COMPUTE_MAXS 自动计算操作数栈和局部变量表大小,需要手动触发 | |
// 如果是参数是 0,不会自动计算需要自己手动指定 | |
// 如果是 ClassWriter.COMPUTE_FRAMES,会自动计算操作数栈和局部临时变量表大小,并且会自动计算 StackMapFrames | |
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS); | |
// 指定类的一些基本信息 | |
writer.visit(V1_8,ACC_PUBLIC,"JavaJVM/ClassLoaderTest/ASMTest.java",null,"java/lang/Object",null); | |
} | |
} |
这里使用 visit 来指定类的一些基本信息
这个方法中需要我们填写的参数例如上图所示,首先是字节码文件的版本,然后是 access 权限,然后类名,然后是一个标识,父类和最后的接口
public class ASMTest { | |
public static void main(String[] args) { | |
//ClassWriter.COMPUTE_MAXS 自动计算操作数栈和局部变量表大小,需要手动触发 | |
// 如果是参数是 0,不会自动计算需要自己手动指定 | |
// 如果是 ClassWriter.COMPUTE_FRAMES,会自动计算操作数栈和局部临时变量表大小,并且会自动计算 StackMapFrames | |
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS); | |
// 指定类的一些基本信息 | |
writer.visit(V1_8,ACC_PUBLIC,"JavaJVM/ClassLoaderTest/ASMTest",null,"java/lang/Object",null); | |
// 调用 visitEnd | |
writer.visitEnd(); | |
try(FileOutputStream stream = new FileOutputStream("./ASMtest.class")){ | |
stream.write(writer.toByteArray()); | |
}catch (IOException e){ | |
e.printStackTrace(); | |
} | |
} | |
} |
一个简答的类字节码文件就创建好了
这里就只有类的一些基本信息,这里来给其添加构造方法
public class ASMTest { | |
public static void main(String[] args) { | |
//ClassWriter.COMPUTE_MAXS 自动计算操作数栈和局部变量表大小,需要手动触发 | |
// 如果是参数是 0,不会自动计算需要自己手动指定 | |
// 如果是 ClassWriter.COMPUTE_FRAMES,会自动计算操作数栈和局部临时变量表大小,并且会自动计算 StackMapFrames | |
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS); | |
// 指定类的一些基本信息 | |
writer.visit(V1_8,ACC_PUBLIC,"JavaJVM/ClassLoaderTest/ASMTest",null,"java/lang/Object",null); | |
writer.visitMethod(ACC_PUBLIC,"<init>","()V",null,null); | |
// 调用 visitEnd 结束写入 | |
writer.visitEnd(); | |
try(FileOutputStream stream = new FileOutputStream("./ASMtest.class")){ | |
stream.write(writer.toByteArray()); | |
}catch (IOException e){ | |
e.printStackTrace(); | |
} | |
} | |
} |
public class ASMTest { | |
public static void main(String[] args) { | |
//ClassWriter.COMPUTE_MAXS 自动计算操作数栈和局部变量表大小,需要手动触发 | |
// 如果是参数是 0,不会自动计算需要自己手动指定 | |
// 如果是 ClassWriter.COMPUTE_FRAMES,会自动计算操作数栈和局部临时变量表大小,并且会自动计算 StackMapFrames | |
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS); | |
// 指定类的一些基本信息 | |
writer.visit(V1_8,ACC_PUBLIC,"JavaJVM/ClassLoaderTest/ASMTest",null,"java/lang/Object",null); | |
MethodVisitor methodVisitor = writer.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); | |
methodVisitor.visitCode(); | |
Label label = new Label(); | |
methodVisitor.visitLabel(label); | |
methodVisitor.visitLineNumber(11,label); | |
methodVisitor.visitVarInsn(ALOAD,0); | |
methodVisitor.visitMethodInsn(INVOKESPECIAL,"java/lang/Object","<init>","()V",false); | |
methodVisitor.visitInsn(RETURN); | |
Label label1 = new Label(); | |
methodVisitor.visitLabel(label1); | |
methodVisitor.visitLocalVariable("this","LJavaJVM/ClassLoaderTest/ASMTest",null,label,label1,0); | |
methodVisitor.visitMaxs(1,1); | |
methodVisitor.visitEnd(); | |
// 调用 visitEnd 结束写入 | |
writer.visitEnd(); | |
try(FileOutputStream stream = new FileOutputStream("./ASMtest.class")){ | |
stream.write(writer.toByteArray()); | |
}catch (IOException e){ | |
e.printStackTrace(); | |
} | |
} | |
} |
这个文件中没有什么变化,这里反编译看一下
PS E:\study_java> javap -v .\ASMtest.class | |
Classfile /E:/study_java/ASMtest.class | |
Last modified 2023年9月12日; size 262 bytes | |
MD5 checksum 42157c808b1713d2ddb1492d529083b0 | |
public class JavaJVM.ClassLoaderTest.ASMTest.java | |
minor version: 0 | |
major version: 52 | |
flags: (0x0001) ACC_PUBLIC | |
this_class: #2 // "JavaJVM/ClassLoaderTest/ASMTest.java" | |
super_class: #4 // java/lang/Object | |
interfaces: 0, fields: 0, methods: 1, attributes: 0 | |
Constant pool: | |
#1 = Utf8 JavaJVM/ClassLoaderTest/ASMTest.java | |
#2 = Class #1 // "JavaJVM/ClassLoaderTest/ASMTest.java" | |
#3 = Utf8 java/lang/Object | |
#4 = Class #3 // java/lang/Object | |
#5 = Utf8 <init> | |
#6 = Utf8 ()V | |
#7 = NameAndType #5:#6 // "<init>":()V | |
#8 = Methodref #4.#7 // java/lang/Object."<init>":()V | |
#9 = Utf8 this | |
#10 = Utf8 LJavaJVM/ClassLoaderTest/ASMTest | |
#11 = Utf8 Code | |
#12 = Utf8 LocalVariableTable | |
#13 = Utf8 LineNumberTable | |
{ | |
public JavaJVM.ClassLoaderTest.ASMTest.java(); | |
descriptor: ()V | |
flags: (0x0001) ACC_PUBLIC | |
Code: | |
stack=1, locals=1, args_size=1 | |
0: aload_0 | |
1: invokespecial #8 // Method java/lang/Object."<init>":()V | |
4: return | |
LocalVariableTable: | |
Start Length Slot Name Signature | |
0 5 0 this LJavaJVM/ClassLoaderTest/ASMTest | |
LineNumberTable: | |
line 11: 0 | |
} |
可以看到这里已经按照我们预期的写进去了