Class 文件格式詳解

Class 文件格式詳解

Write once, run everywhere!,咱們都知道這是 Java 著名的宣傳口號。不一樣的操做系統,不一樣的 CPU 具備不一樣的指令集,如何作到平臺無關性,依靠的就是 Java 虛擬機。計算機永遠只能識別 01組成的二進制文件,虛擬機就是咱們編寫的代碼和計算機之間的橋樑。虛擬機將咱們編寫的 .java 源程序文件編譯爲 字節碼 格式的 .class 文件,字節碼是各類虛擬機與全部平臺統一使用的程序存儲格式,這是平臺無關性的本質,虛擬機在操做系統的應用層實現了平臺無關。實際上不只僅是平臺無關,JVM 也是 語言無關 的。常見的 JVM 語言,如 ScalaGroovy,再到最近的 Android 官方開發語言 Kotlin,通過各自的語言編譯器最終都會編譯爲 .class 文件。適當的瞭解 Class 文件格式,對咱們開發,逆向都是大有裨益的。java

Class 文件結構

class 文件的結構很清晰,以下所示:android

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];
}
複製代碼

其中的 u2 u4 分別表明 2 和 4 字節的無符號數。另外須要注意的是 classs 文件的多字節數據是按照大端表示法(big-endian)進行存儲的,在解析的時候須要注意。git

瞭解一種文件結構最好的方法就是去解析它,包括以後的 AndroidManifest.xmldex等等,都會經過代碼直接解析來學習它們的文件結構。下面就以最簡單的 Hello.java 程序進行解析:github

public class Hello {

    private static String HELLO_WORLD = "Hello World!";

    public static void main(String[] args) {
        System.out.println(HELLO_WORLD);
    }
}
複製代碼

javac 命令編譯生成 Hello.class 文件。這裏推薦一個利器 010Editor,查看分析各類二進制文件結構十分方便,相比 Winhex 或者 Ghex 更加智能。下面是經過 010Editor 打開 Hello.class 文件的截圖:數組

010_hello_class.png

文件結構一目瞭然。點擊各個結構也會自動標記處上半部分文件內容中對應的十六進制數據,至關方便。下面就對照着結構目錄逐項解析。bash

magic

class_magic.png

class 文件的魔數頗有意思, 0xCAFEBABE,也許 Java 創始人真的很熱衷於咖啡吧,包括 Java 的圖標也是一杯咖啡。微信

minor_version && major_version

class_version.png

minor_version 是次版本號,major_version 是主版本號。每一個版本的 JDK 都有本身特定的版本號。高版本的 JDK 向下兼容低版本的 Class 文件,但低版本不能運行高版本的 Class 文件,即便文件格式沒有發生任何變化,虛擬機也拒絕執行高於其版本號的 Class 文件。上面圖中主版本號爲 52,表明 JDK 1.8,在 JDK 1.8 如下的版本是沒法執行的。數據結構

constant_pool

常量池是 Class 文件中的重中之重,存放着各類數據類型,與其餘項目關聯甚多。在解析的時候,咱們能夠把常量池當作一個數組或者集合,既然是數組或者集合,就要先肯定它的長度。首先看一下 Hello.class 文件的常量池部分的截圖:post

class_constant_pool.png

常量池部分以一個 u2 類型開頭,表明常量池中的容量,上例中爲 34。須要注意的是,常量池的下標是從 1 開始的,也就表明該 Class 文件具備 33 個常量。那麼,爲何下標要從 1 開始呢?目的是爲了表示在特定狀況下 不引用任何一個常量池項,這時候下標就用 0 表示。學習

下表是常量池的一些常見數據類型:

類 型 標 志 描 述
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_MethodHandle_info 15 表示方法句柄
CONSTANT_MethodType_info 16 標識方法類型
CONSTANT_InvokeDynamic_info 18 表示一個動態方法調用點

常量池的數據類型有十幾種,各自都有本身的數據結構,可是他們都有一個共有屬性 tagtag 是標誌位,標記是哪種數據結構。咱們在這裏不針對每種數據結構進行分析,就按照 Hello.class 文件的常量池結構粗略分析一下。

