用Java實現JVM第三章《解析class文件》附[classReader拆解]

案例介紹

按照以下虛擬機規範,本文主要介紹java版本jvm提取class字節碼方式。在java中沒有無符號類型,例如js中byte取值是0~25六、java中是-128 ~ +172,因此在實際處理字節碼時[虛擬機規範u一、u二、u4],須要進行轉換。html

[java虛擬機規範]每一個Class文件都是由8字節爲單位的字節流組成,全部的16位、32位和64位長度的數據將被構形成2個、4個和8個8字節單位來表示。多字節數據項老是按照 Big-Endian的順序進行存儲。java

①Big-Endian 順序是指按高位字節在地址最低位,最低字節在地址最高位來存儲數據,它是 SPARC、PowerPC等處理器的默認多字節存儲順序,而 x86等處理器則是使用了相反的 Little-Endian順序來存儲數據。爲了保證 Class 文件在不一樣硬件上具有一樣的含義,所以在 Java 虛擬機規範中是有必要嚴格規定了數據存儲順序的git

ClassFile結構體 u1[1字節=8比特位]、u2[2字節=2×8比特位]、u4[4字節=4×8比特位]

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

字節碼介紹

在JAVA中一共有八種基本數據類型,他們分別是byte、short、int、long、float、double、char、boolean 其中byte、short、int、long都是表示整數的,只不過他們的取值範圍不同 byte的取值範圍爲-128~127,佔用1個字節(-2的7次方到2的7次方-1) short的取值範圍爲-32768~32767,佔用2個字節(-2的15次方到2的15次方-1) int的取值範圍爲(-2147483648~2147483647),佔用4個字節(-2的31次方到2的31次方-1) long的取值範圍爲(-9223372036854774808~9223372036854774807),佔用8個字節(-2的63次方到2的63次方-1)github

二進制求和(127):2^0+2^1+2^2+2^3+2^4+2^5+2^6+2^7
                = 2^(n+1) - 1
				= 127
複製代碼
/** * byte 取值範圍 * +127 = [0][1][1][1][1][1][1][1] * -128 = [1][0][0][0][0][0][0][0] * * 有符號 * -120 = [1][1][1][1][1][0][0][0] * 無符號(增位) 136 = 256 - 120 * 136 = [0][0][0][0][0][0][0][0][1][0][0][0][1][0][0][0] * * 輸出二進制:new BigInteger("-120", 10).toString(2)) */
public class HelloWorld {

    public static void main(String[] args) {

        byte[] val = {-120};

        BigInteger bigInteger = new BigInteger(1, val);

        //無符號(增位)
        String str_hex = bigInteger.toString(16);
        System.out.println(Integer.parseInt(str_hex, 16));

        //有符號
        System.out.println(bigInteger.byteValue());

    }

}
複製代碼
測試輸出:
136
-120
複製代碼

以下讀取字節碼並進行解析

package org.itstack.demo.test;

import java.math.BigInteger;

public class ClassReaderTest {

    //取部分字節碼:java.lang.String
    private static byte[] classData = {
            -54, -2, -70, -66, 0, 0, 0, 52, 2, 26, 3, 0, 0, -40, 0, 3, 0, 0, -37, -1, 3, 0, 0, -33, -1, 3, 0, 1, 0, 0, 8, 0,
            59, 8, 0, 83, 8, 0, 86, 8, 0, 87, 8, 0, 110, 8, 0, -83, 8, 0, -77, 8, 0, -49, 8, 0, -47, 1, 0, 3, 40, 41, 73, 1,
            0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 59, 1, 0, 20, 40, 41,
            76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 3, 40, 41, 86, 1, 0, 3,
            40, 41, 90, 1, 0, 4, 40, 41, 91, 66, 1, 0, 4, 40, 41, 91, 67, 1, 0, 4, 40, 67, 41, 67, 1, 0, 21, 40, 68, 41, 76,
            106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 4, 40, 73, 41, 67, 1, 0, 4};

    public static void main(String[] args) {

        //classData是咱們的字節碼,第一是-54,由於byte取值範圍是-128~+127,因此若是想看到和其餘虛擬機同樣的值,須要進行與運算。
        System.out.println("* byte字節碼與運算原值(-54)換行後(-54 & 0x0FF):" + (-54 & 0x0FF));

        //校驗魔數
        readAndCheckMagic();

        //校驗版本號
        readAndCheckVersion();

        //接下來會依次讀取[能夠參照java版本虛擬機代碼];constantPool、accessFlags、thisClassIdx、supperClassIdx、interfaces、fields、methods、attributes
    }

