Java Class文件格式简析
阅读原文时间:2021年04月20日阅读:1

前言

Java开发只需要编写Java代码之后通过javac命令将其编译成.class文件,.class文件可以被JVM虚拟机加载并执行。如果需要Java能够像动态语言那样编码,通常需要修改.class文件的内容,这种情况下了解.class文件的内部结构就很有必要。

类文件结构

Java的class文件内容大致上包含如下的各种结构,如果某个节点有多个会被表示成数组结构,数组的长度通常都在实际数据之前。

ClassFile { 
    u4 magic;  // 魔法数字,表明当前文件是.class文件,固定0xCAFEBABE
    u2 minor_version; // 分别为Class文件的副版本和主版本
    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]; // 各种属性
}

constant_pool常量池是一种表结构,它包含Class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其它常量。常量池不同于其他,索引从1开始到constant_pool_count - 1。其他的结构都比较简单不需要做深入的了解,只要知道它们的存在就够了。

实例查看

现在定义一个简单的类查看它的.class文件内容,之后通过Javassist来实现读取.class内容。

package callsuper;

public class Person {
    private int age;

    public void say() {
        System.out.println("Hello Person");
    }
}

这个类定义的非常简单,就只有一个字段,一个函数,调用了简单的打印方法。现在通过javac对它做编译操作,再使用javap工具将生成的.class文件反编译查看。

javac callsuper.Person.java
javap -v Person.class

使用javap会将生成的Person.class文件反编译,列出class文件中的各种数据,展示如下:

Classfile /D:/workspace/Super/src/callsuper/Person.class
  Last modified 2018-7-4; size 420 bytes
  MD5 checksum df344c4b08a989c2471d097e90aa39d8
  Compiled from "Person.java"
public class callsuper.Person
  minor version: 0 // 主副版本好
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER // 访问标识
Constant pool: // 常量池
   #1 = Methodref          #6.#16         // java/lang/Object."<init>":()V
   #2 = Fieldref           #17.#18        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #19            // Hello Person
   #4 = Methodref          #20.#21        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #22            // callsuper/Person
   #6 = Class              #23            // java/lang/Object
   #7 = Utf8               age
   #8 = Utf8               I
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               say
  #14 = Utf8               SourceFile
  #15 = Utf8               Person.java
  #16 = NameAndType        #9:#10         // "<init>":()V
  #17 = Class              #24            // java/lang/System
  #18 = NameAndType        #25:#26        // out:Ljava/io/PrintStream;
  #19 = Utf8               Hello Person
  #20 = Class              #27            // java/io/PrintStream
  #21 = NameAndType        #28:#29        // println:(Ljava/lang/String;)V
  #22 = Utf8               callsuper/Person
  #23 = Utf8               java/lang/Object
  #24 = Utf8               java/lang/System
  #25 = Utf8               out
  #26 = Utf8               Ljava/io/PrintStream;
  #27 = Utf8               java/io/PrintStream
  #28 = Utf8               println
  #29 = Utf8               (Ljava/lang/String;)V
{
  public callsuper.Person(); // 构造函数
    descriptor: ()V
    flags: 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 3: 0

  public void say(); // 普通函数
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello Person
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 7: 0
        line 8: 8
}
SourceFile: "Person.java"

上面的带#的部分就是常量池中数据,可以看到里面包含各种方法、字段和字符串等常量,所有存在方法和定义里的常量都被替换成了该常量在常量池中的索引值。上面的代码反编译之后解析出来的是JVM的命令,和Dalvik的smali差别还是比较大的。

Javassist查看

class文件内容还是比较复杂的,如果由开发者直接人工解析还是很费事的,这里推荐使用Javassist的接口来编码查看class文件的内部数据。

CtClass ctClass = ClassPool.getDefault().get("callsuper.Person");
// 获取解析到的类文件对象
ClassFile classFile = ctClass.getClassFile();
// 获取到常量池对象
ConstPool constPool = classFile.getConstPool();

System.out.println("=======Version========");
System.out.println(classFile.getMajorVersion());
System.out.println(classFile.getMinorVersion());
System.out.println("=======Constant Pool Size========");
System.out.println(classFile.getConstPool().getSize());
System.out.println("=======Access Flag========");
int flag = classFile.getAccessFlags();
System.out.println("package = " + AccessFlag.isPackage(flag));
System.out.println("private = " + AccessFlag.isPrivate(flag));
System.out.println("protected = " + AccessFlag.isProtected(flag));
System.out.println("public = " + AccessFlag.isPublic(flag));
System.out.println("=======Super Class=========");
System.out.println(classFile.getSuperclass());
System.out.println("=========This class========");
System.out.println(classFile.getConstPool().getClassName());

System.out.println("=======Interface Info========");
System.out.println(classFile.getInterfaces().length);
for (int i = 0; i < classFile.getInterfaces().length; i++) {
    String face = classFile.getInterfaces()[i];
    System.out.println(face);
}

System.out.println("=======Method Info========");
List<MethodInfo> methodInfoList = classFile.getMethods();
for (MethodInfo methodInfo : methodInfoList) {
    System.out.println(methodInfo.getName());
    List<AttributeInfo> attributeInfoList = methodInfo.getAttributes();
    for (AttributeInfo attributeInfo : attributeInfoList) {
        System.out.println(attributeInfo.getName());
    }
    System.out.println(methodInfo.getDescriptor());
    System.out.println("--------------------------");
}
System.out.println("=======Field Info========");
List<FieldInfo> fieldInfoList = classFile.getFields();
for (int i = 0; i < fieldInfoList.size(); i++) {
    FieldInfo fieldInfo = fieldInfoList.get(i);
    System.out.println(fieldInfo.getName());
    System.out.println(fieldInfo.getDescriptor());
    System.out.println("---------------------------");
}
System.out.println("=======Attributes Info========");
List<AttributeInfo> attributeInfoList = classFile.getAttributes();
for (AttributeInfo attributeInfo : attributeInfoList) {
    System.out.println(attributeInfo.getName());
}

System.out.println("=======All Ref classes========");
System.out.println(constPool.getClassNames());
System.out.println(Arrays.asList(classFile.getInterfaces()));

运行上面的代码查看分析class文件的结果,与前面javap反编译的结果是一致的。

=======Version========
52
0
=======Constant Pool Size========
33
=======Access Flag========
package = false
private = false
protected = false
public = true
=======Super Class=========
java.lang.Object
=========This class========
callsuper.Person
=======Interface Info========
0
=======Method Info========
<init>
Code
()V
--------------------------
say
Code
()V
--------------------------
=======Field Info========
age
I
---------------------------
=======Attributes Info========
SourceFile
=======All Ref classes========
[java/lang/Object, callsuper/Person, java/lang/System, java/io/PrintStream]
[]