首先看 Hello.class 文件常量池的第一項:

class_pool0.png

這是一個 CONSTANT_Methodref_info , 表示類中方法的一些信息,它的數據結構是 tag class_index name_and_type_indextag 標識爲 10。class_index的值是 7,這是一個常量池索引,指向常量池中的某一項數據。注意,常量池的索引是從 1 開始的,因此這裏指向的實際上是第 6 個數據項:

class_constant_class.png

CONSTANT_Methodref_infoclass_index 指向的數據項永遠是 CONSTANT_Class_infotag 標識爲 7,表明的是類或者接口,它的 name_index 也是常量池索引,上圖中能夠看到是第 26 項:

class_pool26.png

這是一個 CONSTANT_Utf8_info,從名稱就能夠看出來這是一個字符串,length 屬性標識長度,後面的 byte[] 表明字符串內容。從 010Editor 解析內容能夠看到這個字符串是 java/lang/Object,表示類的全限定名。

接着回到常量池第一項 CONSTANT_Methodref_info,剛纔看了 name_index 屬性,另外一個屬性是 name_and_type_index,它永遠指向 CONSTANT_NameAndType_info,表示字段或者方法,它的值爲 19,咱們來看一下常量池的第 18 項:

class_pool18.png

CONSTANT_NameAndType_info 的 tag 標識爲 12,具備兩個屬性, name_indexdescriptor_index,它們指向的均是 CONSTANT_Utf8_infoname_index 表示字段或者方法的非限定名,這裏的值是 <init>descriptor_index表示字段描述符或者方法描述符,這裏的值是 ()V

到這裏,常量池的第一個數據項就分析完了,後面的每個數據項均可以按照這樣分析。到這裏就能夠看到常量池的重要性了,包含了 Class 文件的大部分信息。

接着繼續分析常量池以後的文件結構,先整體瀏覽一下:

class_after_pool.png

access_flags

訪問表示,表示類或接口的訪問權限和屬性,下圖爲一些訪問標誌的取值和含義:

標誌名稱 標 志 值 含 義
ACC_PUBIC 0x0001 是否爲 public 類型
ACC_FINAL 0x0010 是否聲明爲 final
ACC_SUPER 0x0020 JDK1.0.2 以後編譯出來的類這個標誌都必須爲真
ACC_INTERFACE 0x0200 是否爲接口
ACC_ABSTRACT 0x0400 是否爲 abstract 類型
ACC_SYNTHETIC 0x1000 標記這個類並不是由用戶代碼產生
ACC_ANNOTATION 0x2000 是否爲註解
ACC_ENUM 0x4000 是否爲枚舉類型

Hello.class 文件的訪問標記爲十進制 33,Hello.java 就是一個普通的類,由 public 修飾,因此應該具備 ACC_PUBICACC_SUPER 這兩個標記, 0x0001 + 0x0010 正好爲十進制 33。

this_class && super_class && interfaces_count && interfaces[]

爲何把這幾項放在同一節進行解釋?由於這幾項數據共同肯定了該類的繼承關係。 this_class 表明類索引,用於肯定該類的全限定名,圖中能夠看到索引值爲 6 ,指向常量池中第 5 個數據項,這個數據項必須是 CONSTANT_Class_info,查找常量池能夠看到類名爲 Hello,表明當前類的名字。super_class表示父類索引, 一樣也是指向 CONSTANT_Class_info,值爲 java/lang/Object。咱們都知道,Object 是 java 中惟一一個沒有父類的類,所以它的父類索引爲 0 。

super_class 以後緊接的兩個字符是 interfaces_count,表示的是該類實現的接口數量。因爲 Hello.java 未實現任何接口,因此該值爲 0。若是實現了若干接口,這些接口信息將存儲在以後的 interfaces[] 之中。

fields_count && field_info

字段表集合,表示該類中聲明的變量。fields_count 指明變量的個數,fields[] 存儲變量的信息。注意,這裏的變量指的是成員變量,並不包括方法中的局部變量。再回憶一下 Hello.java 文件,僅有一個變量:

