自己動手寫JVM-解析ClassFile

筆者博客地址:https://charpty.com
本文代碼委託在:https://github.com/charpty/cjvm

許多同學看了不少關於JVM和GC相關的書,很多概念都熟悉了,但本着經歷過才能身入其境的原則,我覺得必須要自己寫一寫,體會下前人的思想和辛苦,才能對所學JVM和GC相關知識進行實踐性總結。

業餘時間的樂趣型項目,使用C語言實現的一個可高效運行的Java虛擬機,包括解釋執行實現和CS|IP方式實現。使用C99編寫,僅在類unix系統上運行,包含類加載子系統、執行子系統(常用字節碼指令實現)、運行時數據區、GC、JIT等組件的實現。最終的目標是能夠使用該虛擬機運行筆者網站的Java代碼。

01-搜索class文件

這基本上就是一個簡單的文件搜索並讀取的操作,代碼也比較好理解,只是有幾個注意事項:

  • 要遵循規範中ClassLoader的雙親委託模型,總是嘗試在父ClassLoader中找尋文件
  • 要加載的不僅僅是直接的class文件,還有壓縮在jar包、war包中的class文件

雙親委託加載方式

在虛擬機中有3種類加載器,分別是:BootstrpLoaderExtClassLoaderAppClassLoader

對應的我們稱被這3個類加載器加載的class文件路徑爲:bootStrapPathextPathuserPath,其中AppClassLoader也稱爲SystemClassLoader,它用於加載系統(用戶的項目)裏的class文件,所以稱這些class的路徑爲userPath更加形象。

classpath.c

typedef struct ClassPath
{
    char *bootStrapPath;
    char *extPath;
    char *userPath;
    char *(*readClass)(ClassPath *classPath, char *classname);
} ClassPath;

SClass *readClass(ClassPath *classPath, char *classname)
{
    SClass *r;
    if ((r = readBootStrap(classPath, classname)) != NULL)
        return r;
    else if ((r = readExt(classPath, classname)) != NULL)
        return r;
    else
        return readUser(classPath, classname);
}

從jar包中加載

jar包本質上就是zip壓縮包,所以我們使用libzip來讀取它。

classpath.c

SClass *readClassInJar(char *jarPath, char *classname)
{
    int err;
    struct zip *z = zip_open(jarPath, 0, &err);
    // TODO I can't find err code >39 means in zip.h
    if (err != 0 && err < 39)
    {
        LOG_ERROR(__FILE__, __LINE__, "open jar file %s failed, error code is: %d", jarPath, err);
        return NULL;
    }

    const char *name = classname;
    struct zip_stat st;
    zip_stat_init(&st);
    zip_stat(z, name, 0, &st);
    if (st.size <= 0)
        return NULL;
    char *contents = malloc(st.size);
    struct zip_file *f = zip_fopen(z, name, 0);
    zip_fread(f, contents, st.size);
    zip_fclose(f);
    zip_close(z);

    struct SClass *r = (SClass *)malloc(sizeof(struct SClass));
    r->len = st.size;
    r->bytes = contents;
    r->name = classname;
    return r;
}

02-解析class文件的內容

這裏也就是將class文件的裏的字節內容,解析成語言可識別的數據結構,這裏我們將其解析成稱爲ClassFile的結構體。ClassFile將單個字節碼文件的內容解析成C語言的結構體,方便後續能夠被ClassLoader加載爲Class結構體。

僅看第一層內容,ClassFile並不複雜。

classfile.c

// 屬性命名和oracle虛擬機規範儘量保持一直(規範中屬性名都使用下劃線,但結構體我習慣用駝峯形式)
// https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html
ClassFile *readAsClassFile(ClassReader *r)
{
    ClassFile *rs = (ClassFile *)malloc(sizeof(struct ClassFile));
    // 讀取版本信息
    rs->magic = readUint32(r);
    checkMagic(rs->magic);
    rs->minor_version = readUint16(r);
    rs->major_version = readUint16(r);
    checkClassVersion(rs->major_version, rs->minor_version);
    // 讀取常量池,動長
    struct CP *csp = readConstantPool(r);
    rs->constant_pool = csp;
    // 訪問標誌,是一個位圖標記,記錄了類的訪問級別,類是否爲final,是否是註解類型等等
    rs->access_flags = readUint16(r);
    // 當前類名在常量池中的索引
    rs->this_class = readUint16(r);
    // 當前類父類名在常量池中的索引
    rs->super_class = readUint16(r);
    // 讀取該類實現的所有的接口
    rs->interfaces = readUint16s(r, &(rs->interfaces_count));
    // 讀取當前類的屬性,包括靜態屬性
    rs->fields = readMembers(r, csp);
    // 讀取當前類的方法信息,包括靜態方法
    rs->methods = readMembers(r, csp);
    // 讀取剩餘的不包含在方法或者字段裏的其它屬性表信息
    rs->attributes = readAttributes(r, csp);
    return rs;
}

