Jvm之用java解析class文件

前言:

身爲一個java程序員,怎麼能不瞭解JVM呢,假若想學習JVM,那就又必需要了解Class文件,Class之於虛擬機,就如魚之於水,虛擬機由於Class而有了生命。《深刻理解java虛擬機》中花了一整個章節來說解Class文件,但是看完後,一直都仍是迷迷糊糊,似懂非懂。正好前段時間看見一本書很不錯:《本身動手寫Java虛擬機》,做者利用go語言實現了一個簡單的JVM,雖然沒有完整實現JVM的全部功能,可是對於一些對JVM稍感興趣的人來講,可讀性仍是很高的。做者講解的很詳細,每一個過程都分爲了一章,其中一部分就是講解如何解析Class文件。javascript

這本書不太厚,很快就讀完了,讀完後,收穫頗豐。可是紙上得來終覺淺,絕知此事要躬行,我便嘗試着本身解析Class文件。go語言雖然很優秀,可是終究不熟練,尤爲是不太習慣其把類型放在變量以後的語法,仍是老老實實用java吧。java

話很少說,先貼出項目地址:github.com/HalfStackDe…git

Class文件

什麼是Class文件?

java之因此可以實現跨平臺,便在於其編譯階段不是將代碼直接編譯爲平臺相關的機器語言,而是先編譯成二進制形式的java字節碼,放在Class文件之中,虛擬機再加載Class文件,解析出程序運行所需的內容。每一個類都會被編譯成一個單獨的class文件,內部類也會做爲一個獨立的類,生成本身的class。程序員

基本結構

隨便找到一個class文件,用Sublime Text打開是這樣的:github

是否是一臉懵逼,不過java虛擬機規範中給出了class文件的基本格式,只要按照這個格式去解析就能夠了:數組

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

ClassFile中的字段類型有u一、u二、u4,這是什麼類型呢?其實很簡單,就是分別表示1個字節,2個字節和4個字節。jvm

開頭四個字節爲:magic,是用來惟一標識文件格式的,通常被稱做magic number(魔數),這樣虛擬機才能識別出所加載的文件是不是class格式,class文件的魔數爲cafebabe。不僅是class文件,基本上大部分文件都有魔數,用來標識本身的格式。ide

接下來的部分主要是class文件的一些信息,如常量池、類訪問標誌、父類、接口信息、字段、方法等,具體的信息可參考《Java虛擬機規範》。學習

解析

字段類型

上面說到ClassFile中的字段類型有u一、u二、u4,分別表示1個字節,2個字節和4個字節的無符號整數。java中short、int、long分別爲二、四、8個字節的有符號整數,去掉符號位,恰好能夠用來表示u一、u二、u4。ui