private static String HELLO_WORLD = "Hello World!";
複製代碼

上面這行變量聲明告訴咱們,有一個叫 HELLO_WORLDString 類型變量,且是 private static 修飾的。因此 fields[] 所需存儲的也正是這些信息。先來看下 filed_info 的結構:

class_field.png

access_flags是訪問標誌,表示字段的訪問權限和基本屬性,和以前分析過的類的訪問標誌是很類似的。下表是一些常見的訪問標誌的名稱和含義:

標誌名稱 標 志 值 含 義
ACC_PUBIC 0x0001 是否爲 public
ACC_PRIVATE 0x0002 是否爲 private
ACC_PROTECTED 0x0004 是否爲 protected
ACC_STATIC 0x0008 是否爲 static
ACC_FINAL 0x0010 是否爲 final
ACC_VOLATILE 0x0040 是否爲 volatile
ACC_TRANSIENT 0x0080 是否爲 transient
ACC_SYNTHETIC 0x1000 是否由編譯器自動生成
ACC_ENUM 0x4000 是否爲 enum

private static 即爲 0x0002 + 0x0008 ,等於十進制的 10。

name_index 爲常量池索引,表示字段的名稱,查看常量池第 7 項,是一個 CONSTANT_Utf8_info ,值爲 HELLO_WORLD

descriptor_index 也是常量池索引,表示字段的描述,查看常量池第 8 項,是一個 CONSTANT_Utf8_info ,值爲 Ljava/lang/String;

這樣便獲得了這個字段的完整信息。在圖中還能夠看到,descriptor_index 後面還跟着 attributes_count,這裏的值爲 0,不然後面還會跟着 attributes[]。關於屬性表後面還會專門分析到,這裏先不作分析。

methods_count && method_info

緊接着字段表集合的是方法表集合,表示類中的方法。方法表集合和字段表集合的結構很類似,以下圖所示:

class_method.png

access_flags 表示訪問標誌,其標誌值和字段表略有不一樣,以下所示:

標誌名稱 標 志 值 含 義
ACC_PUBIC 0x0001 是否爲 public
ACC_PRIVATE 0x0002 是否爲 private
ACC_PROTECTED 0x0004 是否爲 protected
ACC_STATIC 0x0008 是否爲 static
ACC_FINAL 0x0010 是否爲 final
ACC_SYNCHRONIZED 0x0020 是否爲 sychronized
ACC_BRIDGE 0x0040 是否由編譯器產生的橋接方法
ACC_VARARGS 0x0080 是否接受不定參數
ACC_NATIVE 0x0100 是否爲 native
ACC_ABSTRACT 0x0400 是否爲 abstract
ACC_STRICTFP 0x0800 是否爲 strictfp
ACC_SYNTHETIC 0x1000 是否由編譯器自動產生

name_indexdescriptor_index 與字段表同樣,分別表示方法的名稱和方法的描述,指向常量池中的 CONSTANT_Utf8_info 項。方法中的具體代碼存儲在以後的屬性表中,經編譯器編譯爲字節碼格式存儲。屬性表在下一節進行具體分析。

attributes_count && attribute_info

屬性表在以前已經出現過好幾回,包括字段表,方法表都包含了屬性表。屬性表的種類不少,能夠表示源文件名稱,編譯生成的字節碼指令,final 定義的常量值,方法拋出的異常等等。在 《Java虛擬機規範(Java SE 7)》中已經預約義了 21 項屬性。這裏僅對 Hello.class 文件中出現的屬性進行分析。

首先來看下 Hello.class 文件中緊跟在方法表以後的最後兩項。

class_attribute.png

attributes_count 聲明後面的屬性表長度,這裏爲 1,後面跟了一個屬性。由上圖該屬性結構可知,這是一個定長的屬性,可是大部分屬性類型實際上是不定長的。

attribute_name_index 是屬性名稱索引,指向常量池中的 CONSTANT_Utf8_info,表明的是屬性的類型,該屬性爲 17,因此指向常量池的第 16 項,查閱常量池,其值爲 SourceFile,表示這是一個 SourceFile 屬性,其屬性值爲源文件的名稱。