我們第一步要做的就是將class文件裏的內容解析爲這麼一個不太複雜的結構體,僅有這麼一個結構體還不夠,爲了統一表示對一個class文件的讀取操作,我們使用一個叫ClassReader的結構體表示該操作。

classreader.h

typedef struct ClassReader
{
    // 逐個字節讀下去
    uint32_t position;
    uint32_t len;
    unsigned char *data;
} ClassReader;

// 提供了以下幾種讀取方式
static uint8_t readUint8(ClassReader *r);
static uint16_t readUint16(ClassReader *r);
static uint32_t readUint32(ClassReader *r);
static uint64_t readUint64(ClassReader *r);
static uint16_t *readUint16s(ClassReader *r, u_int16_t *size);
static char *readBytes(ClassReader *r, u_int32_t n);

可以看出,同時對應該結構體也準備了一系列讀取方法,幾個典型實現如下:

classreader.h

static uint16_t readUint16(ClassReader *r)
{
    return (uint16_t)r->data[r->position++] << 8 | (uint16_t)r->data[r->position++];
}

static uint32_t readUint32(ClassReader *r)
{
    u_int8_t x1 = r->data[r->position++];
    u_int8_t x2 = r->data[r->position++];
    u_int8_t x3 = r->data[r->position++];
    u_int8_t x4 = r->data[r->position++];
    // *(uint32_t *)(r->data + r->position);
    return (uint32_t)x1 << 24 | (uint32_t)x2 << 16 | (uint32_t)x3 << 8 | (uint32_t)x4;
}

... 其它函數

static uint16_t *readUint16s(ClassReader *r, u_int16_t *size)
{
    uint16_t *rs = (uint16_t *)malloc((*size = readUint16(r)) * sizeof(u_int16_t));
    for (int i = 0; i < (*size); i++)
    {
        rs[i] = readUint16(r);
    }
    return rs;
}

有了這兩個基礎,剩下的事情就是按字節和規律一個個讀取了。

classfile.c

ClassFile *readAsClassFile(ClassReader *r)
{
    ClassFile *rs = (ClassFile *)malloc(sizeof(struct ClassFile));
    // 讀取版本信息
    rs->magic = readUint32(r);
    checkMagic(rs->magic);
    rs->minor_version = readUint16(r);
    rs->major_version = readUint16(r);
    checkClassVersion(rs->major_version, rs->minor_version);
    // 讀取常量池,動長
    struct CP *csp = readConstantPool(r);
    rs->constant_pool = csp;
    // 訪問標誌,是一個位圖標記,記錄了類的訪問級別,類是否爲final,是否是註解類型等等
    rs->access_flags = readUint16(r);
    // 當前類名在常量池中的索引
    rs->this_class = readUint16(r);
    // 當前類父類名在常量池中的索引
    rs->super_class = readUint16(r);
    // 讀取該類實現的所有的接口
    rs->interfaces = readUint16s(r, &(rs->interfaces_count));
    // 讀取當前類的屬性,包括靜態屬性
    rs->fields = readMembers(r, csp);
    // 讀取當前類的方法信息,包括靜態方法
    rs->methods = readMembers(r, csp);
    // 讀取剩餘的不包含在方法或者字段裏的其它屬性表信息
    rs->attributes = readAttributes(r, csp);
    return rs;
}

接下來比較複雜的就是常量池、方法和屬性簽名、屬性表這3個了。

Class中的常量池

當前這個常量池和後面運行時數據區的常量池不同,它僅是當前這個class文件裏使用的。

constant_pool.h

typedef struct CPInfo
{ 
    uint8_t tag;
    // 常量池裏存着各種各樣類型的信息
    void *v1;
    void *v2;
} CPInfo;

// 用CP表示class裏的常量池,運行期的常量池則用GCP來表示,更親切
typedef struct CP
{
    uint32_t len;
    CpInfo **infos;
} CP;

接下來的任務是將class字節碼的常量池部分解析成常量池對應結構體。

constant_pool.h

static CP *readConstantPool(ClassReader *r)
{
    CP *rs = (CP *)malloc(sizeof(struct CP));
    int cpCount = readUint16(r);
    rs->len = cpCount;
    rs->infos = (CPInfo **)malloc(cpCount * sizeof(CPInfo *));

    // 常量池從下標1開始
    for (int i = 1; i < cpCount; i++)
    {
        rs->infos[i] = readConstantInfo(r, rs);
        // http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4.5
        // 這就是個數的特殊情況,讀到long和double時,必須下一個元素是個空,以兼容老版本
        // 這是由於一個byte佔常量池2個位置
        if (rs->infos[i]->tag == CONSTANT_Long || (rs->infos[i]->tag == CONSTANT_Double))
        {
            ++i;
            continue;
        }
    }
    return rs;
}

