很多人認為 monkey 補丁只能在動態語言,比如 Ruby 和 Python 中才存在。但是,這並不對。因為電腦只是很笨的機器,我們總能讓它做我們想讓它做的事兒!讓我們看看 Go 中的函數是怎麼工作的,並且,我們如何在運行時修改它們。本文會用到大量的 Intel 彙編,所以,我假設你可以讀彙編代碼,或者在讀本文時正拿著[參考手冊](https://software.intel.com/en-us/articles/introduction-to-x64-assembly).**如果你對 monkey 補丁是怎麼工作的不感興趣,你只是想使用它的話,你可以在[這裡](https://github.com/bouk/monkey)找到對應的庫檔案**讓我們看看一下代碼產生的彙編碼:```gopackage mainfunc a() int { return 1 }func main() { print(a())}```> example1.go 由 GitHub 託管 [查看源檔案](https://gist.github.com/bouk/17262666fae75dd24a25/raw/712ae5ef5b1becf4f782d96ca0be0d67ccdcf061/example1.go)上述代碼應該用 go build -gcflags=-l 來編譯,以避免內聯。在本文中我假設你的電腦架構是 64 位元,並且你使用的是一個基於unix 的作業系統比如 Mac OSX 或者某個 Linux 系統。當代碼編譯後,我們用 [Hopper](http://hopperapp.com/) 來查看,可以看到如上代碼會產生如下彙編代碼:![image](https://raw.githubusercontent.com/studygolang/gctt-images/master/monkey-patch/hopper-1.png)我將引用螢幕左側顯示的各種指令的地址。我們的代碼從 main.main 過程開始,從 0x2010 到 0x2026 的指令構建堆棧。你可以在[這兒](http://dave.cheney.net/2013/06/02/why-is-a-goroutines-stack-infinite)獲得更多的相關知識,本文後續的篇幅裡,我將忽略這部分代碼。0x202a 行是調用 0x2000 行的 main.a 函數,這個函數只是簡單的將 0x1 壓入堆棧然後就返回了。0x202f 到 0x2037這幾行 將這個值傳遞給 runtime.printint.足夠簡單!現在讓我們看看在 Go 語言中 函數的值是怎麼實現的。## Go 語言中的函數值是如何工作的看看下面的代碼:```gopackage mainimport ( "fmt" "unsafe")func a() int { return 1 }func main() { f := a fmt.Printf("0x%x\n", *(*uintptr)(unsafe.Pointer(&f)))}```> funcval.go 由 GitHub 託管 [查看源檔案](https://gist.github.com/bouk/c921c3627ddbaae05356/raw/4c18dbaa7cfeb06b74007b65649d85f65384841a/funcval.go)我在第11行 將 a 賦值給 f,這意味者,執行 f() 就會調用 a。然後我使用 Go 中的 [unsafe](http://golang.org/pkg/unsafe/) 包來直接讀出 f 中儲存的值。如果你是有 C 語言的開發背景 ,你可以會覺得 f 就是一個簡單的函數指標,並且這段代碼會輸出 0x2000 (我們在上面看到的 main.a 的地址)。當我在我的機器上運行時,我得到的是 0x102c38, 這個地址甚至與我們的代碼都不挨著!當反編譯時間,這就是上面第11行所對應的:![image](https://raw.githubusercontent.com/studygolang/gctt-images/master/monkey-patch/hopper-2.png)這裡提到了一個 main.a.f,當我們查它的地址,我們可以看到這個:![image](https://raw.githubusercontent.com/studygolang/gctt-images/master/monkey-patch/hopper-3.png)啊哈!main.a.f 的地址是 0x102c38,並且儲存的值是 0x2000,而這個正是 main.a 的地址。看起來 f 並不是一個函數指標,而是一個指向函數指標的指標。讓我們修改一下代碼,以消除其中的偏差。```gopackage mainimport ( "fmt" "unsafe")func a() int { return 1 }func main() { f := a fmt.Printf("0x%x\n", **(**uintptr)(unsafe.Pointer(&f)))}```> funcval2.go 由GitHub託管 [查看源檔案](https://gist.github.com/bouk/c470c4d80ae80d7b30af/raw/d8bd9cd2b80cad288993d5e8f67b115440c6c2a3/funcval2.go)現在輸出的正是預期中的 0x2000。我們可以在[這裡](https://github.com/golang/go/blob/e9d9d0befc634f6e9f906b5ef7476fbd7ebd25e3/src/runtime/runtime2.go#L75-L78)找到一點為什麼代碼要這樣寫的線索。在 Go 語言中函數值可以包含額外的資訊,閉包和綁定執行個體方法藉此實現的。讓我們看看調用一個函數值是怎麼工作的。我把上面的代碼修改一下,在給 f 賦值後直接調用它。```gopackage mainfunc a() int { return 1 }func main() {f := af()}```> callfuncval.go 由 GitHub 託管 [查看源檔案](https://gist.github.com/bouk/58bba533fb3b742ed964/raw/41821274ea8684f7b4c59e81dcc9df6c869c5bfd/callfuncval.go)當我們反編譯後我們可以看到:![image](https://raw.githubusercontent.com/studygolang/gctt-images/master/monkey-patch/hopper-4.png)main.a.f 的地址被載入到 rdx,然後無論 rdx 指向啥都會被載入到 rbx 中,然後 rbx 會被調用。函數的地址都會被首先載入到 rdx 中,然後被調用的函數可以用來載入一些額外的可能用到的資訊。對綁定執行個體方法和匿名閉包函數來說,額外的資訊就是一個指向執行個體的指標。如果你希望瞭解更多,我建議你用反編譯器自己深入研究下。讓我們用剛學到的知識在 Go 中實現 monkey 補丁。## 運行期替換一個函數我們希望做到的是,讓下面的代碼輸出 2:```gopackage mainfunc a() int { return 1 }func b() int { return 2 }func main() {replace(a, b)print(a())}```> replace.go 由GitHub託管 [查看源檔案](https://gist.github.com/bouk/713f3df2115e1b5e554d/raw/65335f4e7d9d0e11a5f72e78d617ec51249c577b/replace.go)現在我們該怎麼實現這種替換?我們需要修改函數 a 跳到 b 的代碼,而不是執行它自己的函數體。本質上,我們需要這麼替換,把 b 的函數值載入到 rdx 然後跳轉到 rdx 所指向的地址。```mov rdx, main.b.f ; 48 C7 C2 ?? ?? ?? ??jmp [rdx] ; FF 22```> replacement.asm 由 GitHub 託管 [查看源檔案](https://gist.github.com/bouk/713f3df2115e1b5e554d/raw/65335f4e7d9d0e11a5f72e78d617ec51249c577b/replace.go)我將上述代碼編譯後產生的對應的機器碼列出來了(用線上編譯器,比如[這個](https://defuse.ca/online-x86-assembler.htm),你可以隨意嘗試編譯)。很明顯,我們需要寫一個能產生這樣機器碼的函數,它應該看起來像這樣:```gofunc assembleJump(f func() int) []byte { funcVal := *(*uintptr)(unsafe.Pointer(&f)) return []byte{ 0x48, 0xC7, 0xC2, byte(funcval >> 0), byte(funcval >> 8), byte(funcval >> 16), byte(funcval >> 24), // MOV rdx, funcVal 0xFF, 0x22, // JMP [rdx] }}```> assemble_jump.go 由 GitHub 託管 [查看源檔案](https://gist.github.com/bouk/4ed563abdcd06fc45fa0/raw/fa9c65c2d5828592e846e28136871ee0bd13e5a9/assemble_jump.go)現在萬事俱備,我們已經準備好將 a 的函數體替換為從 a 跳轉到 b了!下述代碼嘗試直接將機器碼拷貝到函數體中。```gopackage mainimport ("syscall""unsafe")func a() int { return 1 }func b() int { return 2 }func rawMemoryAccess(b uintptr) []byte {return (*(*[0xFF]byte)(unsafe.Pointer(b)))[:]}func assembleJump(f func() int) []byte {funcVal := *(*uintptr)(unsafe.Pointer(&f))return []byte{0x48, 0xC7, 0xC2,byte(funcVal >> 0),byte(funcVal >> 8),byte(funcVal >> 16),byte(funcVal >> 24), // MOV rdx, funcVal0xFF, 0x22, // JMP [rdx]}}func replace(orig, replacement func() int) {bytes := assembleJump(replacement)functionLocation := **(**uintptr)(unsafe.Pointer(&orig))window := rawMemoryAccess(functionLocation)copy(window, bytes)}func main() {replace(a, b)print(a())}```> patch_attempt.go 由 GitHub 託管 [查看源檔案](https://gist.github.com/bouk/4ed563abdcd06fc45fa0/raw/fa9c65c2d5828592e846e28136871ee0bd13e5a9/assemble_jump.go)然而,運行上述代碼並沒有達到我們的目的,實際上,它會產生一個段錯誤。這是因為[預設情況](https://en.wikipedia.org/wiki/Segmentation_fault#Writing_to_read-only_memory)下,已經載入的二進位代碼是不可寫的。我們可以用 mprotect 系統調用來取消這個保護,並且這個最終版本的代碼就像我們期望的一樣,把函數 a 替換成了 b,然後 '2' 被列印出來。```gopackage mainimport ("syscall""unsafe")func a() int { return 1 }func b() int { return 2 }func getPage(p uintptr) []byte {return (*(*[0xFFFFFF]byte)(unsafe.Pointer(p & ^uintptr(syscall.Getpagesize()-1))))[:syscall.Getpagesize()]}func rawMemoryAccess(b uintptr) []byte {return (*(*[0xFF]byte)(unsafe.Pointer(b)))[:]}func assembleJump(f func() int) []byte {funcVal := *(*uintptr)(unsafe.Pointer(&f))return []byte{0x48, 0xC7, 0xC2,byte(funcVal >> 0),byte(funcVal >> 8),byte(funcVal >> 16),byte(funcVal >> 24), // MOV rdx, funcVal0xFF, 0x22, // JMP rdx}}func replace(orig, replacement func() int) {bytes := assembleJump(replacement)functionLocation := **(**uintptr)(unsafe.Pointer(&orig))window := rawMemoryAccess(functionLocation)page := getPage(functionLocation)syscall.Mprotect(page, syscall.PROT_READ|syscall.PROT_WRITE|syscall.PROT_EXEC)copy(window, bytes)}func main() {replace(a, b)print(a())}```> patch_success.go 由 GitHub 託管 [查看源檔案](https://gist.github.com/bouk/55900e1d964099368ab0/raw/976376012f9b073417a6cb68960458392b7f7952/patch_success.go)## 封裝成一個很好的庫我將上述程式碼封裝裝為一個[易用的庫](https://github.com/bouk/monkey)。它支援 32 位,取消補丁,以及對執行個體方法進行補丁,並且我在 README 中寫了幾個使用樣本。## 總結有志者事竟成!讓一個程式在運行期修改自身是可能的,這讓我們可以實現一些很酷的事兒,比如 monkey 補丁。我希望你從這邊部落格中有些收穫,而我在寫這篇文章時很開心!
via: https://bou.ke/blog/monkey-patching-in-go/
作者:Bouke 譯者:MoodWu 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
97 次點擊