attribute_length 是屬性的長度,但不包含 attribute_name_index 和其自己,因此整個屬性的長度應該是 attribute_length + 6

sourcefile_index 是源文件名稱索引,指向常量池中的 CONSTANT_Utf8_info,索引值爲 18,即指向第 17 項,不難猜想,該項表示的字符串就是源文件名稱 Hello.java

這個屬性比較簡單,下面來看 main 方法表中的屬性表,該屬性表所表明的是 main 方法中的代碼通過編譯編譯生成的字節碼:

class_method_main.png

能夠看到 main 方法的方法表中包含了一個屬性,其結構仍是比較複雜的,下面進行逐項分析。

attribute_name_index 指向常量池第 11 項,其字符串爲 Code,表示這是一個 Code 屬性。Code 屬性是 Class 文件中最重要的屬性,它存儲的是 Java 代碼編譯生成的字節碼。

attribute_length 爲 38,表示後面 38 個字節都是該屬性的內容。

max_stack 表明了操做數棧深度的最大值。在方法執行的任意時刻,操做數棧都不會超過這個深度。虛擬機運行的時候須要根據這個值來分配棧幀中的操做棧深度。

max_locals 表明了局部變量表所需的存儲空間,以 slot 爲單位。Slot 是虛擬機爲局部變量分配內存所使用的最小單位。

code_length 指的是編譯生成的字節碼的長度, 緊接着的 code 就是用來存儲字節碼的。上圖中能夠看到這裏的字節碼長度是 10 個字節,咱們來看一下這 10 個字節:

B2 00 02 B2 00 03 B6 00 04 B1
複製代碼

字節碼指令是由操做碼(Opcode)以及跟隨其後的所需參數構成的。操做碼是單字節,表明某種特定操做,參數個數可能爲 0。關於字節碼指令集的指令描述,在 《Java 虛擬機規範》 一書中有詳細介紹。

這裏咱們接着分析上述字節碼。首個操做符爲 0xb2,查表可得表明的操做是 getstatic,獲取類的靜態字段。其後跟着兩字節的索引值,指向常量池中的第 2 項數據,這是一個 CONSTANT_Fieldref_info,表示的是一個字段的符號引用。按照上面的分析方式分析一下這個字段,它的類名是 java/lang/System,名稱是 out,描述符是 Ljava/io/PrintStream;。由此可知,0xb20002 這段字節碼的含義是獲取類 System 中類型爲 Ljava/io/PrintStream 的靜態字段 out

接着看下一個操做符,仍舊是 0xb2,同上,分析可得這個字段是類 Hello 中類型爲 Ljava/lang/String 的靜態字段 HELLO_WORLD

第三個操做符是 0xb6,查表其表明的操做爲 invokevirtual,其含義是調用實例方法。後面緊跟兩個字節,指向常量池中的 CONSTANT_Methodref_info。查看常量池中的第 4 項數據,分析可得其類名爲 java/io/PrintStream,方法名爲 println,方法描述符爲 (Ljava/lang/String;)V。這三個字節的字節碼執行的操做就是咱們的打印語句了。

最後一個操做符是 0xb1,表明的操做爲 return,表示方法返回 void。到這裏,該方法就執行完畢了。

到這裏, Hello.class 的文件結構就基本分析完了。咱們再回顧一下 Class 文件的基本結構:

魔數 | 副版本號 | 主版本號 | 常量池數量 | 常量 | 訪問標誌 | 類索引 | 父類索引 | 接口數量 | 接口表 | 字段數量 | 字段表 | 方法數量 | 方法表 | 屬性數量 | 屬性表

各個項目嚴格按照順序緊湊地排列在 Class 文件之中,中間沒有任何分隔符。

另外咱們也能夠經過 javap 命令快速查看 Class 文件內容:

javap -verbose Hello.class

結果以下圖所示:

javap1.png

javap2.png

代碼解析

Class 文件格式的代碼解析相對比較簡單,讀到文件流逐項解析便可。

