天天看點

Java Class檔案格式簡析

前言

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 --; size  bytes
  MD5 checksum df344c4b08a989c2471d097e90aa39d8
  Compiled from "Person.java"
public class callsuper.Person
  minor version:  // 主副版本好
  major version: 
  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=, locals=, args_size=
         : aload_0
         : invokespecial #1                  // Method java/lang/Object."<init>":()V
         : return
      LineNumberTable:
        line : 

  public void say(); // 普通函數
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=, locals=, args_size=
         : getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         : ldc           #3                  // String Hello Person
         : invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         : return
      LineNumberTable:
        line : 
        line : 
}
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 = ; 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 = ; 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]
[]
           

繼續閱讀