很多人認為 monkey 補丁只能在動態語言,比如 Ruby 和 Python 中才存在。但是,這並不對。因為計算機只是很笨的機器,我們總能讓它做我們想讓它做的事兒!讓我們看看 Go 中的函數是怎麼工作的,並且,我們如何在運行時修改它們。本文會用到大量的 Intel 彙編,所以,我假設你可以讀彙編代碼,或者在讀本文時正拿著參考手冊.
如果你對 monkey 補丁是怎麼工作的不感興趣,你只是想使用它的話,你可以在這裡找到對應的庫文件
讓我們看看一下代碼產生的彙編碼:
packagemainfunca() int { return1 }funcmain() {print(a())}example1.go 由 GitHub 託管 查看源文件
上述代碼應該用 go build -gcflags=-l 來編譯,以避免內聯。在本文中我假設你的電腦架構是 64 位,並且你使用的是一個基於unix 的作業系統比如 Mac OSX 或者某個 Linux 系統。
當代碼編譯後,我們用 Hopper 來查看,可以看到如上代碼會產生如下彙編代碼:
我將引用屏幕左側顯示的各種指令的地址。
我們的代碼從 main.main 過程開始,從 0x2010 到 0x2026 的指令構建堆棧。你可以在這兒獲得更多的相關知識,本文後續的篇幅裡,我將忽略這部分代碼。
0x202a 行是調用 0x2000 行的 main.a 函數,這個函數只是簡單的將 0x1 壓入堆棧然後就返回了。0x202f 到 0x2037這幾行 將這個值傳遞給 runtime.printint.
足夠簡單!現在讓我們看看在 Go 語言中 函數的值是怎麼實現的。
Go 語言中的函數值是如何工作的
看看下面的代碼:
packagemainimport ("fmt""unsafe")funca() int { return1 }funcmain() {f :=afmt.Printf("0x%x\n", *(*uintptr)(unsafe.Pointer(&f)))}funcval.go 由 GitHub 託管 查看源文件
我在第11行 將 a 賦值給 f,這意味者,執行 f() 就會調用 a。然後我使用 Go 中的 unsafe 包來直接讀出 f 中存儲的值。如果你是有 C 語言的開發背景 ,你可以會覺得 f 就是一個簡單的函數指針,並且這段代碼會輸出 0x2000 (我們在上面看到的 main.a 的地址)。當我在我的機器上運行時,我得到的是 0x102c38, 這個地址甚至與我們的代碼都不挨著!當反編譯時,這就是上面第11行所對應的:
這裡提到了一個 main.a.f,當我們查它的地址,我們可以看到這個:
啊哈!main.a.f 的地址是 0x102c38,並且保存的值是 0x2000,而這個正是 main.a 的地址。看起來 f 並不是一個函數指針,而是一個指向函數指針的指針。讓我們修改一下代碼,以消除其中的偏差。
packagemainimport ("fmt""unsafe")funca() int { return1 }funcmain() {f :=afmt.Printf("0x%x\n", **(**uintptr)(unsafe.Pointer(&f)))}funcval2.go 由GitHub託管 查看源文件
現在輸出的正是預期中的 0x2000。我們可以在這裡找到一點為什麼代碼要這樣寫的線索。在 Go 語言中函數值可以包含額外的信息,閉包和綁定實例方法藉此實現的。
讓我們看看調用一個函數值是怎麼工作的。我把上面的代碼修改一下,在給 f 賦值後直接調用它。
packagemainfunca() int { return1 }funcmain() {f :=af()}callfuncval.go 由 GitHub 託管 查看源文件
當我們反編譯後我們可以看到:
main.a.f 的地址被加載到 rdx,然後無論 rdx 指向啥都會被加載到 rbx 中,然後 rbx 會被調用。函數的地址都會被首先加載到 rdx 中,然後被調用的函數可以用來加載一些額外的可能用到的信息。對綁定實例方法和匿名閉包函數來說,額外的信息就是一個指向實例的指針。如果你希望了解更多,我建議你用反編譯器自己深入研究下。
讓我們用剛學到的知識在 Go 中實現 monkey 補丁。
運行期替換一個函數
我們希望做到的是,讓下面的代碼輸出 2:
packagemainfunca() int { return1 }funcb() int { return2 }funcmain() {replace(a, b)print(a())}replace.go 由GitHub託管 查看源文件
現在我們該怎麼實現這種替換?我們需要修改函數 a 跳到 b 的代碼,而不是執行它自己的函數體。本質上,我們需要這麼替換,把 b 的函數值加載到 rdx 然後跳轉到 rdx 所指向的地址。
mov rdx, main.b.f ; 48 C7 C2 ?? ?? ?? ??jmp [rdx] ; FF 22replacement.asm 由 GitHub 託管 查看源文件
我將上述代碼編譯後產生的對應的機器碼列出來了(用在線編譯器,比如這個,你可以隨意嘗試編譯)。很明顯,我們需要寫一個能產生這樣機器碼的函數,它應該看起來像這樣:
funcassembleJump(ffunc() 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] }}assemble_jump.go 由 GitHub 託管 查看源文件
現在萬事俱備,我們已經準備好將 a 的函數體替換為從 a 跳轉到 b了!下述代碼嘗試直接將機器碼拷貝到函數體中。
packagemainimport ("syscall""unsafe")funca() int { return1 }funcb() int { return2 }funcrawMemoryAccess(buintptr) []byte {return (*(*[0xFF]byte)(unsafe.Pointer(b)))[:]}funcassembleJump(ffunc() 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] }}funcreplace(orig, replacementfunc() int) {bytes :=assembleJump(replacement)functionLocation :=**(**uintptr)(unsafe.Pointer(&orig))window :=rawMemoryAccess(functionLocation)copy(window, bytes)}funcmain() {replace(a, b)print(a())}patch_attempt.go 由 GitHub 託管 查看源文件
然而,運行上述代碼並沒有達到我們的目的,實際上,它會產生一個段錯誤。這是因為默認情況下,已經加載的二進位代碼是不可寫的。我們可以用 mprotect 系統調用來取消這個保護,並且這個最終版本的代碼就像我們期望的一樣,把函數 a 替換成了 b,然後 '2' 被列印出來。
packagemainimport ("syscall""unsafe")funca() int { return1 }funcb() int { return2 }funcgetPage(puintptr) []byte {return (*(*[0xFFFFFF]byte)(unsafe.Pointer(p&^uintptr(syscall.Getpagesize()-1))))[:syscall.Getpagesize()]}funcrawMemoryAccess(buintptr) []byte {return (*(*[0xFF]byte)(unsafe.Pointer(b)))[:]}funcassembleJump(ffunc() 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 }}funcreplace(orig, replacementfunc() 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)}funcmain() {replace(a, b)print(a())}patch_success.go 由 GitHub 託管 查看源文件
包裝成一個很好的庫
我將上述代碼包裝為一個易用的庫。它支持 32 位,取消補丁,以及對實例方法進行補丁,並且我在 README 中寫了幾個使用示例。
總結
有志者事竟成!讓一個程序在運行期修改自身是可能的,這讓我們可以實現一些很酷的事兒,比如 monkey 補丁。
我希望你從這邊博客中有些收穫,而我在寫這篇文章時很開心!
via: https://bou.ke/blog/monkey-patching-in-go/
作者:Bouke 譯者:MoodWu 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出