    /** * 校驗魔數 * <p> * 不少文件格式都會規定知足該格式的文件必須以某幾個固定字節開頭,這幾個字節主要起到標識做用,叫做魔數(magic number)。 * 例如; * PDF文件以4字節「%PDF」(0x2五、0x50、0x4四、0x46)開頭, * ZIP文件以2字節「PK」(0x50、0x4B)開頭 * class文件以4字節「0xCAFEBABE」開頭 */
    private static void readAndCheckMagic() {
        System.out.println("\r\n------------ 校驗魔數 ------------");
        //從class字節碼中讀取前四位
        byte[] magic_byte = new byte[4];
        System.arraycopy(classData, 0, magic_byte, 0, 4);

        //將4位byte字節轉成16進制字符串
        String magic_hex_str = new BigInteger(1, magic_byte).toString(16);
        System.out.println("magic_hex_str:" + magic_hex_str);

        //byte_magic_str 是16進制的字符串,cafebabe,由於java中沒有無符號整型,因此若是想要無符號只能放到更高位中
        long magic_unsigned_int32 = Long.parseLong(magic_hex_str, 16);
        System.out.println("magic_unsigned_int32:" + magic_unsigned_int32);

        //魔數比對,一種經過字符串比對,另一種使用假設的無符號16進制比較。若是使用無符號比較須要將0xCAFEBABE & 0x0FFFFFFFFL與運算
        System.out.println("0xCAFEBABE & 0x0FFFFFFFFL:" + (0xCAFEBABE & 0x0FFFFFFFFL));

        if (magic_unsigned_int32 == (0xCAFEBABE & 0x0FFFFFFFFL)) {
            System.out.println("class字節碼魔數無符號16進制數值一致校驗經過");
        } else {
            System.out.println("class字節碼魔數無符號16進制數值一致校驗拒絕");
        }

    }

    /** * 校驗版本號 * <p> * 魔數以後是class文件的次版本號和主版本號,都是u2類型。假設某class文件的主版本號是M,次版本號是m,那麼完整的版本號能夠 * 表示成「M.m」的形式。次版本號只在J2SE 1.2以前用過,從1.2開始基本上就沒有什麼用了(都是0)。主版本號在J2SE 1.2以前是45, * 從1.2開始,每次有大版本的Java版本發佈,都會加1{4五、4六、4七、4八、4九、50、5一、52} */
    private static void readAndCheckVersion() {
        System.out.println("\r\n------------ 校驗版本號 ------------");

        //從class字節碼第4位開始讀取,讀取2位
        byte[] minor_byte = new byte[2];
        System.arraycopy(classData, 4, minor_byte, 0, 2);
        //將2位byte字節轉成16進制字符串
        String minor_hex_str = new BigInteger(1, minor_byte).toString(16);
        System.out.println("minor_hex_str:" + minor_hex_str);
        //minor_unsigned_int32 轉成無符號16進制
        int minor_unsigned_int32 = Integer.parseInt(minor_hex_str, 16);
        System.out.println("minor_unsigned_int32:" + minor_unsigned_int32);

        //從class字節碼第6位開始讀取,讀取2位
        byte[] major_byte = new byte[2];
        System.arraycopy(classData, 6, major_byte, 0, 2);
        //將2位byte字節轉成16進制字符串
        String major_hex_str = new BigInteger(1, major_byte).toString(16);
        System.out.println("major_hex_str:" + major_hex_str);
        //major_unsigned_int32 轉成無符號16進制
        int major_unsigned_int32 = Integer.parseInt(major_hex_str, 16);
        System.out.println("major_unsigned_int32:" + major_unsigned_int32);

        System.out.println("版本號:" + major_unsigned_int32 + "." + minor_unsigned_int32);

    }

}
複製代碼

測試結果

* byte字節碼與運算原值(-54)換行後(-54 & 0x0FF):202

------------ 校驗魔數 ------------
magic_hex_str:cafebabe
magic_unsigned_int32:3405691582
0xCAFEBABE & 0x0FFFFFFFFL3405691582
class字節碼魔數無符號16進制數值一致校驗經過 ------------ 校驗版本號 ------------ minor_hex_str:0 minor_unsigned_int32:0 major_hex_str:34 major_unsigned_int32:52 版本號:52.0 Process finished with exit code 0 複製代碼

上一篇:用Java實現JVM第三章《解析class文件》微信

下一篇:用Java實現JVM第四章《運行時數據區》jvm

微信搜索「bugstack蟲洞棧」公衆號,關注後回覆「用Java實現jvm源碼」獲取本文源碼&更多原創專題案例!測試

相關文章
相關標籤/搜索