Golang實現(xiàn)Java虛擬機之解析class文件詳解
前言
所需前置知識為:JAVA語言、JVM知識
對應(yīng)項目:jvmgo
一、準備環(huán)境
操作系統(tǒng):Windows 11
1.1 JDK版本
openjdk version "1.8.0_382"
1.2 Go版本
go version go1.21.0 windows/amd64
1.3 配置Go工作空間
1.4 java命令指示
Java虛擬機的工作是運行Java應(yīng)用程序。和其他類型的應(yīng)用程序一樣,Java應(yīng)用程序也需要一個入口點,這個入口點就是我們熟知的main()
方法。最簡單的Java程序是 只有一個main()
方法的類,如著名的HelloWorld程序。
public class HelloWorld { public static void main(String[] args) { System.out.println("Hello, world!"); } }
JVM如何知道從哪個類啟動呢,虛擬機規(guī)范并沒有明確,而是需要虛擬機實現(xiàn)。比如Oracle的JVM就是通過java
命令啟動的,主類名由命令行參數(shù)決定。
java命令有如下4種形式:
java [-options] class [args] java [-options] -jar jarfile [args] javaw [-options] class [args] javaw [-options] -jar jarfile [args]
可以向java
命令傳遞三組參數(shù):選項、主類名(或者JAR文件名) 和main()
方法參數(shù)。選項由減號(–)開頭。通常,第一個非選項參數(shù) 給出主類的完全限定名(fully qualified class name)。但是如果用戶提供了–jar選項,則第一個非選項參數(shù)表示JAR文件名,java
命令必須從這個JAR文件中尋找主類。javaw
命令和java命令幾乎一樣,唯 一的差別在于,javaw
命令不顯示命令行窗口,因此特別適合用于啟 動GUI(圖形用戶界面)應(yīng)用程序。
選項可以分為兩類:標準選項和非標準選項。標準選項比較穩(wěn)定,不會輕易變動。非標準選項以-X開頭,
選項 | 用途 |
---|---|
-version | 輸出版本信息,然后退出 |
-? / -help | 輸出幫助信息,然后退出 |
-cp / -classpath | 指定用戶類路徑 |
-Dproperty=value | 設(shè)置Java系統(tǒng)屬性 |
-Xms<size> | 設(shè)置初始堆空間 大小 |
-Xmx<size> | 設(shè)置最大堆空間 大小 |
-Xss<size> | 設(shè)置線程??臻g 大小 |
二、編寫命令行工具
環(huán)境準備完畢,接下來實現(xiàn)java
命令的的第一種用法。
2.1 創(chuàng)建目錄
創(chuàng)建cmd.go
Go源文件一般以.go作為后綴,文件名全部小寫,多個單詞之間使用下劃線分隔。Go語言規(guī)范要求Go源文件必須使用UTF-8編碼,詳見https://golang.org/ref/spec
2.2 結(jié)構(gòu)體存儲cmd選項
在文件中定義cmd中java命令需要的選項和參數(shù)
package ch01 // author:郜宇博 type Cmd struct { // 標注是否為 --help helpFlag bool //標注是否為 --version versionFlag bool //選項 cpOption string //主類名,或者是jar文件 class string //參數(shù) args []string }
Go語言標準庫包
由于要處理的命令行,因此將使用到flag()
函數(shù),此函數(shù)為Go的標準庫包之一。
Go語言的標準庫以包的方式提供支持,下表列出了Go語言標準庫中常見的包及其功能。
Go語言標準庫包名 | 功 能 |
---|---|
bufio | 帶緩沖的 I/O 操作 |
bytes | 實現(xiàn)字節(jié)操作 |
container | 封裝堆、列表和環(huán)形列表等容器 |
crypto | 加密算法 |
database | 數(shù)據(jù)庫驅(qū)動和接口 |
debug | 各種調(diào)試文件格式訪問及調(diào)試功能 |
encoding | 常見算法如 JSON、XML、Base64 等 |
flag | 命令行解析 |
fmt | 格式化操作 |
go | Go語言的詞法、語法樹、類型等??赏ㄟ^這個包進行代碼信息提取和修改 |
html | HTML 轉(zhuǎn)義及模板系統(tǒng) |
image | 常見圖形格式的訪問及生成 |
io | 實現(xiàn) I/O 原始訪問接口及訪問封裝 |
math | 數(shù)學庫 |
net | 網(wǎng)絡(luò)庫,支持 Socket、HTTP、郵件、RPC、SMTP 等 |
os | 操作系統(tǒng)平臺不依賴平臺操作封裝 |
path | 兼容各操作系統(tǒng)的路徑操作實用函數(shù) |
plugin | Go 1.7 加入的插件系統(tǒng)。支持將代碼編譯為插件,按需加載 |
reflect | 語言反射支持??梢詣討B(tài)獲得代碼中的類型信息,獲取和修改變量的值 |
regexp | 正則表達式封裝 |
runtime | 運行時接口 |
sort | 排序接口 |
strings | 字符串轉(zhuǎn)換、解析及實用函數(shù) |
time | 時間接口 |
text | 文本模板及 Token 詞法器 |
flag()函數(shù)
[]: https://studygolang.com/pkgdoc
eg:flag.TypeVar()
基本格式如下: flag.TypeVar(Type指針, flag名, 默認值, 幫助信息)
例如我們要定義姓名、年齡、婚否三個命令行參數(shù),我們可以按如下方式定義:
var name string var age int var married bool var delay time.Duration flag.StringVar(&name, "name", "張三", "姓名") flag.IntVar(&age, "age", 18, "年齡") flag.BoolVar(&married, "married", false, "婚否") flag.DurationVar(&delay, "d", 0, "時間間隔")
2.3 接收處理用戶輸入的命令行指令
創(chuàng)建parseCmd()函數(shù),實現(xiàn)接受處理用戶輸入的命令行指令
func parseCmd() *Cmd { cmd := &Cmd{} flag.Usage = printUsage flag.BoolVar(&cmd.helpFlag, "help", false, "print help message") flag.BoolVar(&cmd.helpFlag, "?", false, "print help message") flag.BoolVar(&cmd.versionFlag, "version", false, "print version and exit") flag.StringVar(&cmd.cpOption, "classpath", "", "classpath") flag.StringVar(&cmd.cpOption, "cp", "", "classpath") flag.Parse() args := flag.Args() if len(args) > 0 { cmd.class = args[0] cmd.args = args[1:] } return cmd } func printUsage() { fmt.Printf("Usage: %s [-options] class [args...]\n", os.Args[0]) //flag.PrintDefaults() }
首先設(shè)置flag.Usage
變量,把printUsage()
函數(shù)賦值給它;
然后調(diào) 用flag包提供的各種Var函數(shù)設(shè)置需要解析的選項;
接著調(diào)用 Parse()
函數(shù)解析選項。
如果Parse()
函數(shù)解析失敗,它就調(diào)用printUsage()
函數(shù)把命令的用法打印到控制臺。
如果解析成功,調(diào)用flag.Args()
函數(shù)可以捕獲其他沒有被解析 的參數(shù)。其中第一個參數(shù)就是主類名,剩下的是要傳遞給主類的參數(shù)。
2.4 測試
與cmd.go文件一樣,main.go文件的包名也是main。在Go 語言中,main是一個特殊的包,這個包所在的目錄(可以叫作任何 名字)會被編譯為可執(zhí)行文件。Go程序的入口也是main()函數(shù),但 是不接收任何參數(shù),也不能有返回值。
測試代碼如下:
package main import "fmt" func main() { cmd := parseCmd() if cmd.versionFlag { //模擬輸出版本 fmt.Println("version 0.0.1") } else if cmd.helpFlag || cmd.class == "" { printUsage() } else { startJVM(cmd) } } // 模擬啟動JVM func startJVM(cmd *Cmd) { fmt.Printf("classpath:%s class:%s args:%v\n", cmd.cpOption, cmd.class, cmd.args) }
main()
函數(shù)先調(diào)用ParseCommand()
函數(shù)解析命令行參數(shù),如 果一切正常,則調(diào)用startJVM()
函數(shù)啟動Java虛擬機。如果解析出現(xiàn)錯誤,或者用戶輸入了-help選項,則調(diào)用PrintUsage()
函數(shù)打印出幫助信息。如果用戶輸入了-version
選項,則輸版本信息。因為我們還沒有真正開始編寫Java虛擬機,所以startJVM()
函數(shù)暫時只是打印一些信息而已。
在終端:
go install jvmgo\ch0
此時在工作空間的bin目錄中會生成ch01.exe的文件,運行:結(jié)果如下
三、獲取類路徑
已經(jīng)完成了JAVA應(yīng)用程序如何啟動:命令行啟動,并獲取到了啟動時需要的選項和參數(shù)。
但是,如果要啟動一個最簡單的“Hello World”程序(如下),也需要加載很多所需的類進入JVM
中
public class HelloWorld { public static void main(String[] args) { System.out.println("Hello, world!"); } }
加載HelloWorld類之前,需要加載該類的父類(超類),也就是java.lang.Object
,main函數(shù)的參數(shù)為String[]
類型,因此也需要將java.lang.String
類和java.lang.String[]
加載,輸出字符串又需要加載java.lang.System
類,等等。接下來就來解決如何獲取這些類的路徑。
3.1類路徑介紹
Java虛擬機規(guī)范并沒有規(guī)定虛擬機應(yīng)該從哪里尋找類,因此不同的虛擬機實現(xiàn)可以采用不同的方法。
Oracle的Java虛擬機實現(xiàn)根據(jù)類路徑(class path)來搜索類。
按照搜索的先后順序,類路徑可以 分為以下3個部分:
- 啟動類路徑(bootstrap classpath)
- 擴展類路徑(extension classpath)
- 用戶類路徑(user classpath)
啟動類路徑默認對應(yīng)jre\lib
目錄,Java標準庫(大部分在rt.jar
里) 位于該路徑。
擴展類路徑默認對應(yīng)jre\lib\ext
目錄,使用Java擴展機制的類位于這個路徑。
用戶類路徑為自己實現(xiàn)的類,以及第三方類庫的路徑??梢酝ㄟ^-Xbootclasspath
選項修改啟動類路徑,不過一般不需要這樣做。
用戶類路徑的默認值是當前目錄,也就是.
。可以設(shè)置 CLASSPATH環(huán)境變量來修改用戶類路徑,但是這樣做不夠靈活,所以不推薦使用。
更好的辦法是給java命令傳遞-classpath
(或簡寫為-cp
)選項。-classpath/-cp
選項的優(yōu)先級更高,可以覆蓋CLASSPATH環(huán)境變量
設(shè)置。如下:
java -cp path\to\classes ... java -cp path\to\lib1.jar ... java -cp path\to\lib2.zip ...
3.2解析用戶類路徑
該功能建立在命令行工具上,因此復制上次的代碼,并創(chuàng)建classpath子目錄。
Java虛擬機將使用JDK的啟動類路徑來尋找和加載Java 標準庫中的類,因此需要某種方式指定jre目錄的位置。
命令行選項可以獲取,所以增加一個非標準選項-Xjre。
修改Cmd結(jié)構(gòu)體,添加XjreOption字段;parseCmd()函數(shù)也要相應(yīng)修改:
type Cmd struct { // 標注是否為 --help helpFlag bool //標注是否為 --version versionFlag bool //選項 cpOption string //主類名,或者是jar文件 class string //參數(shù) args []string // jre路徑 XjreOption string } func parseCmd() *Cmd { cmd := &Cmd{} flag.Usage = printUsage flag.BoolVar(&cmd.helpFlag, "help", false, "print help message") flag.BoolVar(&cmd.helpFlag, "?", false, "print help message") flag.BoolVar(&cmd.versionFlag, "version", false, "print version and exit") flag.StringVar(&cmd.cpOption, "classpath", "", "classpath") flag.StringVar(&cmd.cpOption, "cp", "", "classpath") flag.StringVar(&cmd.XjreOption, "Xjre", "", "path to jre") flag.Parse() args := flag.Args() if len(args) > 0 { //第一個參數(shù)是主類名 cmd.class = args[0] cmd.args = args[1:] } return cmd }
3.3獲取用戶類路徑
可以把類路徑想象成一個大的整體,它由啟動類路徑、擴展類路徑和用戶類路徑三個小路徑構(gòu)成。
三個小路徑又分別由更小的路徑構(gòu)成。是不是很像組合模式(composite pattern)
?
接下來將使用組合模式來設(shè)計和實現(xiàn)類路徑。
1.Entry接口
定義一個Entry
接口,作為所有類的基準。
package classpath import "os" // :(linux/unix) or ;(windows) const pathListSeparator = string(os.PathListSeparator) type Entry interface { // className: fully/qualified/ClassName.class readClass(classpath string) ([]byte, Entry, error) String() string }
常量pathListSeparator是string類型,存放路徑分隔符,后面會用到。
Entry接口中有個兩方法。
readClass()方法:負責尋找和加載class 文件。
參數(shù)是class文件的相對路徑,路徑之間用斜線/
分隔,文件名有.class
后綴。比如要讀取java.lang.Object
類,傳 入的參數(shù)應(yīng)該是java/lang/Object.class
。返回值是讀取到的字節(jié)數(shù)據(jù)、最終定位到class文件的Entry,以及錯誤信息。
String()方法:作用相當于Java中的toString()
,用于返回變量 的字符串表示。
Go的函數(shù)或方法允許返回多個值,按照慣例,可以使用最后一個返回值作為錯誤信息。
還需要一個類似于JAVA構(gòu)造函數(shù)的函數(shù),但在Go語言中沒有構(gòu)造函數(shù)的概念,對象的創(chuàng)建通常交由一個全局的創(chuàng)建函數(shù)來完成,以NewXXX來命令,表示"構(gòu)造函數(shù)"
newEntry()函數(shù)根據(jù)參數(shù)創(chuàng)建不同類型的Entry實例,代碼如下:
func newEntry(path string) Entry { ////如果路徑包含分隔符 表示有多個文件 if strings.Contains(path, pathListSeparator) { return newCompositeEntry(path) } //包含*,則說明要將相應(yīng)目錄下的所有class文件加載 if strings.HasSuffix(path, "*") { return newWildcardEntry(path) } //包含.jar,則說明是jar文件,通過zip方式加載 if strings.HasSuffix(path, ".jar") || strings.HasSuffix(path, ".JAR") || strings.HasSuffix(path, ".zip") || strings.HasSuffix(path, ".ZIP") { return newZipEntry(path) } return newDirEntry(path) }
2.實現(xiàn)類
存在四種類路徑指定方式:
- 普通路徑形式:gyb/gyb
- jar/zip形式:/gyb/gyb.jar
- 通配符形式:gyb/*
- 多個路徑形式:gyb/1:/gyb/2
DirEntry(普通形式)
創(chuàng)建entry_dir.go
,定義DirEntry結(jié)構(gòu)體:
package classpath import "io/ioutil" import "path/filepath" type DirEntry struct { absDir string } func newDirEntry(path string) *DirEntry { //轉(zhuǎn)化為絕對路徑 absDir, err := filepath.Abs(path) if err != nil { panic(err) } return &DirEntry{absDir} } func (self *DirEntry) readClass(className string) ([]byte, Entry, error) { //拼接類文件目錄 和 類文件名 // '/gyb/xxx/' + 'helloworld.class' = '/gyb/xxx/helloworld.class' fileName := filepath.Join(self.absDir, className) data, err := ioutil.ReadFile(fileName) return data, self, err } func (self *DirEntry) String() string { return self.absDir }
DirEntry
只有一個字段,用于存放目錄的絕對路徑。
和Java語言不同,Go結(jié)構(gòu)體不需要顯示實現(xiàn)接口,只要方法匹配即可。
ZipEntry(jar/zip形式)
package classpath import "archive/zip" import "errors" import "io/ioutil" import "path/filepath" type ZipEntry struct { absPath string } func newZipEntry(path string) *ZipEntry { absPath, err := filepath.Abs(path) if err != nil { panic(err) } return &ZipEntry{absPath} } func (self *ZipEntry) readClass(className string) ([]byte, Entry, error) { r, err := zip.OpenReader(self.absPath) if err != nil { return nil, nil, err } defer r.Close() for _, f := range r.File { if f.Name == className { rc, err := f.Open() if err != nil { return nil, nil, err } defer rc.Close() data, err := ioutil.ReadAll(rc) if err != nil { return nil, nil, err } return data, self, nil } } return nil, nil, errors.New("class not found: " + className) } func (self *ZipEntry) String() string { return self.absPath }
首先打開ZIP文件,如果這一步出錯的話,直接返回。然后遍歷 ZIP壓縮包里的文件,看能否找到class文件。如果能找到,則打開 class文件,把內(nèi)容讀取出來,并返回。如果找不到,或者出現(xiàn)其他錯 誤,則返回錯誤信息。有兩處使用了defer語句來確保打開的文件得 以關(guān)閉。
CompositeEntry(多路徑形式)
CompositeEntry
由更小的Entry
組成,正好可以表示成[]Entry。
在Go語言中,數(shù)組屬于比較低層的數(shù)據(jù)結(jié)構(gòu),很少直接使用。大部分情況下,使用更便利的slice類型。
構(gòu)造函數(shù)把參數(shù)(路徑列表)按分隔符分成小路徑,然后把每個小路徑都轉(zhuǎn)換成具體的 Entry實例。
package classpath import "errors" import "strings" type CompositeEntry []Entry func newCompositeEntry(pathList string) CompositeEntry { compositeEntry := []Entry{} for _, path := range strings.Split(pathList, pathListSeparator) { //去判斷 path 屬于哪其他三種哪一種情況 生成對應(yīng)的 ClassDirEntry類目錄對象 entry := newEntry(path) compositeEntry = append(compositeEntry, entry) } return compositeEntry } func (self CompositeEntry) readClass(className string) ([]byte, Entry, error) { //遍歷切片 中的 類目錄對象 for _, entry := range self { //如果找到了 對應(yīng)的 類 直接返回 data, from, err := entry.readClass(className) if err == nil { return data, from, nil } } //沒找到 返回錯誤 return nil, nil, errors.New("class not found: " + className) } func (self CompositeEntry) String() string { strs := make([]string, len(self)) for i, entry := range self { strs[i] = entry.String() } return strings.Join(strs, pathListSeparator) }
WildcardEntry(通配符形式)
WildcardEntry
實際上也是CompositeEntry
,所以就不再定義新的類型了。
首先把路徑末尾的星號去掉,得到baseDir,然后調(diào)用filepath
包的Walk()
函數(shù)遍歷baseDir創(chuàng)建ZipEntry
。Walk()
函數(shù)的第二個參數(shù) 也是一個函數(shù)。
在walkFn
中,根據(jù)后綴名選出JAR文件
,并且返回SkipDir跳過子目錄(通配符類路徑不能遞歸匹配子目錄下的JAR文件)。
package classpath import "os" import "path/filepath" import "strings" func newWildcardEntry(path string) CompositeEntry { //截取通用匹配符 /gyb/* 截取掉 * baseDir := path[:len(path)-1] // remove * //多個 類目錄對象 compositeEntry := []Entry{} walkFn := func(path string, info os.FileInfo, err error) error { if err != nil { return err } //如果為空 if info.IsDir() && path != baseDir { return filepath.SkipDir } //如果是 .jar 或者 .JAR 結(jié)尾的文件 if strings.HasSuffix(path, ".jar") || strings.HasSuffix(path, ".JAR") { jarEntry := newZipEntry(path) compositeEntry = append(compositeEntry, jarEntry) } return nil } //遍歷 目錄下所有 .jar .JAR 文件 生成ZipEntry目錄對象 放在切片中返回 //walFn為函數(shù) filepath.Walk(baseDir, walkFn) return compositeEntry }
3.4實現(xiàn)類目錄
前面提到了java 虛擬機
默認 會先從啟動路徑--->擴展類路徑 --->用戶類路徑
按順序依次去尋找,加載類。
那么就會有3個類目錄對象,所以就要定義一個結(jié)構(gòu)體去存放它。
type Classpath struct { BootClasspath Entry ExtClasspath Entry UserClasspath Entry }
啟動類路徑
啟動路徑,其實對應(yīng)Jre
目錄下``lib` 也就是運行java 程序必須可少的基本運行庫。
通過 -Xjre
指定 如果不指定 會在當前路徑下尋找jre 如果找不到 就會從我們在裝java是配置的JAVA_HOME
環(huán)境變量 中去尋找。
所以獲取驗證環(huán)境變量的方法如下:
func getJreDir(jreOption string) string { //如果 從cmd -Xjre 獲取到目錄 并且存在 if jreOption != "" && exists(jreOption) { //返回目錄 return jreOption } //如果 當前路徑下 有 jre 返回目錄 if exists("./jre") { return "./jre" } //如果 上面都找不到 到系統(tǒng)環(huán)境 變量中尋找 if jh := os.Getenv("JAVA_HOME"); jh != "" { //存在 就返回 return filepath.Join(jh, "jre") } //都找不到 就報錯 panic("Can not find jre folder!") } //判斷 目錄是否存在 func exists(path string) bool { if _, err := os.Stat(path); err != nil { if os.IsNotExist(err) { return false } } return true }
擴展類路徑
擴展類 路徑一般 在啟動路徑 的子目錄下jre/lib/ext
func (self *Classpath) parseBootAndExtClasspath(jreOption string) { jreDir := getJreDir(jreOption) // 拼接成jre 的路徑 // jre/lib/* jreLibPath := filepath.Join(jreDir, "lib", "*") //加載 所有底下的 jar包 self.BootClasspath = newWildcardEntry(jreLibPath) // 拼接 擴展類 的路徑 // jre/lib/ext/* jreExtPath := filepath.Join(jreDir, "lib", "ext", "*") //加載 所有底下的jar包 self.ExtClasspath = newWildcardEntry(jreExtPath) }
用戶類路徑
用戶類路徑通過前面提到的 -classpath 來指定 ,如果沒有指定 就默認為當前路徑就好
func (self *Classpath) parseUserClasspath(cpOption string) { //如果沒有指定 if cpOption == "" { // . 作為當前路徑 cpOption = "." } //創(chuàng)建 類目錄對象 self.UserClasspath = newEntry(cpOption) }
實現(xiàn)類的加載
對于指定文件類名取查找 我們是按前面提到的(啟動路徑--->擴展類路徑 --->用戶類路徑
按順序依次去尋找,加載類),沒找到就挨個查找下去。
如果用戶沒有提供-classpath/-cp
選項,則使用當前目錄作為用 戶類路徑。ReadClass()
方法依次從啟動類路徑、擴展類路徑和用戶 類路徑中搜索class文件,
//根據(jù)類名 分別從 bootClasspath,extClasspath,userClasspath 依次加載類目錄 func (self *Classpath) ReadClass(className string) ([]byte, ClassDirEntry, error) { className = className + ".class" if data, entry, err := self.BootClasspath.readClass(className); err == nil{ return data, entry, err } if data, entry, err := self.ExtClasspath.readClass(className); err == nil { return data, entry, err } return self.UserClasspath.readClass(className) }
初始化類加載目錄
定義一個初始化函數(shù),來作為初始函數(shù),執(zhí)行后生成一個 Classpath對象。
//jreOption 為啟動類目錄 cpOption 為 用戶指定類目錄 從cmd 命令行 中解析獲取 func InitClassPath(jreOption, cpOption string) *Classpath { cp := &Classpath{} //初始化 啟動類目錄 cp.parseBootAndExtClasspath(jreOption) //初始化 用戶類目錄 cp.parseUserClasspath(cpOption) return cp }
注意,傳遞給ReadClass()方法的類名不包含“.class”后綴。
3.5總結(jié)
3.6測試
成功獲取到class文件!
四、解析Class文件
4.1 class文件介紹
作為類/接口信息的載體,每一個class文件
都完整的定義了一個類,為了使Java程序可以實現(xiàn)“編寫一次,處處運行”,java虛擬機對class文件的格式進行了嚴格的規(guī)范。
但是對于從哪里加載class文件
,給予了高度自由空間:第三節(jié)中說過,可以從文件系統(tǒng)讀取jar/zip文件
中的class文件
,除此之外,也可以從網(wǎng)絡(luò)下載,甚至是直接在運行中生成class文件
。
構(gòu)成class文件
的基本數(shù)據(jù)單位是字節(jié),可以把整個class文件當 成一個字節(jié)流來處理。稍大一些的數(shù)據(jù)由連續(xù)多個字節(jié)構(gòu)成,這些數(shù)據(jù)在class文件中以大端(big-endian)
方式存儲。
為了描述class文件格式,Java虛擬機規(guī)范定義了u1
、u2
和u4
三種數(shù)據(jù)類型來表示1、 2和4字節(jié)無符號整數(shù),分別對應(yīng)Go語言的uint8
、uint16
和uint32
類型。
相同類型的多條數(shù)據(jù)一般按表(table)
的形式存儲在class文件中。表由表頭
和表項(item)
構(gòu)成,表頭是u2或u4整數(shù)。假設(shè)表頭是 n,后面就緊跟著n個表項數(shù)據(jù)。
Java虛擬機規(guī)范使用一種類似C語言的結(jié)構(gòu)體語法來描述class 文件格式。整個class文件被描述為一個ClassFile結(jié)構(gòu),代碼如下:
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]; }
!表示大小不定。
4.2解析class文件
Go語言內(nèi)置了豐富的數(shù)據(jù)類型,非常適合處理class文件。
如下為Go和Java語言基本數(shù)據(jù)類型對照關(guān)系:
Go語言類型 | Java語言類型 | 說明 |
---|---|---|
int8 | byte | 8比特有符號整數(shù) |
uint8(別名byte) | N/A | 8比特無符號整數(shù) |
int16 | short | 16比特有符號整數(shù) |
uint16 | char | 16比特無符號整數(shù) |
int32(別名rune) | int | 32比特有符號整數(shù) |
uint32 | N/A | 32比特無符號整數(shù) |
int64 | long | 64比特有符號整數(shù) |
uint64 | N/A | 64比特無符號整數(shù) |
float32 | float | 32比特IEEE-754浮點數(shù) |
float64 | double | 64比特IEEE-754浮點數(shù) |
4.2.1讀取數(shù)據(jù)
解析class文件
的第一步是從里面讀取數(shù)據(jù)。雖然可以把class文件
當成字節(jié)流來處理,但是直接操作字節(jié)很不方便,所以先定義一個結(jié)構(gòu)體ClassReader
來幫助讀取數(shù)據(jù),創(chuàng)建class_reader.go。
package classfile import "encoding/binary" type ClassReader struct { data []byte } func (self *ClassReader) readUint8() uint8 {...} // u1 func (self *ClassReader) readUint16() uint16 {...} // u2 func (self *ClassReader) readUint32() uint32 {...} // u4 func (self *ClassReader) readUint64() uint64 {...} func (self *ClassReader) readUint16s() []uint16 {...} func (self *ClassReader) readBytes(length uint32) []byte {...}
ClassReader
只是[]byte
類型的包裝而已。readUint8()
讀取u1
類型數(shù)據(jù)。
ClassReader
并沒有使用索引記錄數(shù)據(jù)位置,而是使用Go 語言的reslice語法
跳過已經(jīng)讀取的數(shù)據(jù)
實現(xiàn)代碼如下:
// u1 func (self *ClassReader) readUint8() uint8 { val := self.data[0] self.data = self.data[1:] return val } // u2 func (self *ClassReader) readUint16() uint16 { val := binary.BigEndian.Uint16(self.data) self.data = self.data[2:] return val } // u4 func (self *ClassReader) readUint32() uint32 { val := binary.BigEndian.Uint32(self.data) self.data = self.data[4:] return val } func (self *ClassReader) readUint64() uint64 { val := binary.BigEndian.Uint64(self.data) self.data = self.data[8:] return val } func (self *ClassReader) readUint16s() []uint16 { n := self.readUint16() s := make([]uint16, n) for i := range s { s[i] = self.readUint16() } return s } func (self *ClassReader) readBytes(n uint32) []byte { bytes := self.data[:n] self.data = self.data[n:] return bytes }
Go標準庫encoding/binary包中定義了一個變量BigEndian
,可以從[]byte
中解碼多字節(jié)數(shù)據(jù)。
4.2.2解析整體結(jié)構(gòu)
有了ClassReader,可以開始解析class文件了。創(chuàng)建class_file.go文件,在其中定義ClassFile結(jié)構(gòu)體
,與4.1中的class文件中字段對應(yīng)。
package classfile import "fmt" type ClassFile struct { //magic uint32 minorVersion uint16 majorVersion uint16 constantPool ConstantPool accessFlags uint16 thisClass uint16 superClass uint16 interfaces []uint16 fields []*MemberInfo methods []*MemberInfo attributes []AttributeInfo }
在class_file.go文件中實現(xiàn)一系列函數(shù)和方法。
func Parse(classData []byte) (cf *ClassFile, err error) {...} func (self *ClassFile) read(reader *ClassReader) {...} func (self *ClassFile) readAndCheckMagic(reader *ClassReader) {...} func (self *ClassFile) readAndCheckVersion(reader *ClassReader) {...} func (self *ClassFile) MinorVersion() uint16 {...} // getter func (self *ClassFile) MajorVersion() uint16 {...} // getter func (self *ClassFile) ConstantPool() ConstantPool {...} // getter func (self *ClassFile) AccessFlags() uint16 {...} // getter func (self *ClassFile) Fields() []*MemberInfo {...} // getter func (self *ClassFile) Methods() []*MemberInfo {...} // getter func (self *ClassFile) ClassName() string {...} func (self *ClassFile) SuperClassName() string {...} func (self *ClassFile) InterfaceNames() []string {...}
相比Java語言,Go的訪問控制非常簡單:只有公開和私有兩種。
所有首字母大寫的類型、結(jié)構(gòu)體、字段、變量、函數(shù)、方法等都是公開的,可供其他包使用。
首字母小寫則是私有的,只能在包內(nèi)部使用。
解析[]byte
Parse()函數(shù)把[]byte解析成ClassFile結(jié)構(gòu)體。
func Parse(classData []byte) (cf *ClassFile, err error) { defer func() { //嘗試捕獲 panic,并將其存儲在變量 r 中。如果沒有發(fā)生 panic,r 將為 nil。 if r := recover(); r != nil { var ok bool //判斷 r 是否是一個 error 類型 err, ok = r.(error) if !ok { err = fmt.Errorf("%v", r) } } }() cr := &ClassReader{classData} cf = &ClassFile{} cf.read(cr) return }
順序解析
read() 方法依次調(diào)用其他方法解析class文件,順序一定要保證正確,與class文件相對應(yīng)。
func (self *ClassFile) read(reader *ClassReader) { //讀取并檢查類文件的魔數(shù)。 self.readAndCheckMagic(reader) //讀取并檢查類文件的版本號。 self.readAndCheckVersion(reader) //解析常量池,常量池類還沒寫 self.constantPool = readConstantPool(reader) //讀取類的訪問標志 self.accessFlags = reader.readUint16() //讀取指向當前類在常量池中的索引 self.thisClass = reader.readUint16() //父類在常量池中的索引 self.superClass = reader.readUint16() //讀取接口表的數(shù)據(jù) self.interfaces = reader.readUint16s() //讀取類的字段信息 self.fields = readMembers(reader, self.constantPool) //讀取類的方法信息 self.methods = readMembers(reader, self.constantPool) //讀取類的屬性信息(類級別的注解、源碼文件等) self.attributes = readAttributes(reader, self.constantPool) }
self.readAndCheckMagic(reader)
: 這是一個ClassFile
結(jié)構(gòu)的方法,用于讀取并檢查類文件的魔數(shù)。魔數(shù)是類文件的標識符,用于確定文件是否為有效的類文件。self.readAndCheckVersion(reader)
: 這個方法用于讀取并檢查類文件的版本號。Java類文件具有版本號,標識了它們的Java編譯器版本。這里會對版本號進行檢查。self.constantPool = readConstantPool(reader)
: 這一行代碼調(diào)用readConstantPool
函數(shù)來讀取常量池部分的數(shù)據(jù),并將其存儲在ClassFile
結(jié)構(gòu)的constantPool
字段中。常量池是一個包含各種常量信息的表格,用于支持類文件中的各種符號引用。self.accessFlags = reader.readUint16()
: 這一行代碼讀取類的訪問標志,它標識類的訪問權(quán)限,例如public
、private
等。self.thisClass = reader.readUint16()
: 這行代碼讀取指向當前類在常量池中的索引,表示當前類的類名。self.superClass = reader.readUint16()
: 這行代碼讀取指向父類在常量池中的索引,表示當前類的父類名。self.interfaces = reader.readUint16s()
: 這行代碼讀取接口表的數(shù)據(jù),表示當前類實現(xiàn)的接口。self.fields = readMembers(reader, self.constantPool)
: 這行代碼調(diào)用readMembers
函數(shù),以讀取類的字段信息,并將它們存儲在fields
字段中。字段包括類的成員變量。self.methods = readMembers(reader, self.constantPool)
: 這行代碼類似于上一行,但它讀取類的方法信息,并將它們存儲在methods
字段中。self.attributes = readAttributes(reader, self.constantPool)
: 最后,這行代碼調(diào)用readAttributes
函數(shù),以讀取類的屬性信息,并將它們存儲在attributes
字段中。屬性包括類級別的注解、源碼文件等信息。
以下均為類似于Java的getter方法,以后將不再贅述。
func (self *ClassFile) MinorVersion() uint16 { return self.minorVersion } func (self *ClassFile) MajorVersion() uint16 { return self.majorVersion } func (self *ClassFile) ConstantPool() ConstantPool { return self.constantPool } func (self *ClassFile) AccessFlags() uint16 { return self.accessFlags } func (self *ClassFile) Fields() []*MemberInfo { return self.fields } func (self *ClassFile) Methods() []*MemberInfo { return self.methods }
ClassName從常量池中獲取,SuperClassName同理,常量池還未實現(xiàn)。
所有類的超類(父類),Object是java中唯一沒有父類的類,一個類可以不是Object的直接子類,但一定是繼承于Object并拓展于Object。
func (self *ClassFile) ClassName() string { return self.constantPool.getClassName(self.thisClass) } func (self *ClassFile) SuperClassName() string { if self.superClass > 0 { return self.constantPool.getClassName(self.superClass) } //Object類 return "" }
Java的類是單繼承,多實現(xiàn)的,因此獲取接口應(yīng)該使用循環(huán),也從常量池中獲取。
func (self *ClassFile) InterfaceNames() []string { interfaceNames := make([]string, len(self.interfaces)) for i, cpIndex := range self.interfaces { interfaceNames[i] = self.constantPool.getClassName(cpIndex) } return interfaceNames }
解析魔數(shù)
很多文件格式都會規(guī)定滿足該格式的文件必須以某幾個固定字節(jié)開頭,這幾個字節(jié)主要起標識作用,叫作魔數(shù)(magic number)
。
- PDF文件以4字節(jié)“%PDF”(0x25、0x50、0x44、0x46)開頭
- ZIP 文件以2字節(jié)“PK”(0x50、0x4B)開頭
- class文件的魔數(shù) 是“0xCAFEBABE” 。
因此readAndCheckMagic()方法的代碼如下。
func (self *ClassFile) readAndCheckMagic(reader *ClassReader) { magic := reader.readUint32() if magic != 0xCAFEBABE { panic("java.lang.ClassFormatError: magic!") } }
Java虛擬機規(guī)范規(guī)定,如果加載的class文件不符合要求的格式,Java虛擬機實現(xiàn)就拋出java.lang.ClassFormatError異常。
但是因為我們才剛剛開始編寫虛擬機,還無法拋出異常,所以暫時先調(diào)用 panic()方法終止程序執(zhí)行。
版本號
解析版本號
魔數(shù)之后是class文件的次版本號和主版本號,都是u2類型
。
假設(shè)某class文件的主版本號是M,次版本號是m,那么完整的版本號 可以表示成M.m
的形式。
次版本號只在J2SE 1.2
之前用過,從1.2 開始基本上就沒什么用了(都是0)。
主版本號在J2SE 1.2之前是45, 從1.2開始,每次有大的Java版本發(fā)布,都會加1。
Java 版本 | 類文件版本號 |
---|---|
Java 1.1 | 45.3 |
Java 1.2 | 46.0 |
Java 1.3 | 47.0 |
Java 1.4 | 48.0 |
Java 5 | 49.0 |
Java 6 | 50.0 |
Java 7 | 51.0 |
Java 8 | 52.0 |
特定的Java虛擬機實現(xiàn)只能支持版本號在某個范圍內(nèi)的class文 件。
Oracle的實現(xiàn)是完全向后兼容的,比如Java SE 8
支持版本號為 45.0~52.0的class文件。
如果版本號不在支持的范圍內(nèi),Java虛擬機 實現(xiàn)就拋出java.lang.UnsupportedClassVersionError
異常。參考 Java 8,支持版本號為45.0~52.0的class文件。如果遇到其他版本號, 調(diào)用panic()
方法終止程序執(zhí)行。
如下為檢查版本號代碼:
func (self *ClassFile) readAndCheckVersion(reader *ClassReader) { self.minorVersion = reader.readUint16() self.majorVersion = reader.readUint16() switch self.majorVersion { case 45: return case 46, 47, 48, 49, 50, 51, 52: if self.minorVersion == 0 { return } } panic("java.lang.UnsupportedClassVersionError!") }
解析類訪問標識
版本號之后是常量池,但是由于常量池比較復雜,所以放到4.3 節(jié)介紹。
常量池之后是類訪問標志,這是一個16位的bitmask
,指出class文件定義的是類還是接口,訪問級別是public
還是private
,等等。
本章只對class文件進行初步解析,并不做完整驗證,所以只是讀取類訪問標志以備后用。
ClassFileTest的類訪問標志為:0X21
:
解析類和父類索引
類訪問標志之后是兩個u2類型的常量池索引,分別給出類名和超類名。
class文件存儲的類名類似完全限定名,但是把點換成了 斜線,Java語言規(guī)范把這種名字叫作二進制名binary names
。
因為每個類都有名字,所以thisClass
必須是有效的常量池索引。
除 java.lang.Object
之外,其他類都有超類,所以superClass
只在 Object.class
中是0,在其他class文件中必須是有效的常量池索引。如下,ClassFileTest的類索引是5,超類索引是6。
解析接口索引表
類和超類索引后面是接口索引表,表中存放的也是常量池索引,給出該類實現(xiàn)的所有接口的名字。ClassFileTest沒有實現(xiàn)接口, 所以接口表是空的
解析字段和方法表
接口索引表之后是字段表和方法表,分別存儲字段和方法信息。
字段和方法的基本結(jié)構(gòu)大致相同,差別僅在于屬性表。
下面是 Java虛擬機規(guī)范給出的字段結(jié)構(gòu)定義
field_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }
和類一樣,字段和方法也有自己的訪問標志。訪問標志之后是一個常量池索引,給出字段名或方法名,然后又是一個常量池索引,給出字段或方法的描述符,最后是屬性表。
為了避免重復代 碼,用一個結(jié)構(gòu)體統(tǒng)一表示字段和方法。
package classfile type MemberInfo struct { cp ConstantPool accessFlags uint16 nameIndex uint16 descriptorIndex uint16 attributes []AttributeInfo } func readMembers(reader *ClassReader, cp ConstantPool) []*MemberInfo {...} func readMember(reader *ClassReader, cp ConstantPool) *MemberInfo {...} func (self *MemberInfo) AccessFlags() uint16 {...} // getter func (self *MemberInfo) Name() string {...} func (self *MemberInfo) Descriptor() string {...}
cp字段保存常量池指針,后面會用到它。readMembers()讀取字段表或方法表,代碼如下:
func readMembers(reader *ClassReader, cp ConstantPool) []*MemberInfo { memberCount := reader.readUint16() members := make([]*MemberInfo, memberCount) for i := range members { members[i] = readMember(reader, cp) } return members }
readMember()函數(shù)讀取字段或方法數(shù)據(jù)。
func readMember(reader *ClassReader, cp ConstantPool) *MemberInfo { return &MemberInfo{ cp: cp, accessFlags: reader.readUint16(), nameIndex: reader.readUint16(), descriptorIndex: reader.readUint16(), attributes: readAttributes(reader, cp), } }
Name()從常量 池查找字段或方法名,Descriptor()從常量池查找字段或方法描述 符
func (self *MemberInfo) Name() string { return self.cp.getUtf8(self.nameIndex) } func (self *MemberInfo) Descriptor() string { return self.cp.getUtf8(self.descriptorIndex) }
4.2.3解析常量池
常量池占據(jù)了class文件很大一部分數(shù)據(jù),里面存放著各式各樣的常量信息,包括數(shù)字和字符串常量、類和接口名、字段和方法名,等等
創(chuàng)建constant_pool.go
文件,里面定義 ConstantPool類型
。
package classfile type ConstantPool []ConstantInfo func readConstantPool(reader *ClassReader) ConstantPool {...} func (self ConstantPool) getConstantInfo(index uint16) ConstantInfo {...} func (self ConstantPool) getNameAndType(index uint16) (string, string) {...} func (self ConstantPool) getClassName(index uint16) string {...} func (self ConstantPool) getUtf8(index uint16) string {...}
常量池實際上也是一個表,但是有三點需要特別注意。
表頭給出的常量池大小比實際大1。假設(shè)表頭給出的值是n,那么常量池的實際大小是n–1。
有效的常量池索引是1~n–1。0是無效索引,表示不指向任何常量。
CONSTANT_Long_info
和CONSTANT_Double_info
各占兩個位置。也就是說,如果常量池中存在這兩種常量,實際的常量數(shù)量比n–1還要少,而且1~n–1的某些 數(shù)也會變成無效索引。
常量池由readConstantPool()
函數(shù)讀取,代碼如下:
func readConstantPool(reader *ClassReader) ConstantPool { cpCount := int(reader.readUint16()) cp := make([]ConstantInfo, cpCount) // 索引從1開始 for i := 1; i < cpCount; i++ { cp[i] = readConstantInfo(reader, cp) switch cp[i].(type) { //占兩個位置 case *ConstantLongInfo, *ConstantDoubleInfo: i++ } } return cp }
getConstantInfo()
方法按索引查找常量
func (self ConstantPool) getConstantInfo(index uint16) ConstantInfo { if cpInfo := self[index]; cpInfo != nil { return cpInfo } panic(fmt.Errorf("Invalid constant pool index: %v!", index)) }
getNameAndType()
方法從常量池查找字段或方法的名字和描述符
func (self ConstantPool) getNameAndType(index uint16) (string, string) { ntInfo := self.getConstantInfo(index).(*ConstantNameAndTypeInfo) name := self.getUtf8(ntInfo.nameIndex) _type := self.getUtf8(ntInfo.descriptorIndex) return name, _type }
getClassName()
方法從常量池查找類名
func (self ConstantPool) getClassName(index uint16) string { classInfo := self.getConstantInfo(index).(*ConstantClassInfo) return self.getUtf8(classInfo.nameIndex) }
getUtf8()
方法從常量池查找UTF-8字符串
func (self ConstantPool) getUtf8(index uint16) string { utf8Info := self.getConstantInfo(index).(*ConstantUtf8Info) return utf8Info.str }
ConstPool接口
由于常量池中存放的信息各不相同,所以每種常量的格式也不同。
常量數(shù)據(jù)的第一字節(jié)是tag,用來區(qū)分常量類型。
下面是Java 虛擬機規(guī)范給出的常量結(jié)構(gòu)
cp_info { u1 tag; u1 info[]; }
Java虛擬機規(guī)范一共定義了14種常量。創(chuàng)建constant_info.go
文件,在其中定義tag常量值,代碼如下:
package classfile // Constant pool tags const ( CONSTANT_Class = 7 CONSTANT_Fieldref = 9 CONSTANT_Methodref = 10 CONSTANT_InterfaceMethodref = 11 CONSTANT_String = 8 CONSTANT_Integer = 3 CONSTANT_Float = 4 CONSTANT_Long = 5 CONSTANT_Double = 6 CONSTANT_NameAndType = 12 CONSTANT_Utf8 = 1 CONSTANT_MethodHandle = 15 CONSTANT_MethodType = 16 CONSTANT_InvokeDynamic = 18 )
定義ConstantInfo接口來表示常量信息
type ConstantInfo interface { readInfo(reader *ClassReader) } //讀取常量信息 func readConstantInfo(reader *ClassReader, cp ConstantPool) ConstantInfo {...} func newConstantInfo(tag uint8, cp ConstantPool) ConstantInfo {...}
readInfo()方法讀取常量信息,需要由具體的常量結(jié)構(gòu)體實現(xiàn)。 readConstantInfo()函數(shù)先讀出tag值,然后調(diào)用newConstantInfo()函數(shù)創(chuàng)建具體的常量,最后調(diào)用常量的readInfo()方法讀取常量信息, 代碼如下:
func readConstantInfo(reader *ClassReader, cp ConstantPool) ConstantInfo { tag := reader.readUint8() c := newConstantInfo(tag, cp) c.readInfo(reader) return c }
newConstantInfo()根據(jù)tag值創(chuàng)建具體的常量,代碼如下:
func newConstantInfo(tag uint8, cp ConstantPool) ConstantInfo { switch tag { case CONSTANT_Integer: return &ConstantIntegerInfo{} case CONSTANT_Float: return &ConstantFloatInfo{} case CONSTANT_Long: return &ConstantLongInfo{} case CONSTANT_Double: return &ConstantDoubleInfo{} case CONSTANT_Utf8: return &ConstantUtf8Info{} case CONSTANT_String: return &ConstantStringInfo{cp: cp} case CONSTANT_Class: return &ConstantClassInfo{cp: cp} case CONSTANT_Fieldref: return &ConstantFieldrefInfo{ConstantMemberrefInfo{cp: cp}} case CONSTANT_Methodref: return &ConstantMethodrefInfo{ConstantMemberrefInfo{cp: cp}} case CONSTANT_InterfaceMethodref: return &ConstantInterfaceMethodrefInfo{ConstantMemberrefInfo{cp: cp}} case CONSTANT_NameAndType: return &ConstantNameAndTypeInfo{} case CONSTANT_MethodType: return &ConstantMethodTypeInfo{} case CONSTANT_MethodHandle: return &ConstantMethodHandleInfo{} case CONSTANT_InvokeDynamic: return &ConstantInvokeDynamicInfo{} default: panic("java.lang.ClassFormatError: constant pool tag!") } }
CONSTANT_Integer_info
CONSTANT_Integer_info
使用4字節(jié)存儲整數(shù)常量,其JVM結(jié)構(gòu)定義如下:
CONSTANT_Integer_info { u1 tag; u4 bytes; }
CONSTANT_Integer_info
和后面將要介紹的其他三種數(shù)字常量無論是結(jié)構(gòu),還是實現(xiàn),都非常相似,所以把它們定義在同一個文件中。創(chuàng)建cp_numeric.go
文件,在其中定義 ConstantIntegerInfo結(jié)構(gòu)體
,代碼如下:
package classfile import "math" type ConstantIntegerInfo struct { val int32 } func (self *ConstantIntegerInfo) readInfo(reader *ClassReader) {...}
readInfo()先讀取一個uint32
數(shù)據(jù),然后把它轉(zhuǎn)型成int32
類型, 代碼如下
func (self *ConstantIntegerInfo) readInfo(reader *ClassReader) { bytes := reader.readUint32() self.val = int32(bytes) }
CONSTANT_Float_info
CONSTANT_Float_info
使用4字節(jié)存儲IEEE754單精度浮點數(shù)
常量,JVM結(jié)構(gòu)如下:
CONSTANT_Float_info { u1 tag; u4 bytes; }
在cp_numeric.go
文件中定義ConstantFloatInfo結(jié)構(gòu)體
,代碼如下:
type ConstantFloatInfo struct { val float32 } func (self *ConstantFloatInfo) readInfo(reader *ClassReader) { bytes := reader.readUint32() self.val = math.Float32frombits(bytes) }
CONSTANT_Long_info
CONSTANT_Long_info
使用8字節(jié)存儲整數(shù)常量,結(jié)構(gòu)如下:
CONSTANT_Long_info { u1 tag; u4 high_bytes; u4 low_bytes; }
在cp_numeric.go
文件中定義ConstantLongInfo結(jié)構(gòu)體
,代碼如下:
type ConstantLongInfo struct { val int64 } func (self *ConstantLongInfo) readInfo(reader *ClassReader) { bytes := reader.readUint64() self.val = int64(bytes) }
CONSTANT_Double_info
最后一個數(shù)字常量是CONSTANT_Double_info
,使用8字節(jié)存儲IEEE754雙精度浮點數(shù)
,結(jié)構(gòu)如下:
CONSTANT_Double_info { u1 tag; u4 high_bytes; u4 low_bytes; }
在cp_numeric.go
文件中定義ConstantDoubleInfo
結(jié)構(gòu)體,代碼如下:
type ConstantDoubleInfo struct { val float64 } func (self *ConstantDoubleInfo) readInfo(reader *ClassReader) { bytes := reader.readUint64() self.val = math.Float64frombits(bytes) }
CONSTANT_Utf8_info
CONSTANT_Utf8_info常量里放的是MUTF-8編碼的字符串, 結(jié)構(gòu)如下:
CONSTANT_Utf8_info { u1 tag; u2 length; u1 bytes[length]; }
Java類文件中使用MUTF-8(Modified UTF-8)編碼而不是標準的UTF-8,是因為MUTF-8在某些方面更適合于在Java虛擬機內(nèi)部處理字符串。以下是一些原因:
空字符的表示: 在標準的UTF-8編碼中,空字符(U+0000)會使用單個字節(jié)0x00表示,這與C字符串中的字符串終止符相同,可能引起混淆。在MUTF-8中,空字符會使用0xC0 0x80來表示,避免了混淆。
編碼長度: MUTF-8編碼中的每個字符都使用1至3個字節(jié)來表示,這與UTF-8編碼相比更緊湊。對于大多數(shù)常見的字符集,這可以減少存儲和傳輸開銷。
字符的編碼范圍: MUTF-8編碼對字符的范圍進行了限制,只包含Unicode BMP(基本多文種平面)范圍內(nèi)的字符。這些字符通常足夠用于表示Java標識符和字符串文字。
兼容性: 早期版本的Java使用的是MUTF-8編碼,因此為了保持與早期版本的兼容性,后續(xù)版本也繼續(xù)使用MUTF-8。這有助于確保Java類文件的可互操作性。
創(chuàng)建cp_utf8.go
文件,在其中定義 ConstantUtf8Info結(jié)構(gòu)體
,代碼如下:
type ConstantUtf8Info struct { str string } func (self *ConstantUtf8Info) readInfo(reader *ClassReader) { length := uint32(reader.readUint16()) bytes := reader.readBytes(length) self.str = decodeMUTF8(bytes) }
Java序列化機制也使用了MUTF-8編碼。
java.io.DataInput和 java.io.DataOutput接口分別定義了readUTF()
和writeUTF()
方法,可以讀寫MUTF-8編碼的字符串。
如下為簡化版的java.io.DataInputStream.readUTF()
// mutf8 -> utf16 -> utf32 -> string func decodeMUTF8(bytearr []byte) string { utflen := len(bytearr) chararr := make([]uint16, utflen) var c, char2, char3 uint16 count := 0 chararr_count := 0 for count < utflen { c = uint16(bytearr[count]) if c > 127 { break } count++ chararr[chararr_count] = c chararr_count++ } for count < utflen { c = uint16(bytearr[count]) switch c >> 4 { case 0, 1, 2, 3, 4, 5, 6, 7: /* 0xxxxxxx*/ count++ chararr[chararr_count] = c chararr_count++ case 12, 13: /* 110x xxxx 10xx xxxx*/ count += 2 if count > utflen { panic("malformed input: partial character at end") } char2 = uint16(bytearr[count-1]) if char2&0xC0 != 0x80 { panic(fmt.Errorf("malformed input around byte %v", count)) } chararr[chararr_count] = c&0x1F<<6 | char2&0x3F chararr_count++ case 14: /* 1110 xxxx 10xx xxxx 10xx xxxx*/ count += 3 if count > utflen { panic("malformed input: partial character at end") } char2 = uint16(bytearr[count-2]) char3 = uint16(bytearr[count-1]) if char2&0xC0 != 0x80 || char3&0xC0 != 0x80 { panic(fmt.Errorf("malformed input around byte %v", (count - 1))) } chararr[chararr_count] = c&0x0F<<12 | char2&0x3F<<6 | char3&0x3F<<0 chararr_count++ default: /* 10xx xxxx, 1111 xxxx */ panic(fmt.Errorf("malformed input around byte %v", count)) } } // The number of chars produced may be less than utflen chararr = chararr[0:chararr_count] runes := utf16.Decode(chararr) return string(runes) }
- 初始化
chararr
數(shù)組,用于存儲UTF-16字符。 - 遍歷MUTF-8字節(jié)數(shù)組中的字節(jié),根據(jù)字節(jié)的值來判斷字符的編碼方式。
- 如果字節(jié)值小于128,表示ASCII字符,直接轉(zhuǎn)換為UTF-16并存儲。
- 如果字節(jié)值在特定范圍內(nèi),表示多字節(jié)字符,需要根據(jù)UTF-8編碼規(guī)則進行解碼。
- 如果遇到不符合規(guī)則的字節(jié),拋出異常來處理錯誤情況。
- 最后,將解碼后的UTF-16字符轉(zhuǎn)換為Go字符串。
CONSTANT_String_info
CONSTANT_String_info
常量表示java.lang.String
字面量,結(jié)構(gòu)如下:
CONSTANT_String_info { u1 tag; u2 string_index; }
可以看到,CONSTANT_String_info
本身并不存放字符串數(shù)據(jù)
只存了常量池索引
,這個索引指向一個CONSTANT_Utf8_info常量
。
下創(chuàng)建cp_string.go
文件,在其中定義 ConstantStringInfo結(jié)構(gòu)體
type ConstantStringInfo struct { cp ConstantPool stringIndex uint16 } func (self *ConstantStringInfo) readInfo(reader *ClassReader) { self.stringIndex = reader.readUint16() }
String()
方法按索引從常量池中查找字符串:
func (self *ConstantStringInfo) String() string { return self.cp.getUtf8(self.stringIndex) }
CONSTANT_Class_info
CONSTANT_Class_info
常量表示類或者接口的符號引用
他是對類或者接口的符號引用。它描述的可以是當前類型的信息,也可以描述對當前類的引用,還可以描述對其他類的引用。JVM結(jié)構(gòu)如下:
CONSTANT_Class_info { u1 tag; u2 name_index; }
和CONSTANT_String_info
類似,name_index
是常量池索引,指向CONSTANT_Utf8_info
常量。
創(chuàng)建 cp_class.go
文件,定義ConstantClassInfo結(jié)構(gòu)體
type ConstantClassInfo struct { cp ConstantPool nameIndex uint16 } func (self *ConstantClassInfo) readInfo(reader *ClassReader) { self.nameIndex = reader.readUint16() } func (self *ConstantClassInfo) Name() string { return self.cp.getUtf8(self.nameIndex) }
CONSTANT_NameAndType_info
CONSTANT_NameAndType_info
給出字段或方法的名稱和描述符。
CONSTANT_Class_info
和CONSTANT_NameAndType_info
加在 一起可以唯一確定一個字段或者方法。其結(jié)構(gòu)如下:
CONSTANT_NameAndType_info { u1 tag; u2 name_index; u2 descriptor_index; }
字段或方法名由name_index
給出,字段或方法的描述符由descriptor_index
給出。
name_index
和descriptor_index
都是常量池索引,指向CONSTANT_Utf8_info常量
。
Java虛擬機規(guī)范定義了一種簡單的語法來描述字段和方法,可以根據(jù)下面的規(guī)則生成描述符。
一、類型描述符
- 基本類型byte、short、char、int、long、float和double的描述符是單個字母,分別對應(yīng)B、S、C、I、J、F和D。注意,long的描述符是J 而不是L。
- 引用類型的描述符是L+類的完全限定名+分號。
- 數(shù)組類型的描述符是[+數(shù)組元素類型描述符
二、字段描述符
? 字段類型的描述符
三、方法描述符
? 分號分隔的參數(shù)類型描述符+返回值類型描述符,其中void返回值由單個字母V表示。
Java語言支持方法重載(override),不同的方法可 以有相同的名字,只要參數(shù)列表不同即可。
這就是為什么 CONSTANT_NameAndType_info結(jié)構(gòu)要同時包含名稱和描述符的原因。
創(chuàng)建cp_name_and_type.go
文件,在其中定義ConstantName-AndTypeInfo結(jié)構(gòu)體
,代碼如下:
type ConstantNameAndTypeInfo struct { nameIndex uint16 descriptorIndex uint16 } func (self *ConstantNameAndTypeInfo) readInfo(reader *ClassReader) { self.nameIndex = reader.readUint16() self.descriptorIndex = reader.readUint16() }
CONSTANT_Fieldref_info、 CONSTANT_Methodref_info和 CONSTANT_InterfaceMethodref_info
CONSTANT_Fieldref_info
表示字段符號引用,CONSTANT_Methodref_info
表示普通(非接口)方法符號引用, CONSTANT_InterfaceMethodref_info
表示接口方法符號引用。這三種常量結(jié)構(gòu)一模一樣。
其中CONSTANT_Fieldref_info
的結(jié)構(gòu)如下:
CONSTANT_Fieldref_info { u1 tag; u2 class_index; u2 name_and_type_index; }
class_index
和name_and_type_index
都是常量池索引,分別指向CONSTANT_Class_info
和CONSTANT_NameAndType_info
常量。
創(chuàng)建cp_member_ref.go
文件,定義一個統(tǒng)一的結(jié)構(gòu)體ConstantMemberrefInfo
來表示這3種常量,然后定義三個結(jié)構(gòu)體“繼承”ConstantMemberrefInfo
。
Go語言并沒有“繼承”這個概念,但是可以通過結(jié)構(gòu)體嵌套來模擬
type ConstantFieldrefInfo struct{ ConstantMemberrefInfo } type ConstantMethodrefInfo struct{ ConstantMemberrefInfo } type ConstantInterfaceMethodrefInfo struct{ ConstantMemberrefInfo } type ConstantMemberrefInfo struct { cp ConstantPool classIndex uint16 nameAndTypeIndex uint16 } func (self *ConstantMemberrefInfo) readInfo(reader *ClassReader) { self.classIndex = reader.readUint16() self.nameAndTypeIndex = reader.readUint16() } func (self *ConstantMemberrefInfo) ClassName() string { return self.cp.getClassName(self.classIndex) } func (self *ConstantMemberrefInfo) NameAndDescriptor() (string, string) { return self.cp.getNameAndType(self.nameAndTypeIndex) }
還有三個常量沒有介紹:CONSTANT_MethodType_info、 CONSTANT_MethodHandle_info和 CONSTANT_InvokeDynamic_info。它們是Java SE 7才添加到class文件中的,目的是支持新增的invokedynamic指令。本次暫不實現(xiàn)。
總結(jié)
可以把常量池中的常量分為兩類:字面量(literal)
和符號引用 (symbolic reference)
。
字面量
包括數(shù)字常量
和字符串常量
,符號引用
包括類
和接口名
、字段
和方法信息
等。
除了字面量,其他常量都是通過索引直接或間接指向CONSTANT_Utf8_info
常量,以 CONSTANT_Fieldref_info
為例,如下所示。
4.2.4解析屬性表
一些重要的信息沒有出現(xiàn),如方法的字節(jié)碼等。那么這些信息存在哪里呢?答案是屬性表。
AttributeInfo接口
和常量池類似,各種屬性表達的信息也各不相同,因此無法用統(tǒng)一的結(jié)構(gòu)來定義。不同之處在于,常量是由Java虛擬機規(guī)范嚴格 定義的,共有14種。
但屬性是可以擴展的,不同的虛擬機實現(xiàn)可以定義自己的屬性類型。
由于這個原因,Java虛擬機規(guī)范沒有使用tag
,而是使用屬性名來區(qū)別不同的屬性。
屬性數(shù)據(jù)放在屬性名之后的u1表
中,這樣Java虛擬機實現(xiàn)就可以跳過自己無法識別的屬性。 屬性的結(jié)構(gòu)定義如下:
attribute_info { u2 attribute_name_index; u4 attribute_length; u1 info[attribute_length]; }
屬性表中存放的屬性名實際上并不是編碼后的字符串, 而是常量池索引,指向常量池中的CONSTANT_Utf8_info
常量。
創(chuàng)建attribute_info.go
文件,在其中定義 AttributeInfo接口
package classfile type AttributeInfo interface { readInfo(reader *ClassReader) } func readAttributes(reader *ClassReader, cp ConstantPool) []AttributeInfo {...} func readAttribute(reader *ClassReader, cp ConstantPool) AttributeInfo {...} func newAttributeInfo(attrName string, attrLen uint32,cp ConstantPool) AttributeInfo {...}
和ConstantInfo接口
一樣,AttributeInfo接口
也只定義了一個readInfo()
方法,需要由具體的屬性實現(xiàn)。readAttributes()
函數(shù)讀取屬性表。
func readAttributes(reader *ClassReader, cp ConstantPool) []AttributeInfo { attributesCount := reader.readUint16() attributes := make([]AttributeInfo, attributesCount) for i := range attributes { attributes[i] = readAttribute(reader, cp) } return attributes }
讀取單個屬性函數(shù):
func readAttribute(reader *ClassReader, cp ConstantPool) AttributeInfo { attrNameIndex := reader.readUint16() attrName := cp.getUtf8(attrNameIndex) attrLen := reader.readUint32() attrInfo := newAttributeInfo(attrName, attrLen, cp) attrInfo.readInfo(reader) return attrInfo }
readAttribute()
先讀取屬性名索引,根據(jù)它從常量池
中找到屬性名
,然后讀取屬性長度,接著調(diào)用newAttributeInfo()
函數(shù)創(chuàng)建具體的屬性實例。
Java虛擬機規(guī)范預定義了23種屬性,先解析其中的8種。newAttributeInfo()
函數(shù)的代碼如下
func newAttributeInfo(attrName string, attrLen uint32, cp ConstantPool) AttributeInfo { switch attrName { case "Code": return &CodeAttribute{cp: cp} case "ConstantValue": return &ConstantValueAttribute{} case "Deprecated": return &DeprecatedAttribute{} case "Exceptions": return &ExceptionsAttribute{} case "LineNumberTable": return &LineNumberTableAttribute{} case "LocalVariableTable": return &LocalVariableTableAttribute{} case "SourceFile": return &SourceFileAttribute{cp: cp} case "Synthetic": return &SyntheticAttribute{} default: return &UnparsedAttribute{attrName, attrLen, nil} } }
創(chuàng)建attr_unparsed.go文件中,定義UnparsedAttribute結(jié)構(gòu)體
package classfile /* attribute_info { u2 attribute_name_index; u4 attribute_length; u1 info[attribute_length]; } */ type UnparsedAttribute struct { name string length uint32 info []byte } func (self *UnparsedAttribute) readInfo(reader *ClassReader) { self.info = reader.readBytes(self.length) } func (self *UnparsedAttribute) Info() []byte { return self.info }
按照用途,23種預定義屬性可以分為三組。
- 第一組屬性是實現(xiàn) Java虛擬機所必需的,共有5種;
- 第二組屬性是Java類庫所必需的,共有12種;
- 第三組屬性主要提供給工具使用,共有6種。
第三組屬性是可選的,也就是說可以不出現(xiàn)在class文件中。如果class文件中存在第三組屬性,Java虛擬機實現(xiàn)或者Java類庫也是可以利用它們 的,比如使用LineNumberTable屬性在異常堆棧中顯示行號。
如下給出了這23 種屬性出現(xiàn)的Java版本、分組以及它們在class文件中的位置。
Deprecated和Synthetic屬性
Deprecated
和Synthetic
是最簡單的兩種屬性,僅起標記作用,不包含任何數(shù)據(jù)。
這兩種屬性都是JDK1.1引入的,可以出現(xiàn)在 ClassFile、field_info和method_info結(jié)構(gòu)中,它們的結(jié)構(gòu)定義如下:
Deprecated_attribute { u2 attribute_name_index; u4 attribute_length; } Synthetic_attribute { u2 attribute_name_index; u4 attribute_length; }
由于不包含任何數(shù)據(jù),所以attribute_length
的值必須是0。
Deprecated屬性
用于指出類、接口、字段或方法已經(jīng)不建議使用,編譯器等工具可以根據(jù)Deprecated屬性輸出警告信息。
J2SE 5.0
之前 可以使用Javadoc提供的@deprecated標簽
指示編譯器給類、接口、字段或方法添加Deprecated屬性,語法格式如下:
/** @deprecated */ public void oldMethod() {...}
從J2SE 5.0
開始,也可以使用@Deprecated注解
,語法格式如下:
@Deprecated public void oldMethod() {}
在Java中,編譯器可能會生成一些額外的方法、字段或類,用于支持內(nèi)部的匿名內(nèi)部類、枚舉、泛型等特性。這些生成的元素可能會被標記為 Synthetic
。
創(chuàng)建attr_markers.go
文件,在其中定義 DeprecatedAttribute
和SyntheticAttribute
結(jié)構(gòu)體,代碼如下:
package classfile type DeprecatedAttribute struct { MarkerAttribute } type SyntheticAttribute struct { MarkerAttribute } type MarkerAttribute struct{} func (self *MarkerAttribute) readInfo(reader *ClassReader) { // read nothing }
SourceFile屬性
SourceFile
屬性是Java類文件中的一個屬性,它用于指定源文件的名稱,即生成該類文件的源代碼文件的名稱。這個屬性并不直接影響類的運行時行為。其結(jié)構(gòu)定義如下:
SourceFile_attribute { u2 attribute_name_index; u4 attribute_length; u2 sourcefile_index; }
attribute_length
的值必須是2。sourcefile_index
是常量池索引, 指向CONSTANT_Utf8_info常量
創(chuàng)建 attr_source_file.go
文件,在其中定義SourceFileAttribute結(jié)構(gòu)體
,代碼如下:
package classfile type SourceFileAttribute struct { cp ConstantPool sourceFileIndex uint16 } func (self *SourceFileAttribute) readInfo(reader *ClassReader) { self.sourceFileIndex = reader.readUint16() } func (self *SourceFileAttribute) FileName() string { return self.cp.getUtf8(self.sourceFileIndex) }
例如,如果有一個名為 MyClass.java
的源代碼文件,它包含以下類:
public class MyClass { public static void main(String[] args) { System.out.println("Hello, World!"); } }
當編譯 MyClass.java
文件時,會生成一個名為 MyClass.class
的類文件,并在其中添加一個 SourceFile
屬性,將其值設(shè)置為 MyClass.java
。
ConstantValue屬性
ConstantValue
屬性是Java類文件中的一個屬性,通常與字段(field)相關(guān)聯(lián)。這個屬性的作用是為字段提供一個常量初始值。這意味著,如果您在類中聲明一個字段,并為其分配了 ConstantValue
屬性,那么該字段的初始值將在類加載時被設(shè)置為 ConstantValue
中指定的常量。
ConstantValue_attribute { u2 attribute_name_index; u4 attribute_length; u2 constantvalue_index; }
例如,假設(shè)有以下Java代碼:
public class MyClass { public final int myField = 42; }
在對應(yīng)的類文件中,將包含一個 ConstantValue
屬性,指定了常量值 42
,并與 myField
字段相關(guān)聯(lián)。當類加載時,myField
將被初始化為 42
。
constantvalue_index
是常量池索引,具體指向哪種常量因字段類型而異,如下為對照表
創(chuàng)建attr_constant_value.go
文件,在其中定義ConstantValueAttribute
結(jié)構(gòu)體,代碼如下:
package classfile type ConstantValueAttribute struct { constantValueIndex uint16 } func (self *ConstantValueAttribute) readInfo(reader *ClassReader) { self.constantValueIndex = reader.readUint16() } func (self *ConstantValueAttribute) ConstantValueIndex() uint16 { return self.constantValueIndex }
Code屬性
Code
屬性是Java類文件中的一個屬性,通常與方法(Method)相關(guān)聯(lián)。
它包含了方法的字節(jié)碼指令,即實際的可執(zhí)行代碼。Code
屬性是Java類文件中最重要的屬性之一,因為它包含了方法的實際執(zhí)行邏輯。
以下是關(guān)于 Code
屬性的一些重要信息:
屬性結(jié)構(gòu):Code
屬性通常包含以下信息:
- 最大堆棧深度(
max_stack
):方法執(zhí)行時所需的最大堆棧深度。 - 局部變量表的大?。?code>max_locals):方法內(nèi)部局部變量表的大小。
- 字節(jié)碼指令(
code
):實際的字節(jié)碼指令序列,即方法的執(zhí)行代碼。 - 異常處理器列表(
exception_table
):用于捕獲和處理異常的信息。 - 方法屬性(
attributes
):其他與方法相關(guān)的屬性,例如局部變量表、行號映射表等。
字節(jié)碼指令:Code
屬性中的 code
部分包含了方法的實際字節(jié)碼指令,這些指令由Java虛擬機執(zhí)行。每個指令執(zhí)行一些特定的操作,例如加載、存儲、算術(shù)操作、分支、方法調(diào)用等。
異常處理:Code
屬性中的 exception_table
部分包含了異常處理器的信息,指定了哪些字節(jié)碼范圍可以拋出哪些異常,并且指定了如何處理這些異常。
局部變量表:Code
屬性中的局部變量表(max_locals
)用于存儲方法執(zhí)行期間的局部變量,例如方法參數(shù)和臨時變量。
屬性:Code
屬性中還可以包含其他屬性,如局部變量表、行號映射表等,這些屬性提供了更多的調(diào)試和運行時信息。
Code
屬性是Java虛擬機實際執(zhí)行方法的關(guān)鍵部分,它描述了方法的行為和操作,包括如何處理輸入和生成輸出。編譯器將源代碼編譯為字節(jié)碼,然后將字節(jié)碼填充到 Code
屬性中,這使得Java程序可以在虛擬機上執(zhí)行。
創(chuàng)建attr_code.go
文件,定義CodeAttribute結(jié)構(gòu)體
、ExceptionTableEntry結(jié)構(gòu)體
,代碼如下:
type CodeAttribute struct { cp ConstantPool maxStack uint16 maxLocals uint16 code []byte exceptionTable []*ExceptionTableEntry attributes []AttributeInfo } func (self *CodeAttribute) readInfo(reader *ClassReader) { self.maxStack = reader.readUint16() self.maxLocals = reader.readUint16() codeLength := reader.readUint32() self.code = reader.readBytes(codeLength) self.exceptionTable = readExceptionTable(reader) self.attributes = readAttributes(reader, self.cp) }
type ExceptionTableEntry struct { startPc uint16 endPc uint16 handlerPc uint16 catchType uint16 } func readExceptionTable(reader *ClassReader) []*ExceptionTableEntry { exceptionTableLength := reader.readUint16() exceptionTable := make([]*ExceptionTableEntry, exceptionTableLength) for i := range exceptionTable { exceptionTable[i] = &ExceptionTableEntry{ startPc: reader.readUint16(), endPc: reader.readUint16(), handlerPc: reader.readUint16(), catchType: reader.readUint16(), } } return exceptionTable }
Exceptions屬性
Exceptions屬性通常與方法(Method)相關(guān)聯(lián),用于指定方法可能拋出的受檢查異常(checked exceptions)的列表。
Exceptions_attribute { u2 attribute_name_index; u4 attribute_length; u2 number_of_exceptions; u2 exception_index_table[number_of_exceptions]; }
創(chuàng)建attr_exceptions.go
文件,在其中定義ExceptionsAttribute結(jié)構(gòu)體
type ExceptionsAttribute struct { exceptionIndexTable []uint16 } func (self *ExceptionsAttribute) readInfo(reader *ClassReader) { self.exceptionIndexTable = reader.readUint16s() } func (self *ExceptionsAttribute) ExceptionIndexTable() []uint16 { return self.exceptionIndexTable }
LineNumberTable和LocalVariableTable屬性
LineNumberTable
和 LocalVariableTable
屬性是Java類文件中的兩個用于調(diào)試和運行時跟蹤的屬性,它們包含了與源代碼中行號和局部變量相關(guān)的信息。
LineNumberTable 屬性:用于建立源代碼行號和字節(jié)碼指令之間的映射。它允許開發(fā)工具在調(diào)試時將異常棧軌跡映射到源代碼的特定行,以便開發(fā)者可以更容易地定位和修復代碼中的問題。結(jié)構(gòu)如下:
LineNumberTable { u2 attribute_name_index; u4 attribute_length; u2 line_number_table_length; { u2 start_pc; u2 line_number; } line_number_table[line_number_table_length]; }
LocalVariableTable 屬性:用于跟蹤局部變量的信息,包括局部變量的名稱、數(shù)據(jù)類型、作用域范圍和字節(jié)碼偏移。
創(chuàng)建attr_line_number_table.go
文件,定義LineNumberTableAttribute結(jié)構(gòu)體
,代碼如下:
type LineNumberTableAttribute struct { lineNumberTable []*LineNumberTableEntry } type LineNumberTableEntry struct { startPc uint16 lineNumber uint16 } func (self *LineNumberTableAttribute) readInfo(reader *ClassReader) { lineNumberTableLength := reader.readUint16() self.lineNumberTable = make([]*LineNumberTableEntry, lineNumberTableLength) for i := range self.lineNumberTable { self.lineNumberTable[i] = &LineNumberTableEntry{ startPc: reader.readUint16(), lineNumber: reader.readUint16(), } } } func (self *LineNumberTableAttribute) GetLineNumber(pc int) int { for i := len(self.lineNumberTable) - 1; i >= 0; i-- { entry := self.lineNumberTable[i] if pc >= int(entry.startPc) { return int(entry.lineNumber) } } return -1 }
創(chuàng)建attr_local_variable_table.go
文件,定義LocalVariableTableAttribute
,代碼如下:
type LocalVariableTableAttribute struct { localVariableTable []*LocalVariableTableEntry } type LocalVariableTableEntry struct { startPc uint16 length uint16 nameIndex uint16 descriptorIndex uint16 index uint16 } func (self *LocalVariableTableAttribute) readInfo(reader *ClassReader) { localVariableTableLength := reader.readUint16() self.localVariableTable = make([]*LocalVariableTableEntry, localVariableTableLength) for i := range self.localVariableTable { self.localVariableTable[i] = &LocalVariableTableEntry{ startPc: reader.readUint16(), length: reader.readUint16(), nameIndex: reader.readUint16(), descriptorIndex: reader.readUint16(), index: reader.readUint16(), } } }
4.3測試
打開ch03\main.go文件,修改import
語句和startJVM()
函數(shù),代碼如下:
package main import "fmt" import "strings" import "jvmgo/ch03/classfile" import "jvmgo/ch03/classpath" func main() { cmd := parseCmd() if cmd.versionFlag { fmt.Println("version 0.0.1") } else if cmd.helpFlag || cmd.class == "" { printUsage() } else { startJVM(cmd) } } func startJVM(cmd *Cmd) { cp := classpath.Parse(cmd.XjreOption, cmd.cpOption) className := strings.Replace(cmd.class, ".", "/", -1) cf := loadClass(className, cp) fmt.Println(cmd.class) printClassInfo(cf) } func loadClass(className string, cp *classpath.Classpath) *classfile.ClassFile { classData, _, err := cp.ReadClass(className) if err != nil { panic(err) } cf, err := classfile.Parse(classData) if err != nil { panic(err) } return cf } func printClassInfo(cf *classfile.ClassFile) { fmt.Printf("version: %v.%v\n", cf.MajorVersion(), cf.MinorVersion()) fmt.Printf("constants count: %v\n", len(cf.ConstantPool())) fmt.Printf("access flags: 0x%x\n", cf.AccessFlags()) fmt.Printf("this class: %v\n", cf.ClassName()) fmt.Printf("super class: %v\n", cf.SuperClassName()) fmt.Printf("interfaces: %v\n", cf.InterfaceNames()) fmt.Printf("fields count: %v\n", len(cf.Fields())) for _, f := range cf.Fields() { fmt.Printf(" %s\n", f.Name()) } fmt.Printf("methods count: %v\n", len(cf.Methods())) for _, m := range cf.Methods() { fmt.Printf(" %s\n", m.Name()) } }
首先go install jvmgo\ch03 生產(chǎn)ch03.exe
然后執(zhí)行,并輸入命令行語句,得到結(jié)果如下:
- version: 52.0:這表示
java.lang.String
類的類文件版本為 52.0。類文件版本號與Java版本號有關(guān),52.0 對應(yīng)于Java 8。 - constants count: 548:這表示常量池中包含 548 個常量。常量池包含了類的常量、方法、字段等信息。
- access flags: 0x31:這表示類的訪問標志,
0x31
是十六進制表示,對應(yīng)于二進制00110001
。這些標志描述類的訪問權(quán)限和特性。 - this class: java/lang/String:這表示類的名稱,即
java.lang.String
。 - super class: java/lang/Object:這表示
java.lang.String
類繼承自java.lang.Object
類。 - interfaces: [java/io/Serializable java/lang/Comparable java/lang/CharSequence]:這表示
java.lang.String
類實現(xiàn)了三個接口,分別是Serializable
、Comparable
和CharSequence
。 - fields count: 5:這表示
java.lang.String
類包含 5 個字段。 - methods count: 94:這表示
java.lang.String
類包含 94 個方法。其中一些是構(gòu)造方法(<init>
),其他是實例方法。
以上就是Golang實現(xiàn)Java虛擬機之解析class文件詳解的詳細內(nèi)容,更多關(guān)于Go實現(xiàn)Java虛擬機的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang基于內(nèi)存的鍵值存儲緩存庫go-cache
go-cache是一個內(nèi)存中的key:value store/cache庫,適用于單機應(yīng)用程序,本文主要介紹了Golang基于內(nèi)存的鍵值存儲緩存庫go-cache,具有一定的參考價值,感興趣的可以了解一下2025-03-03Go語言集成開發(fā)環(huán)境之VS Code安裝使用
VS Code是微軟開源的一款編輯器,插件系統(tǒng)十分的豐富,下面介紹如何用VS Code搭建go語言開發(fā)環(huán)境,需要的朋友可以參考下2021-10-10Go調(diào)用opencv實現(xiàn)圖片矯正的代碼示例
這篇文章主要為大家詳細介紹了Go調(diào)用opencv實現(xiàn)圖片矯正的代碼示例,文中的示例代碼簡潔易懂,感興趣的小伙伴可以跟隨小編一起學習一下2023-09-09