上一篇我們已經根據路徑讀取到了我們需要的位元組碼文件,就以java.lang.Object這個類為例,可以看到類似下麵這種東西,那麼這些數字是什麼呢? 要瞭解這個,我們大概可以猜到這是十進位的,線上將十進位轉為十六進位看看https://tool.oschina.net/hexconvert/,註意上 ...
上一篇我們已經根據路徑讀取到了我們需要的位元組碼文件,就以java.lang.Object這個類為例,可以看到類似下麵這種東西,那麼這些數字是什麼呢?
要瞭解這個,我們大概可以猜到這是十進位的,線上將十進位轉為十六進位看看https://tool.oschina.net/hexconvert/,註意上圖中已經用空格隔開了每個數,我們將最前面的變成十六進位看看效果,202對應CA,254對應FE,186對應BA,190對應BE,合起來就是CAFEBABE,有興趣的可以查查這代表的時一種咖啡,所有的符合jvm規範的位元組碼文件都是以這個開頭,專業稱呼 "魔數";
不知道大家有沒有發現,如果我們分析這個的時候要自己一個一個的轉換,簡直太坑爹了,但是有很多工具可以幫助我們更好的看十六進位的,比如vscode,editplus,winhex,jclasslib(這個看不到十六進位,但是可以看位元組碼文件的結構),實在不想下載的其他東西話用vim也可以看十六進位;這裡強烈推薦一款工具叫做classpy,這個工具可以同時看十六進位和class位元組碼文件的結構,用起來很舒服;
鏈接:https://pan.baidu.com/s/1s_fqLxQjG0lVXMEB5z1mlg 提取碼:gmyt ,使用這個classpy的時候,但是有一個前提,你電腦必須要有gradle環境!!!首先解壓,然後需要進入classpy-master文件夾,命令行運行gradle uberjar,最後就是gradle run ,以後每次的話直接使用gradle run就行了!打開ui界面之後,把class手動丟進去就行了,如下圖,左邊是class文件的結構,右邊的對應的十六進位;
1.簡單說說class文件結構
首先說說class位元組碼文件的結構,看有哪幾部分組成,其實在上圖左邊已經差不多說明瞭,下圖更清楚:其中u2表示兩個位元組,u4表示四個位元組,這之外的比如cp_info表示的是一張表,然後表中每一個欄位又對應著一張表(這麼說肯定不好理解,見過多維數組沒,表就看作數組就好,只不多數組每個位置又對應這一個數組,這就叫多維數組);
至於下麵這些代表什麼意思,這裡 就不多做贅述了,自己去看位元組碼文件的組成吧,不是我們的重點;
這裡的結構有個很有意思的現象,就是在列出該項數據之前,會提前指明該數據有幾個位元組;比如constant_pool_count表示常量池中有n個表,占用2個位元組;而緊接其後的constant_pool[constant_pool_count-1]存的就是各個表實際的數據,由於每個表第一個位元組表示該表的類型,然後後面又會指定該表的大小,所以可以確定總共占用多少位元組;access_flags表示訪問許可權,占兩個位元組,等等
接下來說說常量池中表的類型以及每個表的結構(每一種表都標識了自己占用的位元組大小),如下所示,每一種表都有自己特有的結構,還要註意一點,下麵這麼多表中,某一個表中某一項可能會引用另外一張表的數據的;
常量池之外每個部分表示的什麼,我隨便找了一篇博客,參考這篇說的比較仔細的:https://www.jianshu.com/p/247e2475fc3a;這就不多說了,這也不是我們的重點;
2.讀取class位元組碼文件
總的目錄結構如下所示:
根據上面這個圖我們將classfile中的文件分為幾個部分理解一下,首先是class_reader.go這個文件裡面是結構體,存了class文件的全部數據的位元組切片,並且定義了一些方法一下子讀取1位元組,2位元組,4位元組和8位元組等方法,方便於我們讀取數據;
然後class_file.go文件中一個結構體,存了位元組碼中所有結構,就是魔數,版本號,常量池,訪問修飾符等等,然後定義了一些獲取這些部分的方法,可想而知這些方法需要使用前面說的class_reader.go文件中結構體讀取數據;
再然後比較關鍵,就是class_file.go文件中定義的那些獲取各個部分的方法,下圖所示,其中最關鍵的就是讀取常量池和屬性表;
說道讀取常量池數據,那麼因為常量池中有很多不同類型的表,我們定義一個介面,所有的表都必須實現這個介面;至於總共有些什麼類型的表,大致分為兩種,一種是字元型,一種是引用型的;字元型的分為字元串和數字類的,分別是在上面的cp_utf8.go和cp_numberic.go中,其他的以cp開頭的都是引用類型的表;
在讀取常量池中的表的時候,我們首先要確定正在讀取表的類型,在讀取第一個位元組的時候,該位元組就是說明該表示什麼類型,如下所示,然後每一種表都規定了位元組的結構,前面已經說明白了;
const ( CONSTANT_Utf8 = 1 CONSTANT_Integer = 3 CONSTANT_Float = 4 CONSTANT_Long = 5 CONSTANT_Double = 6 CONSTANT_Class = 7 CONSTANT_String = 8 CONSTANT_Fieldref = 9 CONSTANT_Methodref = 10 CONSTANT_InterfaceMethodref = 11 CONSTANT_NameAndType = 12 CONSTANT_MethodHandle = 15 CONSTANT_MethodType = 16 CONSTANT_InvokeDynamic = 18 )
然後就是屬性表,其實和常量池差不多定義了一個頂層介面,只不過屬性表這裡不是用這種數字來決定表的類型,而是用屬性名(也就是字元串來區分),所以我們可以看到下麵這種結構,通過讀取屬性表前面兩個位元組找到常量池的Constant_Utf8表的索引,然後取到字元串,再到下麵這個switch中確定是什麼類型的屬性表;
屬性表也有很多類型,我們這裡只是列舉其中的8種,至於每一種是什麼意思,看看這個博客:https://www.cnblogs.com/lrh-xl/p/5351182.html,在上面的目錄中attr_xxx開頭的都是屬性表,
3.各個文件
class_reader.go:用於幫助我們讀取位元組切片中的數據:
package classfile import "encoding/binary" //這個結構體從位元組數組中讀取數據 type ClassReader struct { data []byte } //讀取一個位元組,而且data數據也要將第一個位元組幹掉 func (this *ClassReader) readUint8() uint8 { //u1 val := this.data[0] this.data = this.data[1:] return val } //讀取兩個位元組 func (this *ClassReader) readUint16() uint16 { //u2 val := binary.BigEndian.Uint16(this.data) this.data = this.data[2:] return val } //讀取四個位元組 func (this *ClassReader) readUint32() uint32 { //u4 val := binary.BigEndian.Uint32(this.data) this.data = this.data[4:] return val } //讀取8個位元組 func (this *ClassReader) readUint64() uint64 { val := binary.BigEndian.Uint64(this.data) this.data = this.data[8:] return val } //讀取最前面的兩個位元組,表示數量 //根據這個數量繼續往後面讀取n個uint16的位元組 func (this *ClassReader) readUint16s() []uint16 { n := this.readUint16() s := make([]uint16, n) for i := range s { s[i] = this.readUint16() } return s } //獲取指定數量的位元組 func (this *ClassReader) readBytes(length uint32) []byte { bytes := this.data[:length] this.data = this.data[length:] return bytes }View Code
class_file.go:定義了位元組碼文件的結構
package classfile import "fmt" //這個結構體就是體現了class文件的內容 type ClassFile struct { magic uint32 //魔數 u4 minorVersion uint16 //次版本號 u2 majorVersion uint16 //主版本號 u2 constantPool ConstantPool //常量池 accessFlags uint16 //修飾符 thisClass uint16 //當前類 superClass uint16 //父類 interfaces []uint16 //介面,木有介面的數組 fields []*MemberInfo //欄位 methods []*MemberInfo //方法 attributes []AttributeInfo //屬性,例如全類名就是保存在這裡 } //這個方法就是將byte數組解析成FileClass結構體 func Parse(classData []byte) (cf *ClassFile, err error) { //defer和recover模式,類似於java中的finally,這裡就是做一個異常不火再進行處理 defer func() { if r := recover(); r != nil { var ok bool err, ok = r.(error) if !ok { err = fmt.Errorf("%v", r) } } }() //實例化一個ClassFile實例,用於保存位元組碼各個部分信息 cf = &ClassFile{} //實例化一個class文件解析器,將存有位元組碼文件所有信息的數組傳遞進去 cr := &ClassReader{classData} //read方法開始解析class文件各個部分的數據 cf.read(cr) return } //這個方法就是按照位元組碼文件中各部分的順序進行讀取 func (this *ClassFile) read(reader *ClassReader) { this.readAndCheckMagic(reader) this.readAndCheckVersion(reader) this.constantPool = readConstantPool(reader) this.accessFlags = reader.readUint16() this.thisClass = reader.readUint16() this.superClass = reader.readUint16() this.interfaces = reader.readUint16s() this.fields = readMembers(reader, this.constantPool) this.methods = readMembers(reader, this.constantPool) this.attributes = readAttributes(reader, this.constantPool) } //獲取魔數,魔數是占有4個位元組 func (this *ClassFile) readAndCheckMagic(reader *ClassReader) { magic := reader.readUint32() //註意,所有符合jvm規範的魔數都是CAFEBABE,不符合條件的直接panic終止程式 if magic != 0xCAFEBABE { panic("java.lang.ClassFormatError:magic") } } //次版本號和主版本號都是兩個位元組 //次版本號在jdk1.2之後就沒有用過了,都是0 //主版本號的版本從1.2開始是45,每次經過一個大的版本,就會+1,現在是52 func (this *ClassFile) readAndCheckVersion(reader *ClassReader) { this.minorVersion = reader.readUint16() this.majorVersion = reader.readUint16() switch this.majorVersion { case 45: return case 46, 47, 48, 49, 50, 51, 52: if this.minorVersion == 0 { return } } panic("java.lang.UnsupportedClassVersionError") } //獲取主版本號 func (this *ClassFile) MinorVersion() uint16 { return this.minorVersion } //獲取副版本號 func (this *ClassFile) MajorVersion() uint16 { return this.majorVersion } //獲取常量池 func (this *ClassFile) ConstantPool() ConstantPool { return this.constantPool } //獲取修飾符 func (this *ClassFile) AccessFlags() uint16 { return this.accessFlags } //從常量池中獲取類名 func (this *ClassFile) ClassName() string { return this.constantPool.getClassName(this.thisClass) } //從常量池中獲取超類名,註意,這裡需要判斷是不是Object類 func (this *ClassFile) SuperClassName() string { if this.superClass > 0 { return this.constantPool.getClassName(this.superClass) } return "" //這裡當類是Object的時候,那麼self.superClass為0 } //獲取欄位 func (this *ClassFile) Fields() []*MemberInfo { return this.fields } //獲取方法 func (this *ClassFile) Methods() []*MemberInfo { return this.methods } //從常量池中找實現的所有介面名稱 func (this *ClassFile) InterfacesNames() []string { interfaceNames := make([]string, len(this.interfaces)) for index, value := range this.interfaces { interfaceNames[index] = this.constantPool.getClassName(value) } return interfaceNames }View Code
constant_pool.go:定義了一些方法幫助我們根據索引獲取各種表
package classfile //這個介面表示常量池中每一張表 type ConstantInfo interface { readInfo(reader *ClassReader) } //常量池,其實就是所有類型表的切片 type ConstantPool []ConstantInfo //用於讀取常量池中的表,將常量池中每張表解析之後放到這個切片中來,然後就可以根據索引獲取表數據了 //首先兩個位元組是常量池中表的個數cpCount,緊接著就是各種表的實際數據,每個表中第一個欄位表示了自己是什麼類型的表, // 然後也已經規定好了自己所占位元組大小 //註意兩種表ConstantLongInfo和ConstantDoubleInfo,這種表示占兩個位置,其他類型的占用一個位置 //所以常量池中表實際的數量肯定是要小於cpCount func readConstantPool(reader *ClassReader) ConstantPool { cpCount := int(reader.readUint16()) cp := make([]ConstantInfo, cpCount) //註意,常量池遍歷從1開始,0表示不指向任何常量池數據 for i := 1; i < cpCount; i++ { cp[i] = readConstantInfo(reader, cp) switch cp[i].(type) { case *ConstantLong, *ConstantDouble: //如果是這兩種類型的表,那麼在常量池中就占兩個位置 i++ } } return cp } //根據索引值獲取常量池中表 func (this ConstantPool) getConstantInfo(index uint16) ConstantInfo { if cpInfo := this[index]; cpInfo != nil { return cpInfo } panic("Invalid constant pool index!") } //根據索引從常量池中獲取某個ConstantNameAndTypeInfo表,然後獲取這張表的名字和描述 //註意,這個名字和描述分別又對應著常量池中的表 func (this ConstantPool) getNameAndType(index uint16) (string, string) { //這裡做了一個斷言,因為這裡沒有接收nil,所以如果失敗,直接panic ntInfo := this.getConstantInfo(index).(*ConstantNameAndTypeInfo) name := this.getUtf8(ntInfo.nameIndex) _type := this.getUtf8(ntInfo.descriptorIndex) return name, _type } //根據索引獲取常量池中ConstantClassInfo表,獲取該表的名字 //這個名字又對應常量池中一張ConstantUtf8Info表 func (this ConstantPool) getClassName(index uint16) string { classInfo := this.getConstantInfo(index).(*ConstantClassInfo) return this.getUtf8(classInfo.nameIndex) } //根據索引獲取常量池中的ConstantUtf8Info表,獲取其中保存的值 func (this ConstantPool) getUtf8(index uint16) string { utf8Info := this.getConstantInfo(index).(*ConstantUtf8Info) return utf8Info.str } //讀取常量池中的一個表,註意,不管是什麼表,它的第一個位元組tag表示表的類型 //我們這裡先獲取表的類型,然後實例化相應的表,最後調用該表實現的readInfo方法讀取表數據 func readConstantInfo(reader *ClassReader, constantPool ConstantPool) ConstantInfo { tag := reader.readUint8() info := newConstantInfo(tag, constantPool) info.readInfo(reader) return info } //下麵就是常量池中的所有類型,其中最下麵三種被註釋了,是因為這是在jdk7才被添加的, // 為了支持新增的invokedynamic指令 //而且從下麵我們大概將常量池分為兩大類,字面量和符號引用; //字面量:字元串常量和數字常量 //符號引用:類名,介面名,以及欄位和方法信息,為什麼叫做符號引用呢?因為這幾個表中沒有存實際的數據, //存的都是指向常量池中ConstantUtf8Info表的索引 func newConstantInfo(tag uint8, constantPool ConstantPool) ConstantInfo { switch tag { case CONSTANT_Utf8: return &ConstantUtf8Info{} case CONSTANT_Integer: return &ConstantIntegerInfo{} case CONSTANT_Float: return &ConstantFloatInfo{} case CONSTANT_Long: return &ConstantLong{} case CONSTANT_Double: return &ConstantDouble{} case CONSTANT_Class: return &ConstantClassInfo{} case CONSTANT_String: return &ConstantStringInfo{} case CONSTANT_Fieldref: return &ConstantFieldrefInfo{} case CONSTANT_Methodref: return &ConstantMethodrefInfo{} case CONSTANT_InterfaceMethodref: return &ConstantInterfaceMethodrefInfo{} case CONSTANT_NameAndType: return &ConstantNameAndTypeInfo{} //case CONSTANT_MethodHandle: // return &ConstantMethodHandleInfo{} //case CONSTANT_MethodType: // return &ConstantMethodTypeInfo{} //case CONSTANT_InvokeDynamic: // return &ConstantInvokeDynamic{} default: panic("java.lang.ClassFormatError: constant pool tag!") } }View Code
constant_info.go:常量池中表的類型
package classfile const ( CONSTANT_Utf8 = 1 CONSTANT_Integer = 3 CONSTANT_Float = 4 CONSTANT_Long = 5 CONSTANT_Double = 6 CONSTANT_Class = 7 CONSTANT_String = 8 CONSTANT_Fieldref = 9 CONSTANT_Methodref = 10 CONSTANT_InterfaceMethodref = 11 CONSTANT_NameAndType = 12 CONSTANT_MethodHandle = 15 CONSTANT_MethodType = 16 CONSTANT_InvokeDynamic = 18 )View Code
cp_utf8.go:
package classfile type ConstantUtf8Info struct { str string } //CONSTANT_Utf8_info { //u1 tag; //u2 length; //u1 bytes[length]; //} //註意,這種表,第一個位元組表示表的類型,然後兩個位元組表示該表存的字元串的長度 //最後根據這個長度去讀取第三部分的數據,返回位元組切片,我們簡單的轉為字元串 func (this *ConstantUtf8Info) readInfo(reader *ClassReader) { length := uint32(reader.readUint16()) bytes := reader.readBytes(length) this.str = string(bytes) }View Code
cp_numberic.go:
package classfile import ( "math" ) //該文件放四種與數字相關的表 //第一種表 type ConstantIntegerInfo struct { val int32 } //實現了ConstantInfo介面,這種表第一個位元組表示類型,後面4個位元組表示存的數據 //CONSTANT_Integer_info { //u1 tag; //u4 bytes; //} func (this *ConstantIntegerInfo) readInfo(reader *ClassReader) { readUint32 := reader.readUint32() this.val = int32(readUint32) } //第二種表 type ConstantLong struct { val int64 } //CONSTANT_Long_info { //u1 tag; //u4 high_bytes; //u4 low_bytes; //} func (this *ConstantLong) readInfo(reader *ClassReader) { readUint64 := reader.readUint64() this.val = int64(readUint64) } //第三種表 type ConstantFloatInfo struct { val float32 } //CONSTANT_Float_info { //u1 tag; //u4 bytes; //} func (this *ConstantFloatInfo) readInfo(reader *ClassReader) { readUint32 := reader.readUint32() //將uint32類型的轉為float32類型的 this.val = math.Float32frombits(readUint32) } //第四種表 type ConstantDouble struct { val float64 } //CONSTANT_Double_info { //u1 tag; //u4 high_bytes; //u4 low_bytes; //} func (this *ConstantDouble) readInfo(reader *ClassReader) { readUint64 := reader.readUint64() this.val = math.Float64frombits(readUint64) }View Code
cp_string.go
package classfile //CONSTANT_String_info { //u1 tag; //u2 string_index; //} //這個表中沒有存數據,第一個位元組表示該表的類型,再之後的兩個位元組表示索引 // 這個索引表示指向常量池中ConstantUtf8Info表 type ConstantStringInfo struct { pool ConstantPool stringIndex uint16 } func (this *ConstantStringInfo) readInfo(reader *ClassReader) { this.stringIndex = reader.readUint16() } //獲取ConstantStringInfo對應的字元串 //在常量池中根據索引找到對應的ConstantUtf8Info表 func (this *ConstantStringInfo) String() string { return this.pool.getUtf8(this.stringIndex) }View Code
cp_name_and_type.go
package classfile //CONSTANT_NameAndType_info { //u1 tag; //u2 name_index; //u2 descriptor_index; //} type ConstantNameAndTypeInfo struct { nameIndex uint16 descriptorIndex uint16 } func (this *ConstantNameAndTypeInfo) readInfo(reader *ClassReader) { this.nameIndex = reader.readUint16() this.descriptorIndex = reader.readUint16() }View Code
cp_member_ref.go
package classfile //CONSTANT_Fieldref_info { //u1 tag; //u2 class_index; //u2 name_and_type_index; //} type ConstantMemberrefInfo struct { pool ConstantPool classIndex uint16 nameAndTypeIndex uint16 } func (this *ConstantMemberrefInfo) readInfo(reader *ClassReader) { this.classIndex = reader.readUint16() this.nameAndTypeIndex = reader.readUint16() } func (this *ConstantMemberrefInfo) ClassName() string { return this.pool.getClassName(this.classIndex) } func (this *ConstantMemberrefInfo) NameAndDescriptor() (string, string) { return this.pool.getNameAndType(this.nameAndTypeIndex) } type ConstantFieldrefInfo struct { ConstantMemberrefInfo } type ConstantMethodrefInfo struct { ConstantMemberrefInfo } type ConstantInterfaceMethodrefInfo struct { ConstantMemberrefInfo }View Code
cp_class.go
package classfile //CONSTANT_Class_info { //u1 tag; //u2 name_index; //} type ConstantClassInfo struct { pool ConstantPool nameIndex uint16 } func (this *ConstantClassInfo) readInfo(reader *ClassReader) { this.nameIndex = reader.readUint16() } func (this *ConstantClassInfo) Name() string { return this.pool.getUtf8(this.nameIndex) }View Code
member_info.go:方法表的欄位表都是一樣的,只是其中屬性表有點差異,所以可以用下麵這個結構體表示:
package classfile //field_info { //u2 access_flags; //u2 name_index; //u2 descriptor_index; //u2 attributes_count; //attribute_info attributes[attributes_count]; //} //欄位表和方法表的結構幾乎是一樣的,只是屬性表不同,就用這個結構體表示 type MemberInfo struct { constPool ConstantPool accessFlags uint16 //訪問修飾符 nameIndex uint16 //欄位名 descriptorIndex uint16 //欄位的類型 attributes []AttributeInfo //屬性表切片 } //func (self *MemberInfo) AccessFlags() uint16 {...} // getter //func (self *MemberInfo) Name() string {...} //func (self *MemberInfo) Descriptor() string {...} //因為欄位或者方法可能有多個,所以就遍歷進行讀取 func readMembers(reader *ClassReader, cp ConstantPool) []*MemberInfo { memberCount := reader.readUint16() infos := make([]*MemberInfo, memberCount) for index := range infos { infos[index] = readMember(reader, cp) } return infos } func readMember(reader *ClassReader, cp ConstantPool) *MemberInfo { return &MemberInfo{ constPool: cp, accessFlags: reader.readUint16(), nameIndex: reader.readUint16(), descriptorIndex: reader.readUint16(), attributes: readAttributes(reader, cp), } } //根據索引獲取常量池中的ConstantUtf8Info表中存的欄位或者方法的字面量 func (this *MemberInfo) Name() string { return this.constPool.getUtf8(this.nameIndex) } //根據索引獲取常量池中的ConstantNameAndTypeInfo表中的欄位或者方法的描述 func (this *MemberInfo) Descriptor() string { return this.constPool.getUtf8(this.descriptorIndex) }View Code
再下麵的都是屬性表相關的內容(包括八個屬性表):
attribute_info.go:屬性表對應的頂層介面,所有的屬性表必須實現該介面
package classfile //attribute_info { //u2 attribute_name_index; //u4 attribute_length; //u1 info[attribute_length]; //} //各個屬性表表達的屬性都不相同,所以不能用常量池中表的類型可以靠tag來區分 //這裡是使用屬性名來區分 //並且屬性表中也沒有存實際的數據,存的是指向常量池中ConstantUtf8Info表的索引 type AttributeInfo interface { readInfo(reader *ClassReader) } //這裡的話獲取class文件中屬性表的數量,根據屬性表的數量去常量池中讀取屬性表 func readAttributes(reader *ClassReader, pool ConstantPool) []AttributeInfo { attributesCount := reader.readUint16() attributes := make([]AttributeInfo, attributesCount) for i := range attributes { attributes[i] = readAttribute(reader, pool) } return attributes } //至於怎麼讀屬性表呢?首先讀前兩個位元組表示屬性名稱的索引 // 根據這個索引去常量池中獲取ConstantUtf8Info表中的數據獲取屬性名稱 //然後再讀取4個位元組表示屬性表的長度,根據屬性名稱和長度去讀取各種屬性表的數據,保存到各個結構體中 func readAttribute(reader *ClassReader, pool ConstantPool) AttributeInfo { attrNameIndex := reader.readUint16() attrName := pool.getUtf8(attrNameIndex) attrLength := reader.readUint32() attrInfo := newAttributeInfo(attrName, attrLength, pool) attrInfo.readInfo(reader) return attrInfo } func newAttributeInfo(attrName string, attrLen uint32, pool ConstantPool) AttributeInfo { switch attrName { case "Deprecated": return &DeprecatedAttribute{} case "Synthetic": return &SyntheticAttribute{} case "SourceFile":