Golang實現JAVA虛擬機-解析class文件

来源:https://www.cnblogs.com/Gao-yubo/archive/2023/12/25/17925208.html
-Advertisement-
Play Games

這一章介紹了Thymeleaf,Java模板引擎,用於Web和獨立環境,與Spring Boot緊密集成。它適用於有無網路的場景,讓美工和程式員分別在瀏覽器和伺服器上查看靜態與動態頁面。筆記詳細講解Thymeleaf的配置、語法,如th:text提交基本數據、th:each穿越集合,以及通過th:i... ...


原文鏈接:https://gaoyubo.cn/blogs/de1bedad.html

前言

所需前置知識為:JAVA語言、JVM知識、Go筆記

對應項目:jvmgo

一、準備環境

操作系統: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應用程式。和其他類型的應用程式一樣,Java應用程式也需要一個入口點,這個入口點就是我們熟知的main()方法。最簡單的Java程式是 只有一個main()方法的類,如著名的HelloWorld程式。

public class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, world!");
  }
}

JVM如何知道從哪個類啟動呢,虛擬機規範並沒有明確,而是需要虛擬機實現。比如Oracle的JVM就是通過java命令啟動的,主類名由命令行參數決定

java命令有如下4種形式:

java [-options] class [args]
java [-options] -jar jarfile [args]
javaw [-options] class [args]
javaw [-options] -jar jarfile [args]

可以向java命令傳遞三組參數:選項、主類名(或者JAR文件名) 和main()方法參數。選項由減號(–)開頭。通常,第一個非選項參數 給出主類的完全限定名(fully qualified class name)。但是如果用戶提供了–jar選項,則第一個非選項參數表示JAR文件名,java命令必須從這個JAR文件中尋找主類。javaw命令和java命令幾乎一樣,唯 一的差別在於,javaw命令不顯示命令行視窗,因此特別適合用於啟 動GUI(圖形用戶界面)應用程式。

選項可以分為兩類:標準選項和非標準選項。標準選項比較穩定,不會輕易變動。非標準選項以-X開頭,

選項 用途
-version 輸出版本信息,然後退出
-? / -help 輸出幫助信息,然後退出
-cp / -classpath 指定用戶類路徑
-Dproperty=value 設置Java系統屬性
-Xms<size> 設置初始堆空間大小
-Xmx<size> 設置最大堆空間大小
-Xss<size> 設置線程棧空間大小

二、編寫命令行工具

環境準備完畢,接下來實現java命令的的第一種用法。

2.1 創建目錄

創建cmd.go

Go源文件一般以.go作為尾碼,文件名全部小寫,多個單詞之間使用下劃線分隔。Go語言規範要求Go源文件必須使用UTF-8編碼,詳見https://golang.org/ref/spec

2.2 結構體存儲cmd選項

在文件中定義cmd中java命令需要的選項和參數

package ch01

// author:郜宇博
type Cmd struct {
	// 標註是否為 --help
	helpFlag bool
	//標註是否為 --version
	versionFlag bool
	//選項
	cpOption string
	//主類名,或者是jar文件
	class string
	//參數
	args []string
}

Go語言標準庫包

由於要處理的命令行,因此將使用到flag()函數,此函數為Go的標準庫包之一。

Go語言的標準庫以包的方式提供支持,下表列出了Go語言標準庫中常見的包及其功能。

Go語言標準庫包名 功 能
bufio 帶緩衝的 I/O 操作
bytes 實現位元組操作
container 封裝堆、列表和環形列表等容器
crypto 加密演算法
database 資料庫驅動和介面
debug 各種調試文件格式訪問及調試功能
encoding 常見演算法如 JSON、XML、Base64 等
flag 命令行解析
fmt 格式化操作
go Go語言的詞法、語法樹、類型等。可通過這個包進行代碼信息提取和修改
html HTML 轉義及模板系統
image 常見圖形格式的訪問及生成
io 實現 I/O 原始訪問介面及訪問封裝
math 數學庫
net 網路庫,支持 Socket、HTTP、郵件、RPC、SMTP 等
os 操作系統平臺不依賴平臺操作封裝
path 相容各操作系統的路徑操作實用函數
plugin Go 1.7 加入的插件系統。支持將代碼編譯為插件,按需載入
reflect 語言反射支持。可以動態獲得代碼中的類型信息,獲取和修改變數的值
regexp 正則表達式封裝
runtime 運行時介面
sort 排序介面
strings 字元串轉換、解析及實用函數
time 時間介面
text 文本模板及 Token 詞法器

