這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。JIT(Just-int-time) 編譯器是任何程式在被轉換成機器碼的運行過程中產生的。JIT 代碼和其他代碼(比如,fmt.Println)的區別在於 JIT 代碼是在運行過程中產生的。用 Golang 編寫的程式是靜態類型且提前編譯。產生任意代碼似乎是不可能的,更不用說執行所述代碼了。但是,可以將指令發送到正在啟動並執行進程。這是使用 Type Magic 完成的 - 將任何類型轉換為任何其他類型的能力。 請注意,如果您有興趣瞭解更多關於 Type Magic 的資訊,請在下面留言,我會在後面寫文闡述。## x64 指令集上的 JIT 編譯器機器碼是對處理器有特殊意義的一系列位元組。用於編寫此部落格並測試代碼的機器使用的是 x64 處理器,因此我使用了 [`x64` 指令集](https://software.intel.com/en-us/articles/introduction-to-x64-assembly)。 以下代碼必須在 x64 處理器上運行。## 產生 x64 代碼列印 "Hello World"為了列印 “Hello World”,系統調用應該指示處理器列印資料。列印資料的系統調用是 [write(int fd,const void * buf,size_t count)](http://man7.org/linux/man-pages/man2/write.2.html)。 此系統調用的第一個參數是要寫入的位置,表示為檔案描述符。將輸出列印到控制台是通過寫入標準檔案描述符 stdout 來實現的。stdout 的檔案描述符編號為 1. 第二個參數是必須寫入的資料的位置。有關這方面的更多資訊將在下一節中提供。 第三個運算元是 count - 即要寫入的位元組數。在 “Hello World!” 的情況下,要寫入的位元組數為 12。為了進行系統調用,需要將三個運算元儲存在特定的寄存器中。這裡有一個表格顯示了儲存運算元的寄存器。|Syscall # |Param 1 | Param 2 | Param 3 | Param 4 | Param 5 | Param 6 ||:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:||rax|rdi|rsi|rdx|r10|r8|r9將所有這些放在一起,這裡是一系列代表初始化一些寄存器的指令的位元組。```0: 48 c7 c0 01 00 00 00 mov rax,0x1 7: 48 c7 c7 01 00 00 00 mov rdi,0x1 e: 48 c7 c2 0c 00 00 00 mov rdx,0xc ```- 第一條指令將 rax 設定為 1 - 表示寫入系統調用。- 第二條指令將 rdi 設定為 1 - 表示 stdout 的檔案描述符- 第三條指令將 rdx 設定為 12 以表示要列印的位元組數。- 資料的位缺失,實際上調用 write 就是如此為了指定包含 “Hello World!” 的資料的位置,資料需要先擁有一個位置 - 即它需要儲存在記憶體中的某個位置。 表示 “Hello World!” 的位元組序列是 48 65 6c 6c 6f 20 57 6f 72 6c 64 21。這應該儲存在處理器不會嘗試執行的位置。否則,該程式將引發段錯誤(segmentation fault)。 在這種情況下,資料可以儲存在可執行指令的末尾 - 即在返回指令之後。在返回指令之後儲存資料是安全的,因為處理器在遇到返回時“跳”到不同的地址,並且不會順序執行。 由於直到返回指令被布置時才知道過去的返回地址,所以可以使用它的臨時預留位置,並且一旦資料的地址已知就用正確的地址替換。這是連接器所遵循的確切程式。連結過程只需填寫這些地址以指向正確的資料或函數。```15: 48 8d 35 00 00 00 00 lea rsi,[rip+0x0] # 0x15 1c: 0f 05 syscall 1e: c3 ret ```在上面的代碼中,載入 “Hello World!” 地址的 lea 指令指向自己(指向距離 rip 0 位元組的位置)。這是因為資料尚未儲存,資料地址未知。 系統調用本身由位元組序列 0F 05 表示。現在可以儲存資料,因為返回指令已經布置。```1f: 48 65 6c 6c 6f 20 57 6f 72 6c 64 21 // Hello World!```在整個程式中,現在我們可以更新指令來指向資料。以下是更新的代碼:```0: 48 c7 c0 01 00 00 00 mov rax,0x17: 48 c7 c7 01 00 00 00 mov rdi,0x1e: 48 c7 c2 0c 00 00 00 mov rdx,0xc15: 48 8d 35 03 00 00 00 lea rsi,[rip+0x3] # 0x1f1c: 0f 05 syscall1e: c3 ret1f: 48 65 6c 6c 6f 20 57 6f 72 6c 64 21 // Hello World! ```上面的代碼可以表示為 Golang 中任何基本類型的片段。 uint16 類型的數組/ slice 是一個不錯的選擇,因為它可以儲存成對的小端有序單詞,同時仍然保持可讀性。這裡是儲存上述程式的 `[]uint16` 資料結構```goprintFunction := []uint16{0x48c7,0xc001, 0x0, // mov %rax ,$0x10x48,0xc7c7,0x100,0x0, // mov %rdi ,$0x10x48c7, 0xc20c, 0x0, // mov 0x13, %rdx0x48, 0x8d35, 0x400, 0x0, // lea 0x4(%rip), %rsi0xf05, // syscall0xc3cc, // ret0x4865, 0x6c6c, 0x6f20, // Hello_(whitespace)0x576f, 0x726c, 0x6421, 0xa, // World!} ```與上面列出的位元組相比,上述位元組略有偏差。這是因為當它與切片條目的開始對齊時,它更清晰(更易於讀取和調試)來表示資料 “Hello World!”。 因此,我使用填充指令 cc 指令(無操作)將資料部分的開始推送到 slice 中的下一個條目。我還更新了 lea 指向 4 個位元組的位置以反映這一變化。 注意:您可以在此`[連結](https://filippo.io/linux-syscall-table/)找到各種系統調用的系統調用號碼。## 轉換切片函數`[]uint16` 資料結構中的指令必須轉換為一個函數,以便可以調用它。下面的代碼示範了這種轉換。```gotype printFunc func()unsafePrintFunc := (uintptr)(unsafe.Pointer(&printFunction)) printer := *(*printFunc)(unsafe.Pointer(&unsafePrintFunc)) printer()```Golang 函數值只是一個指向 C 函數指標的指標(注意兩級指標)。從切片到函數的轉換首先是提取一個指向儲存可執行代碼的資料結構的指標。這儲存在 unsafePrintFunc 中。指向 unsafePrintFunc 的指標可以被轉換為所需的函數類型。 此方法僅適用於沒有參數或傳回值的函數。需要為調用具有參數或傳回值的函數建立堆疊框架。函數定義應始終以指令開始,以動態分配堆疊框架以支援可變參數函數。有關不同函數類型的更多資訊,請參閱[此處](https://docs.google.com/document/d/1bMwCey-gmqZVTpRax-ESeVuZGmjwbocYs1iHplK-cjo/pub)。 如果您希望我寫關於在 Golang 中產生更複雜的函數的資訊,請在下面評論。## 使函數可執行上述函數不會實際運行。這是因為 Golang 將所有資料結構儲存在二進位檔案的資料部分。本節中的資料設定了[No-Execute](https://en.wikipedia.org/wiki/NX_bit)標誌,阻止其執行。 printFunction slice 中的資料需要儲存在一段可執行檔記憶體中。這可以通過刪除 printFunction slice 上的 No-Execute 標誌或將其複製到可執行檔記憶體位置來實現。 在下面的代碼中,資料已被複製到一個新分配的可執行記憶體(使用 mmap)。這種方法比較好,因為只在整個頁面上設定不執行標誌 - 很容易使資料部分的其他部分無法執行。```goexecutablePrintFunc, err := syscall.Mmap(-1,0,128, syscall.PROT_READ | syscall.PROT_WRITE | syscall.PROT_EXEC, syscall.MAP_PRIVATE|syscall.MAP_ANONYMOUS)if err != nil {fmt.Printf("mmap err: %v", err)}j := 0for i := range printFunction {executablePrintFunc[j] = byte(printFunction[i] >> 8)executablePrintFunc[j+1] = byte(printFunction[i])j = j + 2}```標誌 syscall.PROT_EXEC 確保新分配的記憶體位址是可執行檔。將此資料結構轉換為函數將使其運行平穩。以下是完整的代碼,嘗試在x64機器上運行。```gopackage mainimport ("fmt""syscall""unsafe")type printFunc func()func main() {printFunction := []uint16{0x48c7, 0xc001, 0x0, // mov %rax,$0x10x48, 0xc7c7, 0x100, 0x0, // mov %rdi,$0x10x48c7, 0xc20c, 0x0, // mov 0x13, %rdx0x48, 0x8d35, 0x400, 0x0, // lea 0x4(%rip), %rsi0xf05, // syscall0xc3cc, // ret0x4865, 0x6c6c, 0x6f20, // Hello_(whitespace)0x576f, 0x726c, 0x6421, 0xa, // World!}executablePrintFunc, err := syscall.Mmap(-1,0,128,syscall.PROT_READ|syscall.PROT_WRITE|syscall.PROT_EXEC,syscall.MAP_PRIVATE|syscall.MAP_ANONYMOUS)if err != nil {fmt.Printf("mmap err: %v", err)}j := 0for i := range printFunction {executablePrintFunc[j] = byte(printFunction[i] >> 8)executablePrintFunc[j+1] = byte(printFunction[i])j = j + 2}type printFunc func()unsafePrintFunc := (uintptr)(unsafe.Pointer(&executablePrintFunc))printer := *(*printFunc)(unsafe.Pointer(&unsafePrintFunc))printer()}```## 結論嘗試以上原始碼。敬請期待 Golang 的深入探索!
via: https://medium.com/kokster/writing-a-jit-compiler-in-golang-964b61295f
作者:Sidhartha Mani 譯者:jiangwei161002010 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
935 次點擊