# 前言

在这个分类中的前三篇简单的记录了一下 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 打开

image-20230911152311714

这里可以看到的是,整个文件全部都是一个字节一个字节分组,从左上角向下逐行读取,这里还用 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"

这种反编译后的内容前面的文章中已经记录过多次了,这里就不在主句分析,它采用了一种微结构体来存放数据,其中只有两种数据类型

  1. 无符号数属于基本数据类型,以 u1、u2、u4、u8 分别代表 1 个字节、2 个字节、4 个字节与 8 个字节的无符号数。无符号数通常可以用来描述数字、引用索引、数量值或按照 UTF-8 编码构成的字符串值
  2. 表是由多个无符号数或其他表作为数据项组成的复合数据结构。表的命名通常以 "_info" 结尾。表用于描述有层次关系的复合结构的数据。整个 Class 文件也可以视为一张表

无符号数

类型名称数量
u4magic1
u2minor_version1
u2major_version1
u2constant_pool_count1
cp_infoconstant_poolconstant_pool_count-1
u2access_flags1
u2this_class1
u2super_class1
u2interfaces_count1
u2interfacesinterfaces_count
u2fields_count1
field_infofieldsfields_count
u2methods_count1
method_infomethodsmethods_count
u2attributes_count1
attribute_infoattributesattributes_count

无符号数类型介绍

image-20230911163720442

这里还是看 winhex 中的内容

image-20230911152941763

这里前四个字节是文件头,标识这是一个 class 文件,在 Linux 系统中就是通过这个来识别文件类型

image-20230911153307703

这里可以看到,在 Linux 的 file 命令中还给出了版本(52 是 JDK8,53 是 JDK9),这个信息其实就是上面的后八个字节表示的

image-20230911153658877

这里次版本号是 0(现在基本不再使用),主版本号是 16 进制的 34,转换过来后其实就是 52,这个版本号的作用实际上就是来判断当前的运行环境是否兼容

java 中低版本编译文件是可以在高版本中运行的

然后再紧接着就是常量池中的数据了

image-20230911155339155

这两个字节存放的是常量池中常量的数量

这两个字节存放的是 16 进制,比如这个 16 就是 22,但是是从 1 开始计算的,所以这里有 21 个常量

对应的常量池数据,这里直接看 javap 反编译得到的数据

image-20230911163251332

这里安装一下 IDEA 上的插件 jclasslib 来看一下

image-20230911174534740

安装完成后,选中字节码文件,然后点击视图选择工具

image-20230911175308506

然后就可以在右边看到

image-20230911175324077

这里可以看到下面也是列出了方法等信息

image-20230911185917757

值得注意的是,如果这里有静态代码块

image-20230911190305355

就会存在这个 clinit 方法

# 字节码指令

在上面一张图中,可以看到 clinit 的字节码,这里来看一下 test 方法的字节码指令

image-20230912085943044

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 来指定类的一些基本信息

image-20230912093657606

这个方法中需要我们填写的参数例如上图所示,首先是字节码文件的版本,然后是 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();
        }
    }
}

一个简答的类字节码文件就创建好了

image-20230912193649618

这里就只有类的一些基本信息,这里来给其添加构造方法

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();
        }
    }
}

image-20230912193555416

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();
        }
    }
}

image-20230912193551928

这个文件中没有什么变化,这里反编译看一下

PS E:\study_java> javap -v .\ASMtest.class
Classfile /E:/study_java/ASMtest.class
  Last modified 2023912; 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
}

可以看到这里已经按照我们预期的写进去了