flag()函數

[]: https://studygolang.com/pkgdoc

eg:flag.TypeVar()

基本格式如下: flag.TypeVar(Type指針, flag名, 預設值, 幫助信息) 例如我們要定義姓名、年齡、婚否三個命令行參數,我們可以按如下方式定義:

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 接收處理用戶輸入的命令行指令

創建parseCmd()函數,實現接受處理用戶輸入的命令行指令

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()
}

首先設置flag.Usage變數,把printUsage()函數賦值給它;
然後調 用flag包提供的各種Var函數設置需要解析的選項;
接著調用 Parse()函數解析選項。

如果Parse()函數解析失敗,它就調用 printUsage()函數把命令的用法列印到控制台。

如果解析成功,調用flag.Args()函數可以捕獲其他沒有被解析 的參數。其中第一個參數就是主類名,剩下的是要傳遞給主類的參數。

2.4 測試

與cmd.go文件一樣,main.go文件的包名也是main。在Go 語言中,main是一個特殊的包,這個包所在的目錄(可以叫作任何 名字)會被編譯為可執行文件。Go程式的入口也是main()函數,但 是不接收任何參數,也不能有返回值。

測試代碼如下:

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()函數先調用ParseCommand()函數解析命令行參數,如 果一切正常,則調用startJVM()函數啟動Java虛擬機。如果解析出現錯誤,或者用戶輸入了-help選項,則調用PrintUsage()函數列印出幫助信息。如果用戶輸入了-version選項,則輸版本信息。因為我們還沒有真正開始編寫Java虛擬機,所以startJVM()函數暫時只是列印一些信息而已。

在終端:

go install jvmgo\ch0

此時在工作空間的bin目錄中會生成ch01.exe的文件,運行:結果如下

三、獲取類路徑

已經完成了JAVA應用程式如何啟動:命令行啟動,並獲取到了啟動時需要的選項和參數。

但是,如果要啟動一個最簡單的“Hello World”程式(如下),也需要載入很多所需的類進入JVM

public class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, world!");
  }
}

載入HelloWorld類之前,需要載入該類的父類(超類),也就是java.lang.Object,main函數的參數為String[]類型,因此也需要將java.lang.String類和java.lang.String[]載入,輸出字元串又需要載入java.lang.System類,等等。接下來就來解決如何獲取這些類的路徑。

3.1類路徑介紹

Java虛擬機規範並沒有規定虛擬機應該從哪裡尋找類,因此不同的虛擬機實現可以採用不同的方法。

Oracle的Java虛擬機實現根據類路徑(class path)來搜索類。

按照搜索的先後順序,類路徑可以 分為以下3個部分:

  • 啟動類路徑(bootstrap classpath)
  • 擴展類路徑(extension classpath)
  • 用戶類路徑(user classpath)

啟動類路徑預設對應jre\lib目錄,Java標準庫(大部分在rt.jar里) 位於該路徑。

擴展類路徑預設對應jre\lib\ext目錄,使用Java擴展機制的類位於這個路徑。

用戶類路徑為自己實現的類,以及第三方類庫的路徑。可以通過-Xbootclasspath選項修改啟動類路徑,不過一般不需要這樣做。
用戶類路徑的預設值是當前目錄,也就是. 。可以設置 CLASSPATH環境變數來修改用戶類路徑,但是這樣做不夠靈活,所以不推薦使用。
更好的辦法是給java命令傳遞-classpath(或簡寫為-cp)選項。-classpath/-cp選項的優先順序更高,可以覆蓋CLASSPATH環境變數設置。如下:

java -cp path\to\classes ...
java -cp path\to\lib1.jar ...
java -cp path\to\lib2.zip ...

3.2解析用戶類路徑

該功能建立在命令行工具上,因此複製上次的代碼,並創建classpath子目錄。

Java虛擬機將使用JDK的啟動類路徑來尋找和載入Java 標準庫中的類,因此需要某種方式指定jre目錄的位置。

命令行選項可以獲取,所以增加一個非標準選項-Xjre。