public class U1 {
    public static short read(InputStream inputStream) {
        byte[] bytes = new byte[1];
        try {
            inputStream.read(bytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
        short value = (short) (bytes[0] & 0xFF);
        return value;
    }
}

public class U2 {
    public static int read(InputStream inputStream) {
        byte[] bytes = new byte[2];
        try {
            inputStream.read(bytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
        int num = 0;
        for (int i= 0; i < bytes.length; i++) {
            num <<= 8;
            num |= (bytes[i] & 0xff);
        }
        return num;
    }
}                                                                                                                                                                                   

public class U4 {
    public static long read(InputStream inputStream) {
        byte[] bytes = new byte[4];
        try {
            inputStream.read(bytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
        long num = 0;
        for (int i= 0; i < bytes.length; i++) {
            num <<= 8;
            num |= (bytes[i] & 0xff);
        }
        return num;
    }
}複製代碼

常量池

定義好字段類型後,咱們就能夠讀取class文件了,首先是讀取魔數之類的基本信息,這部分很簡單:

FileInputStream inputStream = new FileInputStream(file);
ClassFile classFile = new ClassFile();
classFile.magic = U4.read(inputStream);
classFile.minorVersion = U2.read(inputStream);
classFile.majorVersion = U2.read(inputStream);複製代碼

這部分只是熱熱身,接下來的大頭在於常量池。解析常量池以前,咱們先來解釋一下常量池是什麼。

常量池,顧名思義,存放常量的資源池,這裏的常量指的是字面量和符號引用。字面量指的是一些字符串資源,而符號引用分爲三類:類符號引用、方法符號引用和字段符號引用。經過將資源放在常量池中,其餘項就能夠直接定義成常量池中的索引了,避免了空間的浪費,不僅是class文件,Android可執行文件dex也是一樣如此,將字符串資源等放在DexData中,其餘項經過索引定位資源。java虛擬機規範給出了常量池中每一項的格式:

cp_info {
    u1 tag;
    u1 info[]; 
}複製代碼

上面的這個格式只是一個通用格式,常量池中真正包含的數據有14種格式,每種格式的tag值不一樣,具體以下所示:

因爲格式太多,文章中只挑選一部分講解:

這裏首先讀取常量池的大小,初始化常量池:

//解析常量池
int constant_pool_count = U2.read(inputStream);
ConstantPool constantPool = new ConstantPool(constant_pool_count);
constantPool.read(inputStream);複製代碼

接下來再逐個讀取每項內容,並存儲到數組cpInfo中,這裏須要注意的是,cpInfo[]下標從1開始,0無效,且真正的常量池大小爲constant_pool_count-1。

public class ConstantPool {
    public int constant_pool_count;
    public ConstantInfo[] cpInfo;

    public ConstantPool(int count) {
        constant_pool_count = count;
        cpInfo = new ConstantInfo[constant_pool_count];
    }

    public void read(InputStream inputStream) {
        for (int i = 1; i < constant_pool_count; i++) {
            short tag = U1.read(inputStream);
            ConstantInfo constantInfo = ConstantInfo.getConstantInfo(tag);
            constantInfo.read(inputStream);
            cpInfo[i] = constantInfo;
            if (tag == ConstantInfo.CONSTANT_Double || tag == ConstantInfo.CONSTANT_Long) {
                i++;
            }
        }
    }
}複製代碼

咱們先來看看CONSTANT_Utf8格式,這一項裏面存放的是MUTF-8編碼的字符串:

CONSTANT_Utf8_info { 
    u1 tag;
    u2 length;
    u1 bytes[length]; 
}複製代碼

那麼如何讀取這一項呢?

public class ConstantUtf8 extends ConstantInfo {
    public String value;

    @Override
    public void read(InputStream inputStream) {
        int length = U2.read(inputStream);
        byte[] bytes = new byte[length];
        try {
            inputStream.read(bytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            value = readUtf8(bytes);
        } catch (UTFDataFormatException e) {
            e.printStackTrace();
        }
    }

    private String readUtf8(byte[] bytearr) throws UTFDataFormatException {
        //copy from java.io.DataInputStream.readUTF()
    }
}複製代碼

很簡單,首先讀取這一項的字節數組長度,接着調用readUtf8(),將字節數組轉化爲String字符串。

再來看看CONSTANT_Class這一項,這一項存儲的是類或者接口的符號引用:

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}複製代碼

注意這裏的name_index並非直接的字符串,而是指向常量池中cpInfo數組的name_index項,且cpInfo[name_index]必定是CONSTANT_Utf8格式。

public class ConstantClass extends ConstantInfo {
    public int nameIndex;

    @Override
    public void read(InputStream inputStream) {
        nameIndex = U2.read(inputStream);
    }
}複製代碼

常量池解析完畢後,就能夠供後面的數據使用了,比方說ClassFile中的this_class指向的就是常量池中格式爲CONSTANT_Class的某一項,那麼咱們就能夠讀取出類名:

int classIndex = U2.read(inputStream);
ConstantClass clazz = (ConstantClass) constantPool.cpInfo[classIndex];
ConstantUtf8 className = (ConstantUtf8) constantPool.cpInfo[clazz.nameIndex];
classFile.className = className.value;
System.out.print("classname:" + classFile.className + "\n");複製代碼

字節碼指令

解析常量池以後還須要接着解析一些類信息,如父類、接口類、字段等,可是相信你們最好奇的仍是java指令的存儲,你們都知道,咱們平時寫的java代碼會被編譯成java字節碼,那麼這些字節碼到底存儲在哪呢?別急,講解指令以前,咱們先來了解下ClassFile中的method_info,其格式以下:

method_info {
    u2 access_flags;
    u2 name_index;
    u2 descriptor_index;
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}複製代碼

method_info裏主要是一些方法信息:如訪問標誌、方法名索引、方法描述符索引及屬性數組。這裏要強調的是屬性數組,由於字節碼指令就存儲在這個屬性數組裏。屬性有不少種,好比說異常表就是一個屬性,而存儲字節碼指令的屬性爲CODE屬性,看這名字也知道是用來存儲代碼的了。屬性的通用格式爲:

attribute_info {
    u2 attribute_name_index;
    u4 attribute_length;
    u1 info[attribute_length];
}複製代碼

根據attribute_name_index能夠從常量池中拿到屬性名,再根據屬性名就能夠判斷屬性種類了。

Code屬性的具體格式爲:

Code_attribute {
    u2 attribute_name_index; u4 attribute_length;
    u2 max_stack;
    u2 max_locals;
    u4 code_length;
    u1 code[code_length];
    u2 exception_table_length; 
    {
        u2 start_pc;
        u2 end_pc;
        u2 handler_pc;
        u2 catch_type;
    } exception_table[exception_table_length];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}複製代碼

其中code數組裏存儲就是字節碼指令,那麼如何解析呢?每條指令在code[]中都是一個字節,咱們平時javap命令反編譯看到的指令實際上是助記符,只是方便閱讀字節碼使用的,jvm有一張字節碼與助記符的對照表,根據對照表,就能夠將指令翻譯爲可讀的助記符了。這裏我也是在網上隨便找了一個對照表,保存到本地txt文件中,並在使用時解析成HashMap。代碼很簡單,就不貼了,能夠參考我代碼中InstructionTable.java。

接下來咱們就能夠解析字節碼了:

for (int j = 0; j < methodInfo.attributesCount; j++) {
    if (methodInfo.attributes[j] instanceof CodeAttribute) {
        CodeAttribute codeAttribute = (CodeAttribute) methodInfo.attributes[j];
        for (int m = 0; m < codeAttribute.codeLength; m++) {
            short code = codeAttribute.code[m];
            System.out.print(InstructionTable.getInstruction(code) + "\n");
        }
    }
}複製代碼

運行

整個項目終於寫完了,接下來就來看看效果如何,隨便找一個class文件解析運行:

哈哈,是否是很贊!

最後再貼一下項目地址:github.com/HalfStackDe…,歡迎Fork And Star!

總結

Class文件看起來很複雜,其實真正解析起來,也沒有那麼難,關鍵是要本身動手試試,才能完全理解,但願各位看完後也能覺知此事要躬行!

參考:

1. 周志明《java虛擬機規範(JavaSE7)》

2. 張秀宏《本身動手寫Java虛擬機》

3. 周志明《深刻理解Java虛擬機(第2版)》

(若有錯誤,歡迎指正!)

(轉載請標明ID:半棧工程師,我的博客:halfstackdeveloper.github.io)

歡迎關注個人知乎專欄:zhuanlan.zhihu.com/halfstack

歡迎Follow個人github: github.com/HalfStackDe…

相關文章
相關標籤/搜索