常量池裏存着各種類型的信息,但最多的也就兩個屬性,所以這裏就用兩個void*指針表示了。常量信息使用tag表示屬性類型,有14種類型。

constant_pool.h

#define CONSTANT_Class 7
#define CONSTANT_Fieldref 9
#define CONSTANT_Methodref 10
#define CONSTANT_InterfaceMethodref 11
#define CONSTANT_String 8
#define CONSTANT_Integer 3
#define CONSTANT_Float 4
#define CONSTANT_Long 5
#define CONSTANT_Double 6
#define CONSTANT_NameAndType 12
#define CONSTANT_Utf8 1
#define CONSTANT_MethodHandle 15
#define CONSTANT_MethodType 16
#define CONSTANT_InvokeDynamic 18

根據不同的類型,我們需要不同的方式,列舉一部分。

constant_pool.h

static CPInfo *readConstantInfo(ClassReader *r, CP *cp)
{
    CPInfo *rs = (CPInfo *)malloc(sizeof(struct CPInfo));
    uint8_t tag = rs->tag = readUint8(r);
    if (tag == CONSTANT_Class)
    {
        // nameIndex
        // 存儲class存儲的位置索引
        rs->v1 = malloc(sizeof(uint16_t));
        *(uint16_t *)rs->v1 = readUint16(r);
    }
    else if (tag == CONSTANT_Fieldref)
    {
        // classIndex and nameAndTypeIndex
        rs->v1 = malloc(sizeof(uint16_t));
        rs->v2 = malloc(sizeof(uint16_t));
        *(uint16_t *)rs->v1 = readUint16(r);
        *(uint16_t *)rs->v2 = readUint16(r);
    }
    ... 各種類似解析
    else if (tag == CONSTANT_InvokeDynamic)
    {
        // bootstrapMethodAttrIndex and nameAndTypeIndex
        rs->v1 = malloc(sizeof(uint16_t));
        rs->v2 = malloc(sizeof(uint16_t));
        *(uint16_t *)rs->v1 = readUint16(r);
        *(uint16_t *)rs->v2 = readUint16(r);
    }

方法和屬性簽名

方法和屬性簽名帶的幾個屬性是相同的,所以都用同一個結構體表示了。

member_info.h

typedef struct MemberInfo
{
    // 訪問控制符,是否靜態,是否公開等
    uint16_t accessFlags;
    // 方法名|字段名在常量池中索引
    uint16_t nameIndex;
    // 描述符字符串
    // https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.2
    uint16_t descriptorIndex;
    // 屬性表,方法代碼存在屬性表中
    AttributeInfos *attributes;
} MemberInfo;

可以看到,在這個結構體中外層只有一些簽名信息。

就方法而言,包括方法的訪問控制信息和特性信息、方法的名稱信息、方法的描述信息3部分,其中方法的描述符也是一串字符串,如下:

(IDLjava/lang/Thread;)Ljava/lang/Object;

實際就是方法

Object m(int i, double d, Thread t) {...}

那麼方法中的具體實現代碼存在哪裏呢?答案是屬性表中,屬性表可以說是最複雜多樣的一個結構了,基本上什麼都有。

屬性表

我們使用一個簡單的結構體來表示屬性表

attribute_info.h

typedef struct AttributeInfo
{
    // 保留文件常量池的指針,後續不用每次傳遞了
    CP *cp;
    // 一共23中屬性表,CJVM中僅解析需要用到的部分
    // https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7
    void *info;
} AttributeInfo;

typedef struct AttributeInfos
{
    uint32_t size;
    AttributeInfo **infos;
} AttributeInfos;

和常量池信息類似,屬性表中的信息也有多種類型,類型很多,我們就不一一解析了,僅解析我們需要用到的幾個。

attribute_info.h

typedef struct ExceptionTableEntry
{
    // PC計數器起,可以理解爲代碼起,包括
    uint16_t startPc;
    // try-catch代碼行止,不包括
    uint16_t endPc;
    // catch時處理行起,必須指向有效的code數組某一個下標
    uint16_t handlerPc;
    // catch異常類型類名
    uint16_t catchType;
} ExceptionTableEntry;

typedef struct ExceptionTable
{
    uint32_t size;
    ExceptionTableEntry **entrys;
} ExceptionTable;

/*
 * 實際的代碼(指令)存儲在屬性表中
 */
typedef struct AttrCode
{
    uint16_t maxStack;
    uint16_t maxLocals;
    uint32_t codeLen;
    char *code;
    ExceptionTable *exceptionTable;
    AttributeInfos *attributes;
} AttrCode;


// Deprecated過期、內部生成字段等標記位
typedef struct MarkerAttribute
{

} MarkerAttribute;

// 定長屬性
typedef struct ConstantValueAttribute
{
    uint16_t constantValueIndex;
} ConstantValueAttribute;

// 方法表示
typedef struct EnclosingMethodAttribute
{
    uint16_t classIndex;
    uint16_t methodIndex;
} EnclosingMethodAttribute;

// 指向異常表
typedef struct ExceptionsAttribute
{
    uint32_t len;
    uint16_t *exceptionIndexTable[];
} ExceptionsAttribute;

// 內部類
typedef struct InnerClassInfo
{
    uint16_t innerClassInfoIndex;
    uint16_t outerClassInfoIndex;
    uint16_t innerNameIndex;
    uint16_t innerClassAccessFlags;
} InnerClassInfo;

// 代碼行數信息,方便在出錯時定位問題,但不完全準確
typedef struct LineNumberTableEntry
{
    uint16_t startPc;
    uint16_t lineNumber;
} LineNumberTableEntry;

// 棧幀本地變量表
typedef struct LocalVariableTableEntry
{
    uint16_t startPc;
    uint16_t length;
    uint16_t nameIndex;
    uint16_t descriptorIndex;
    uint16_t index;
} LocalVariableTableEntry;

typedef struct MethodParameter
{
    uint16_t nameIndex;
    // 參數、方法、屬性、類都有權限控制標記
    uint16_t accessFlags;
} MethodParameter;

// JDK8以後可以指定編譯器保留形參的名稱
typedef struct MethodParameters
{
    uint8_t len;
    MethodParameter **parameters;
} MethodParameters;

// 從哪編譯而來
typedef struct SourceFileAttribute
{
    uint16_t signatureIndex;
} SourceFileAttribute;

// 後續再解析的屬性
typedef struct UnparsedAttribute
{
    uint32_t nameLen;
    char *name;
    uint32_t length;
    uint32_t infoLen;
    char *info;
} UnparsedAttribute;

只展示了一部分解析後的結構體,對於我們不想解析的或者後續再解析的,我們統一使用UnparsedAttribute表示。

同樣的,我們也是按照類型逐個解析這些屬性表

attribute_info.h

static AttributeInfo *readAttribute(ClassReader *r, CP *cp)
{
    uint16_t attrNameIndex = readUint16(r);
    char *attrName = getUtf8(cp, attrNameIndex);
    u_int32_t attrLen = readUint32(r);
    struct AttributeInfo *rs = (AttributeInfo *)malloc(sizeof(struct AttributeInfo));
    rs->cp = cp;
    if (strcmp(attrName, "Code") == 0)
    {
        struct AttrCode *attr = (AttrCode *)malloc(sizeof(struct AttrCode));
        attr->maxStack = readUint16(r);
        attr->maxLocals = readUint16(r);
        attr->codeLen = readUint32(r);
        attr->code = readBytes(r, attr->codeLen);

        uint16_t exceptionTableLength = readUint16(r);
        ExceptionTable *exceptionTable = malloc(sizeof(ExceptionTable));
        exceptionTable->size = exceptionTableLength;
        exceptionTable->entrys = malloc(sizeof(ExceptionTableEntry *) * exceptionTableLength);

        for (int i = 0; i < exceptionTableLength; i++)
        {
            exceptionTable->entrys[i] = malloc(sizeof(ExceptionTableEntry));
            exceptionTable->entrys[i]->startPc = readUint16(r);
            exceptionTable->entrys[i]->endPc = readUint16(r);
            exceptionTable->entrys[i]->handlerPc = readUint16(r);
            exceptionTable->entrys[i]->catchType = readUint16(r);
        }
        attr->attributes = readAttributes(r, cp);
        rs->info = attr;
    }
    else if (strcmp(attrName, "ConstantValue") == 0)
    {
        struct ConstantValueAttribute *attr = (ConstantValueAttribute *)malloc(sizeof(struct ConstantValueAttribute));
        attr->constantValueIndex = readUint16(r);
        rs->info = attr;
    }
    
    ...其他類型的解析
    
    else
    {
        struct UnparsedAttribute *attr = (UnparsedAttribute *)malloc(sizeof(struct UnparsedAttribute));
        attr->name = attrName;
        attr->infoLen = attrLen;
        attr->info = readBytes(r, attrLen);
    }
}

至此,我們已經將class文件解析爲ClassFile結構體,接下來可以把它交給ClassLoader加載爲運行時的Class結構體。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章