魔數和主副版本解析:

private void parseHeader() {
    try {
        String magic = reader.readHexString(4);
        log("magic: %s", magic);

        int minor_version = reader.readUnsignedShort();
        log("minor_version: %d", minor_version);

        int major_version = reader.readUnsignedShort();
        log("major_version: %d", major_version);
    } catch (IOException e) {
        log("Parser header error:%s", e.getMessage());
    }
}
複製代碼

常量池解析:

private void parseConstantPool() {
    try {
        int constant_pool_count = reader.readUnsignedShort();
        log("constant_pool_count: %d", constant_pool_count);

        for (int i = 0; i < constant_pool_count - 1; i++) {

            int tag = reader.readUnsignedByte();
            switch (tag) {
                case ConstantTag.METHOD_REF:
                    ConstantMethodref methodRef = new ConstantMethodref();
                    methodRef.read(reader);
                    log("%s", methodRef.toString());
                    break;

                case ConstantTag.FIELD_REF:
                    ConstantFieldRef fieldRef = new ConstantFieldRef();
                    fieldRef.read(reader);
                    log("%s", fieldRef.toString());
                    break;

                case ConstantTag.STRING:
                    ConstantString string = new ConstantString();
                    string.read(reader);
                    log("%s", string.toString());
                    break;

                case ConstantTag.CLASS:
                    ConstantClass clazz = new ConstantClass();
                    clazz.read(reader);
                    log("%s", clazz.toString());
                    break;

                case ConstantTag.UTF8:
                    ConstantUtf8 utf8 = new ConstantUtf8();
                    utf8.read(reader);
                    log("%s", utf8.toString());
                    break;

                case ConstantTag.NAME_AND_TYPE:
                    ConstantNameAndType nameAndType = new ConstantNameAndType();
                    nameAndType.read(reader);
                    log("%s", nameAndType.toString());
                    break;

            }

        }
    } catch (IOException e) {
        log("Parser constant pool error:%s", e.getMessage());
    }
}
複製代碼

剩餘信息解析:

private void parseOther() {
    try {
        int access_flags = reader.readUnsignedShort();
        log("access_flags: %d", access_flags);

        int this_class = reader.readUnsignedShort();
        log("this_class: %d", this_class);

        int super_class = reader.readUnsignedShort();
        log("super_class: %d", super_class);

        int interfaces_count = reader.readUnsignedShort();
        log("interfaces_count: %d", interfaces_count);

        // TODO parse interfaces[]

        int fields_count = reader.readUnsignedShort();
        log("fields_count: %d", fields_count);

        List<Field> fieldList=new ArrayList<>();
        for (int i = 0; i < fields_count; i++) {
            Field field=new Field();
            field.read(reader);
            fieldList.add(field);
            log(field.toString());
        }

        int method_count=reader.readUnsignedShort();
        log("method_count: %d", method_count);

        List<Method> methodList=new ArrayList<>();
        for (int i=0;i<method_count;i++){
            Method method=new Method();
            method.read(reader);
            methodList.add(method);
            log(method.toString());
        }

        int attribute_count=reader.readUnsignedShort();
        log("attribute_count: %d", attribute_count);

        List<Attribute> attributeList = new ArrayList<>();
        for (int i = 0; i < attribute_count; i++) {
            Attribute attribute=new Attribute();
            attribute.read(reader);
            attributeList.add(attribute);
            log(attribute.toString());
        }

    } catch (IOException e) {
        e.printStackTrace();
    }
}
複製代碼

因爲屬性種類衆多,這裏未對屬性就行詳細解析,僅爲了加深對 Class 文件結構的瞭解,至關於一個低配版的 javap 。

Class 文件結構的基本瞭解就到這裏,文中相關文件和 Class 文件解析工程源碼都在這裏, android-reverse

下一篇開始學習 smali 語言,Smali 語法解析——Hello World

文章同步更新於微信公衆號: 秉心說 , 專一 Java 、 Android 原創知識分享,LeetCode 題解,歡迎關注!

相關文章
相關標籤/搜索