修改Cmd結構體,添加XjreOption欄位;parseCmd()函數也要相應修改:

type Cmd struct {
	// 標註是否為 --help
	helpFlag bool
	//標註是否為 --version
	versionFlag bool
	//選項
	cpOption string
	//主類名,或者是jar文件
	class string
	//參數
	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 {
		//第一個參數是主類名
		cmd.class = args[0]
		cmd.args = args[1:]
	}

	return cmd
}

3.3獲取用戶類路徑

可以把類路徑想象成一個大的整體,它由啟動類路徑、擴展類路徑和用戶類路徑三個小路徑構成。

三個小路徑又分別由更小的路徑構成。是不是很像組合模式(composite pattern)

接下來將使用組合模式來設計和實現類路徑。

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介面中有個兩方法。

  1. readClass()方法:負責尋找和載入class 文件。
    參數是class文件的相對路徑,路徑之間用斜線/分隔,文件名有.class尾碼。比如要讀取java.lang.Object類,傳 入的參數應該是java/lang/Object.class。返回值是讀取到的位元組數據、最終定位到class文件的Entry,以及錯誤信息。

  2. String()方法:作用相當於Java中的toString(),用於返回變數 的字元串表示。

Go的函數或方法允許返回多個值,按照慣例,可以使用最後一個返回值作為錯誤信息。

還需要一個類似於JAVA構造函數的函數,但在Go語言中沒有構造函數的概念,對象的創建通常交由一個全局的創建函數來完成,以NewXXX來命令,表示"構造函數"

newEntry()函數根據參數創建不同類型的Entry實例,代碼如下:

