欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Golang實現(xiàn)Java虛擬機之解析class文件詳解

 更新時間:2024年01月11日 09:18:45   作者:橡皮筋兒  
這篇文章主要為大家詳細介紹了Golang實現(xiàn)Java虛擬機之解析class文件的相關(guān)知識,文中的示例代碼講解詳細,感興趣的小伙伴可以跟隨小編一起學習一下

前言

所需前置知識為: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格式化操作
goGo語言的詞法、語法樹、類型等??赏ㄟ^這個包進行代碼信息提取和修改
htmlHTML 轉(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ù)
pluginGo 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文件分析

作為類/接口信息的載體,每一個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、u2u4三種數(shù)據(jù)類型來表示1、 2和4字節(jié)無符號整數(shù),分別對應(yīng)Go語言的uint8uint16uint32類型。

相同類型的多條數(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語言類型說明
int8byte8比特有符號整數(shù)
uint8(別名byte)N/A8比特無符號整數(shù)
int16short16比特有符號整數(shù)
uint16char16比特無符號整數(shù)
int32(別名rune)int32比特有符號整數(shù)
uint32N/A32比特無符號整數(shù)
int64long64比特有符號整數(shù)
uint64N/A64比特無符號整數(shù)
float32float32比特IEEE-754浮點數(shù)
float64double64比特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.145.3
Java 1.246.0
Java 1.347.0
Java 1.448.0
Java 549.0
Java 650.0
Java 751.0
Java 852.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_infoCONSTANT_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_infoCONSTANT_NameAndType_info加在 一起可以唯一確定一個字段或者方法。其結(jié)構(gòu)如下:

CONSTANT_NameAndType_info {
    u1 tag;
    u2 name_index;
    u2 descriptor_index;
}

字段或方法名由name_index給出,字段或方法的描述符由descriptor_index給出。

name_indexdescriptor_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_indexname_and_type_index都是常量池索引,分別指向CONSTANT_Class_infoCONSTANT_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屬性

DeprecatedSynthetic是最簡單的兩種屬性,僅起標記作用,不包含任何數(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文件,在其中定義 DeprecatedAttributeSyntheticAttribute結(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)了三個接口,分別是 SerializableComparable 和 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

    Golang基于內(nèi)存的鍵值存儲緩存庫go-cache

    go-cache是一個內(nèi)存中的key:value store/cache庫,適用于單機應(yīng)用程序,本文主要介紹了Golang基于內(nèi)存的鍵值存儲緩存庫go-cache,具有一定的參考價值,感興趣的可以了解一下
    2025-03-03
  • go的切片擴容機制詳解

    go的切片擴容機制詳解

    本文主要介紹了go的切片擴容機制詳解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2023-04-04
  • 手把手帶你走進Go語言之運算符解析

    手把手帶你走進Go語言之運算符解析

    這篇文章主要介紹了手Go語言之運算符解析,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2021-09-09
  • Go語言集成開發(fā)環(huán)境之VS Code安裝使用

    Go語言集成開發(fā)環(huán)境之VS Code安裝使用

    VS Code是微軟開源的一款編輯器,插件系統(tǒng)十分的豐富,下面介紹如何用VS Code搭建go語言開發(fā)環(huán)境,需要的朋友可以參考下
    2021-10-10
  • Go語言實現(xiàn)定時器的方法

    Go語言實現(xiàn)定時器的方法

    這篇文章主要介紹了Go語言實現(xiàn)定時器的方法,涉及Go語言時間操作技巧,具有一定參考借鑒價值,需要的朋友可以參考下
    2015-02-02
  • CGO編程基礎(chǔ)快速入門

    CGO編程基礎(chǔ)快速入門

    這篇文章主要為大家介紹了CGO編程基礎(chǔ)快速入門示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-12-12
  • Golang分布式注冊中心實現(xiàn)流程講解

    Golang分布式注冊中心實現(xiàn)流程講解

    這篇文章主要介紹了Golang分布式注冊中心實現(xiàn)流程,注冊中心可以用于服務(wù)發(fā)現(xiàn),服務(wù)注冊,配置管理等方面,在分布式系統(tǒng)中,服務(wù)的發(fā)現(xiàn)和注冊是非常重要的組成部分,需要的朋友可以參考下
    2023-05-05
  • go語言實現(xiàn)兩個協(xié)程交替打印

    go語言實現(xiàn)兩個協(xié)程交替打印

    這篇文章主要介紹了go語言實現(xiàn)兩個協(xié)程交替打印,文章主要分享了兩種方法使用兩個channel和使用一個channel,內(nèi)容介紹詳細具有一定的參考價值,需要的小伙伴可以參考一下
    2022-03-03
  • Go一站式配置管理工具Viper的使用教程

    Go一站式配置管理工具Viper的使用教程

    Viper是一個方便Go語言應(yīng)用程序處理配置信息的庫,它可以處理多種格式的配置,這篇文章主要為大家介紹了它的具體使用教程,需要的可以參考下
    2023-08-08
  • Go調(diào)用opencv實現(xiàn)圖片矯正的代碼示例

    Go調(diào)用opencv實現(xiàn)圖片矯正的代碼示例

    這篇文章主要為大家詳細介紹了Go調(diào)用opencv實現(xiàn)圖片矯正的代碼示例,文中的示例代碼簡潔易懂,感興趣的小伙伴可以跟隨小編一起學習一下
    2023-09-09

最新評論