func newEntry(path string) Entry {
	////如果路徑包含分隔符 表示有多個文件 
	if strings.Contains(path, pathListSeparator) {
		return newCompositeEntry(path)
	}
	//包含*,則說明要將相應目錄下的所有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.實現類

存在四種類路徑指定方式:

  • 普通路徑形式:gyb/gyb
  • jar/zip形式:/gyb/gyb.jar
  • 通配符形式:gyb/*
  • 多個路徑形式:gyb/1:/gyb/2

DirEntry(普通形式)

創建entry_dir.go,定義DirEntry結構體:

package classpath

import "io/ioutil"
import "path/filepath"

type DirEntry struct {
	absDir string
}

func newDirEntry(path string) *DirEntry {
	//轉化為絕對路徑
	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結構體不需要顯示實現介面,只要方法匹配即可。

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文件,把內容讀取出來,並返回。如果找不到,或者出現其他錯 誤,則返回錯誤信息。有兩處使用了defer語句來確保打開的文件得 以關閉。

CompositeEntry(多路徑形式)

CompositeEntry由更小的Entry組成,正好可以表示成[]Entry。

在Go語言中,數組屬於比較低層的數據結構,很少直接使用。大部分情況下,使用更便利的slice類型。

構造函數把參數(路徑列表)按分隔符分成小路徑,然後把每個小路徑都轉換成具體的 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 屬於哪其他三種哪一種情況 生成對應的 ClassDirEntry類目錄對象
		entry := newEntry(path)
		compositeEntry = append(compositeEntry, entry)
	}

	return compositeEntry
}

func (self CompositeEntry) readClass(className string) ([]byte, Entry, error) {
	//遍歷切片 中的 類目錄對象
	for _, entry := range self {
		//如果找到了 對應的 類 直接返回
		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,然後調用filepath包的Walk()函數遍歷baseDir創建ZipEntryWalk()函數的第二個參數 也是一個函數。

walkFn中,根據尾碼名選出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 結尾的文件
		if strings.HasSuffix(path, ".jar") || strings.HasSuffix(path, ".JAR") {
			jarEntry := newZipEntry(path)
			compositeEntry = append(compositeEntry, jarEntry)
		}
		return nil
	}
	//遍歷 目錄下所有 .jar .JAR 文件 生成ZipEntry目錄對象 放在切片中返回
	//walFn為函數
	filepath.Walk(baseDir, walkFn)

	return compositeEntry
}

3.4實現類目錄

前面提到了 java 虛擬機預設 會先從啟動路徑--->擴展類路徑 --->用戶類路徑
按順序依次去尋找,載入類。
那麼就會有3個類目錄對象,所以就要定義一個結構體去存放它。

type Classpath struct {
	BootClasspath Entry
	ExtClasspath  Entry
	UserClasspath Entry
}

啟動類路徑

啟動路徑,其實對應Jre目錄下``lib` 也就是運行java 程式必須可少的基本運行庫。

通過 -Xjre 指定 如果不指定 會在當前路徑下尋找jre 如果找不到 就會從我們在裝java是配置的JAVA_HOME環境變數 中去尋找。

所以獲取驗證環境變數的方法如下:

func getJreDir(jreOption string) string {
	//如果 從cmd  -Xjre 獲取到目錄 並且存在
	if jreOption != "" && exists(jreOption) {
	//返回目錄
		return jreOption 
	}
	//如果 當前路徑下 有 jre 返回目錄
	if exists("./jre") { 
		return "./jre"
	}
	//如果 上面都找不到 到系統環境 變數中尋找 
	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 = "." 
	}
	//創建 類目錄對象
	self.UserClasspath = newEntry(cpOption)
}

實現類的載入

對於指定文件類名取查找 我們是按前面提到的(啟動路徑--->擴展類路徑 --->用戶類路徑
按順序依次去尋找,載入類),沒找到就挨個查找下去。

如果用戶沒有提供-classpath/-cp選項,則使用當前目錄作為用 戶類路徑。ReadClass()方法依次從啟動類路徑、擴展類路徑和用戶 類路徑中搜索class文件,

//根據類名 分別從 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)
}

初始化類載入目錄

定義一個初始化函數,來作為初始函數,執行後生成一個 Classpath對象。

//jreOption 為啟動類目錄  cpOption 為 用戶指定類目錄 從cmd 命令行 中解析獲取
func InitClassPath(jreOption, cpOption string) *Classpath {
	cp := &Classpath{}
	//初始化 啟動類目錄
	cp.parseBootAndExtClasspath(jreOption)
	//初始化 用戶類目錄
	cp.parseUserClasspath(cpOption)
	return cp
}

註意,傳遞給ReadClass()方法的類名不包含“.class”尾碼。

3.5總結

3.6測試

成功獲取到class文件!

四、解析Class文件

4.1 class文件介紹

詳細class文件分析

作為類/介面信息的載體,每一個class文件都完整的定義了一個類,為了使Java程式可以實現“編寫一次,處處運行”,java虛擬機對class文件的格式進行了嚴格的規範。

但是對於從哪裡載入class文件,給予了高度自由空間:第三節中說過,可以從文件系統讀取jar/zip文件中的class文件,除此之外,也可以從網路下載,甚至是直接在運行中生成class文件

構成class文件的基本數據單位是位元組,可以把整個class文件當 成一個位元組流來處理。稍大一些的數據由連續多個位元組構成,這些數據在class文件中以大端(big-endian)方式存儲。

為了描述class文件格式,Java虛擬機規範定義了u1u2u4三種數據類型來表示1、 2和4位元組無符號整數,分別對應Go語言的uint8uint16uint32類型。

相同類型的多條數據一般按表(table)的形式存儲在class文件中。表由表頭表項(item)構成,表頭是u2或u4整數。假設表頭是 n,後面就緊跟著n個表項數據。

Java虛擬機規範使用一種類似C語言的結構體語法來描述class 文件格式。整個class文件被描述為一個ClassFile結構,代碼如下:

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語言內置了豐富的數據類型,非常適合處理class文件。

如下為Go和Java語言基本數據類型對照關係:

Go語言類型 Java語言類型 說明
int8 byte 8比特有符號整數
uint8(別名byte) N/A 8比特無符號整數
int16 short 16比特有符號整數
uint16 char 16比特無符號整數
int32(別名rune) int 32比特有符號整數
uint32 N/A 32比特無符號整數
int64 long 64比特有符號整數
uint64 N/A 64比特無符號整數
float32 float 32比特IEEE-754浮點數
float64 double 64比特IEEE-754浮點數

4.2.1讀取數據

解析class文件的第一步是從裡面讀取數據。雖然可以把class文件當成位元組流來處理,但是直接操作位元組很不方便,所以先定義一個結構體ClassReader來幫助讀取數據,創建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類型數據。

ClassReader並沒有使用索引記錄數據位置,而是使用Go 語言的reslice語法跳過已經讀取的數據

實現代碼如下:

// 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中解碼多位元組數據。

4.2.2解析整體結構

有了ClassReader,可以開始解析class文件了。創建class_file.go文件,在其中定義ClassFile結構體,與4.1中的class文件中欄位對應。

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文件中實現一系列函數和方法。

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的訪問控制非常簡單:只有公開和私有兩種。

所有首字母大寫的類型、結構體、欄位、變數、函數、方法等都是公開的,可供其他包使用。
首字母小寫則是私有的,只能在包內部使用。

解析[]byte

Parse()函數把[]byte解析成ClassFile結構體。

func Parse(classData []byte) (cf *ClassFile, err error) {
    defer func() {
       //嘗試捕獲 panic,並將其存儲在變數 r 中。如果沒有發生 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() 方法依次調用其他方法解析class文件,順序一定要保證正確,與class文件相對應。

func (self *ClassFile) read(reader *ClassReader) {
  	//讀取並檢查類文件的魔數。 
    self.readAndCheckMagic(reader)
	//讀取並檢查類文件的版本號。
    self.readAndCheckVersion(reader)
    //解析常量池,常量池類還沒寫
    self.constantPool = readConstantPool(reader)
    //讀取類的訪問標誌
    self.accessFlags = reader.readUint16()
    //讀取指向當前類在常量池中的索引
    self.thisClass = reader.readUint16()
    //父類在常量池中的索引
    self.superClass = reader.readUint16()
    //讀取介面表的數據
    self.interfaces = reader.readUint16s()
    //讀取類的欄位信息
    self.fields = readMembers(reader, self.constantPool)
    //讀取類的方法信息
    self.methods = readMembers(reader, self.constantPool)
    //讀取類的屬性信息(類級別的註解、源碼文件等)
    self.attributes = readAttributes(reader, self.constantPool)
}
  1. self.readAndCheckMagic(reader): 這是一個 ClassFile 結構的方法,用於讀取並檢查類文件的魔數。魔數是類文件的標識符,用於確定文件是否為有效的類文件。
  2. self.readAndCheckVersion(reader): 這個方法用於讀取並檢查類文件的版本號。Java類文件具有版本號,標識了它們的Java編譯器版本。這裡會對版本號進行檢查。
  3. self.constantPool = readConstantPool(reader): 這一行代碼調用 readConstantPool 函數來讀取常量池部分的數據,並將其存儲在 ClassFile 結構的 constantPool 欄位中。常量池是一個包含各種常量信息的表格,用於支持類文件中的各種符號引用。
  4. self.accessFlags = reader.readUint16(): 這一行代碼讀取類的訪問標誌,它標識類的訪問許可權,例如 publicprivate 等。
  5. self.thisClass = reader.readUint16(): 這行代碼讀取指向當前類在常量池中的索引,表示當前類的類名。
  6. self.superClass = reader.readUint16(): 這行代碼讀取指向父類在常量池中的索引,表示當前類的父類名。
  7. self.interfaces = reader.readUint16s(): 這行代碼讀取介面表的數據,表示當前類實現的介面。
  8. self.fields = readMembers(reader, self.constantPool): 這行代碼調用 readMembers 函數,以讀取類的欄位信息,並將它們存儲在 fields 欄位中。欄位包括類的成員變數。
  9. self.methods = readMembers(reader, self.constantPool): 這行代碼類似於上一行,但它讀取類的方法信息,並將它們存儲在 methods 欄位中。
  10. self.attributes = readAttributes(reader, self.constantPool): 最後,這行代碼調用 readAttributes 函數,以讀取類的屬性信息,並將它們存儲在 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同理,常量池還未實現。

所有類的超類(父類),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的類是單繼承,多實現的,因此獲取介面應該使用迴圈,也從常量池中獲取。

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
}

解析魔數

很多文件格式都會規定滿足該格式的文件必須以某幾個固定位元組開頭,這幾個位元組主要起標識作用,叫作魔數(magic number)

  • PDF文件以4位元組“%PDF”(0x25、0x50、0x44、0x46)開頭
  • ZIP 文件以2位元組“PK”(0x50、0x4B)開頭
  • class文件的魔數 是“0xCAFEBABE” 。

因此readAndCheckMagic()方法的代碼如下。

func (self *ClassFile) readAndCheckMagic(reader *ClassReader) {
    magic := reader.readUint32()
    if magic != 0xCAFEBABE {
       panic("java.lang.ClassFormatError: magic!")
    }
}

Java虛擬機規範規定,如果載入的class文件不符合要求的格式,Java虛擬機實現就拋出java.lang.ClassFormatError異常。

但是因為我們才剛剛開始編寫虛擬機,還無法拋出異常,所以暫時先調用 panic()方法終止程式執行。

版本號

解析版本號

魔數之後是class文件的次版本號和主版本號,都是u2類型

假設某class文件的主版本號是M,次版本號是m,那麼完整的版本號 可以表示成M.m的形式。
次版本號只在J2SE 1.2之前用過,從1.2 開始基本上就沒什麼用了(都是0)。
主版本號在J2SE 1.2之前是45, 從1.2開始,每次有大的Java版本發佈,都會加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虛擬機實現只能支持版本號在某個範圍內的class文 件。
Oracle的實現是完全向後相容的,比如Java SE 8支持版本號為 45.0~52.0的class文件。

如果版本號不在支持的範圍內,Java虛擬機 實現就拋出java.lang.UnsupportedClassVersionError異常。參考 Java 8,支持版本號為45.0~52.0的class文件。如果遇到其他版本號, 調用panic()方法終止程式執行。
如下為檢查版本號代碼:

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 節介紹。

常量池之後是類訪問標誌,這是一個16位的bitmask,指出class文件定義的是類還是介面,訪問級別是public還是private,等等。

本章只對class文件進行初步解析,並不做完整驗證,所以只是讀取類訪問標誌以備後用。

ClassFileTest的類訪問標誌為:0X21:

解析類和父類索引

類訪問標誌之後是兩個u2類型的常量池索引,分別給出類名和超類名。

class文件存儲的類名類似完全限定名,但是把點換成了 斜線,Java語言規範把這種名字叫作二進位名binary names

因為每個類都有名字,所以thisClass必須是有效的常量池索引。
java.lang.Object之外,其他類都有超類,所以superClass只在 Object.class中是0,在其他class文件中必須是有效的常量池索引。如下,ClassFileTest的類索引是5,超類索引是6。

解析介面索引表

類和超類索引後面是介面索引表,表中存放的也是常量池索引,給出該類實現的所有介面的名字。ClassFileTest沒有實現介面, 所以介面表是空的

解析欄位和方法表

介面索引表之後是欄位表和方法表,分別存儲欄位和方法信息。

欄位和方法的基本結構大致相同,差別僅在於屬性表。
下麵是 Java虛擬機規範給出的欄位結構定義

field_info {
    u2 access_flags;
    u2 name_index;
    u2 descriptor_index;
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

和類一樣,欄位和方法也有自己的訪問標誌。訪問標誌之後是一個常量池索引,給出欄位名或方法名,然後又是一個常量池索引,給出欄位或方法的描述符,最後是屬性表。

為了避免重覆代 碼,用一個結構體統一表示欄位和方法。

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()函數讀取欄位或方法數據。

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解析常量池

常量池占據了class文件很大一部分數據,裡面存放著各式各樣的常量信息,包括數字和字元串常量、類和介面名、欄位和方法名,等等

創建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。假設表頭給出的值是n,那麼常量池的實際大小是n–1。

有效的常量池索引是1~n–1。0是無效索引,表示不指向任何常量。

CONSTANT_Long_info CONSTANT_Double_info各占兩個位置。也就是說,如果常量池中存在這兩種常量,實際的常量數量比n–1還要少,而且1~n–1的某些 數也會變成無效索引。

常量池由readConstantPool()函數讀取,代碼如下:

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介面

由於常量池中存放的信息各不相同,所以每種常量的格式也不同。
常量數據的第一位元組是tag,用來區分常量類型。

下麵是Java 虛擬機規範給出的常量結構

cp_info {
    u1 tag;
    u1 info[];
}

Java虛擬機規範一共定義了14種常量。創建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()方法讀取常量信息,需要由具體的常量結構體實現。 readConstantInfo()函數先讀出tag值,然後調用newConstantInfo()函數創建具體的常量,最後調用常量的readInfo()方法讀取常量信息, 代碼如下:

func readConstantInfo(reader *ClassReader, cp ConstantPool) ConstantInfo {
    tag := reader.readUint8()
    c := newConstantInfo(tag, cp)
    c.readInfo(reader)
    return c
}

newConstantInfo()根據tag值創建具體的常量,代碼如下:

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位元組存儲整數常量,其JVM結構定義如下:

CONSTANT_Integer_info {
    u1 tag;
    u4 bytes;
}

CONSTANT_Integer_info和後面將要介紹的其他三種數字常量無論是結構,還是實現,都非常相似,所以把它們定義在同一個文件中。創建cp_numeric.go文件,在其中定義 ConstantIntegerInfo結構體,代碼如下:

package classfile
import "math"
type ConstantIntegerInfo struct {
	val int32
}
func (self *ConstantIntegerInfo) readInfo(reader *ClassReader) {...}

readInfo()先讀取一個uint32數據,然後把它轉型成int32類型, 代碼如下

func (self *ConstantIntegerInfo) readInfo(reader *ClassReader) {
    bytes := reader.readUint32()
    self.val = int32(bytes)
}

CONSTANT_Float_info

CONSTANT_Float_info使用4位元組存儲IEEE754單精度浮點數常量,JVM結構如下:

CONSTANT_Float_info {
    u1 tag;
    u4 bytes;
}

cp_numeric.go文件中定義ConstantFloatInfo結構體,代碼如下:

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位元組存儲整數常量,結構如下:

CONSTANT_Long_info {
    u1 tag;
    u4 high_bytes;
    u4 low_bytes;
}

cp_numeric.go文件中定義ConstantLongInfo結構體,代碼如下:

type ConstantLongInfo struct {
    val int64
}

func (self *ConstantLongInfo) readInfo(reader *ClassReader) {
    bytes := reader.readUint64()
    self.val = int64(bytes)
}

CONSTANT_Double_info

最後一個數字常量是CONSTANT_Double_info,使用8位元組存儲IEEE754雙精度浮點數,結構如下:

CONSTANT_Double_info {
    u1 tag;
    u4 high_bytes;
    u4 low_bytes;
}

cp_numeric.go文件中定義ConstantDoubleInfo結構體,代碼如下:

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編碼的字元串, 結構如下:

CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

Java類文件中使用MUTF-8(Modified UTF-8)編碼而不是標準的UTF-8,是因為MUTF-8在某些方面更適合於在Java虛擬機內部處理字元串。以下是一些原因:

  1. 空字元的表示: 在標準的UTF-8編碼中,空字元(U+0000)會使用單個位元組0x00表示,這與C字元串中的字元串終止符相同,可能引起混淆。在MUTF-8中,空字元會使用0xC0 0x80來表示,避免了混淆。
  2. 編碼長度: MUTF-8編碼中的每個字元都使用1至3個位元組來表示,這與UTF-8編碼相比更緊湊。對於大多數常見的字元集,這可以減少存儲和傳輸開銷。
  3. 字元的編碼範圍: MUTF-8編碼對字元的範圍進行了限制,只包含Unicode BMP(基本多文種平面)範圍內的字元。這些字元通常足夠用於表示Java標識符和字元串文字。
  4. 相容性: 早期版本的Java使用的是MUTF-8編碼,因此為了保持與早期版本的相容性,後續版本也繼續使用MUTF-8。這有助於確保Java類文件的可互操作性。

創建cp_utf8.go文件,在其中定義 ConstantUtf8Info結構體,代碼如下:

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)
}
  1. 初始化 chararr 數組,用於存儲UTF-16字元。
  2. 遍歷MUTF-8位元組數組中的位元組,根據位元組的值來判斷字元的編碼方式。
  3. 如果位元組值小於128,表示ASCII字元,直接轉換為UTF-16並存儲。
  4. 如果位元組值在特定範圍內,表示多位元組字元,需要根據UTF-8編碼規則進行解碼。
  5. 如果遇到不符合規則的位元組,拋出異常來處理錯誤情況。
  6. 最後,將解碼後的UTF-16字元轉換為Go字元串。

CONSTANT_String_info

CONSTANT_String_info常量表示java.lang.String字面量,結構如下:

CONSTANT_String_info {
    u1 tag;
    u2 string_index;
}

可以看到,CONSTANT_String_info本身並不存放字元串數據
只存了常量池索引,這個索引指向一個CONSTANT_Utf8_info常量

下創建cp_string.go文件,在其中定義 ConstantStringInfo結構體

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結構如下:

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

CONSTANT_String_info類似,name_index是常量池索引,指向CONSTANT_Utf8_info常量。
創建 cp_class.go文件,定義ConstantClassInfo結構體

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加在 一起可以唯一確定一個欄位或者方法。其結構如下:

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

欄位或方法名由name_index給出,欄位或方法的描述符由 descriptor_index給出。

name_indexdescriptor_index都是常量池索引,指向CONSTANT_Utf8_info常量

Java虛擬機規範定義了一種簡單的語法來描述欄位和方法,可以根據下麵的規則生成描述符。

一、類型描述符

  1. 基本類型byte、short、char、int、long、float和double的描述符是單個字母,分別對應B、S、C、I、J、F和D。註意,long的描述符是J 而不是L。
  2. 引用類型的描述符是L+類的完全限定名+分號。
  3. 數組類型的描述符是[+數組元素類型描述符

二、欄位描述符

​ 欄位類型的描述符

三、方法描述符

​ 分號分隔的參數類型描述符+返回值類型描述符,其中void返回值由單個字母V表示。

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 二三、編譯器 1、One Definition Rule 1)轉化單元 我們寫好的每個源文件(.cpp,.c)將其所包含的頭文件(#include <xxx.h>)合併後,稱為一個轉化單元。 編譯器單獨的將每一個轉化單元生成為對應的對象文件(.obj),對象文件包含了轉化單元的機器碼和轉化單元的引用 ...
  • Qt 是一個跨平臺C++圖形界面開發庫,利用Qt可以快速開發跨平臺窗體應用程式,在Qt中我們可以通過拖拽的方式將不同組件放到指定的位置,實現圖形化開發極大的方便了開發效率,本章將重點介紹`QCharts`折線圖的常用方法及靈活運用。折線圖(Line Chart)是一種常用的數據可視化圖表,用於展示隨... ...
  • Go 泛型之瞭解類型參數 目錄Go 泛型之瞭解類型參數一、Go 的泛型與其他主流編程語言的泛型差異二、返回切片中值最大的元素三、類型參數(type parameters)四、泛型函數3.1 泛型函數的結構3.2 調用泛型函數3.3 泛型函數實例化(instantiation)五、泛型類型5.1 聲明 ...
  • C語言中的布爾值 在編程中,您經常需要一種只能有兩個值的數據類型,例如: 是/否 開/關 真/假 為此,C語言有一個 bool 數據類型,稱為布爾值。 布爾變數 在C語言中,bool 類型不是內置數據類型,例如 int 或 char 它是在 C99 中引入的,您必須導入以下頭文件才能使用它: #in ...
  • 痞子衡嵌入式半月刊: 第 88 期 這裡分享嵌入式領域有用有趣的項目/工具以及一些熱點新聞,農曆年分二十四節氣,希望在每個交節之日準時發佈一期。 本期刊是開源項目(GitHub: JayHeng/pzh-mcu-bi-weekly),歡迎提交 issue,投稿或推薦你知道的嵌入式那些事兒。 上期回顧 ...
  • 一、簡介 由飛利浦主導開發的片間互聯協議。iic通信使用三線(sda scl以及gnd,不包括電源線),極大程度上減少了對ic的io口的占用。同時iic支持多主機以及多從機,方便了程式的設計。 二、協議層簡介 在iic匯流排上scl的電平決定了整條iic匯流排的有效性。 當scl出於高電平時,主機與從機 ...
  • 系統環境 ██████████████████ ████████ littleblacklb@lb-desktop ██████████████████ ████████ ██████████████████ ████████ OS: Manjaro Linux x86_64 ███████████ ...
  • 前言 經過前面的文章介紹,基本上 UniApp 的內容就介紹完畢了 那麼從本文開始,我們就開始進行一個項目的實戰 這次做的項目是蘋果計算器,這個項目的難度不是很大,但是也不是很簡單,適合練手 創建項目 打開 HBuilderX,點擊左上角 文件 -> 新建 -> 項目: 搭建基本佈局 項目創建完畢之 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...