「GCTT 出品」Go 語言中的 Monkey 補丁

2020-12-23 Go中國

很多人認為 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語言中文網 榮譽推出

相關焦點

  • 「GCTT 出品」關於結構化並發的筆記——Go 語言中有害的聲明語句
    你不能從一個函數中跳出另一個函數,並且返回可以將你從當前函數中取出,但不能再進一步。無論控制流程如何,一個函數在內部起作用,其他函數不必關心。這甚至延伸到 goto 本身。你會發現幾種語言仍然有一些他們稱之為 goto 的語言,比如 C,C#,Golang ......但是它們增加了很多限制。
  • 「GCTT 出品」在 go 中如何調用私有函數(綁定隱藏的標識符)
    2016 年 4 月 28 日名字在 golang 中的重要性和在其他任何一種語言是一樣的。他們甚至含有語義的作用:在一個包的外部某個名字的可見性是由這個名字首字母是否是大寫來決定的。有時為了更好的組織代碼或者在其他包使用某些隱藏的函數時需要克服這種限制。
  • 『GCTT出品』通過 go/parser 理解 Go
    justforfunc 前情提要我們在上一篇文章中使用 go/scanner 找出了標準庫中最常用的標識符。這個標識符就是 v為了能獲取到更有價值的信息,我們只考慮大於等於三個字符的標識符。不出所料,在 Go 中最具代表性的判斷語句 if err != nil {} 中的 err 和 nil 出現的最為頻繁。
  • 「GCTT 出品」對 Go 中長時間運行 io.Reader 和 io.Writer 的操作...
    為了保證 reader 可以在並發環境中安全使用(在這個例子中至關重要),我們可以使用 atomic.AddInt64 作為安全的計數器。// Reader :計數通過它讀取的字節數。小助手我們還添加了一個小助手,它可以給你一個進度上的 go channel 來周期性報告。 你可以開啟一個新的 goroutine 並列印進度,或更新進度,這取決於您的用例。
  • 「寫作專區」付錦霆 「Monkey」 in the Zoo
    「Monkey」 in the Zoo付錦霆One day, Dad said to Jack, 「Let’s go to the zoo!」」Now?」When they had to go home. Dad noticed that Jack was gone.He looked everywhere, but he could not find Jack!
  • 「GoLang編程」Go語言中如何操作Excel表格
    以前都是用python,現在學習Go語言,剛好試試。要操作excel,自然需要找讀寫Excel的Package,前人栽好樹,等我去乘涼。去哪裡找合適的Package呢?Go語言的包在 https://pkg.go.dev/。打開就能搜索。這裡錄入關鍵字xlsx(如果需要讀寫xls則錄入xls也可以)。(技巧哦,找第三方包的方法)
  • 『GCTT 出品』Go 中 defer 的 5 個坑 - 第一部分
    上面只是一個簡單的案例,但同樣的案例也可能發生在真實世界中,所以如果你遇上的話,可以想想是不是掉進了這個坑裡。#2 — 在循環中使用 defer切忌在循環中使用 defer,除非你清楚自己在做什麼,因為它們的執行結果常常會出人意料。
  • Go語言愛好者周刊:第 12 期
    由於微信不允許外鏈,文中連結可以通過文末引用找到,或「閱讀原文」查看。鑑於大部分人可能沒法堅持把英文文章看完,因此,周刊中會儘可能推薦優質的中文文章。優秀的英文文章,我們的 GCTT 組織會進行翻譯。可以使用它來讀取、寫入 XLSX 文件,相比較其他的開源類庫,Excelize 支持操作帶有數據透視表、切片器、圖表與圖片的 Excel 並支持向 Excel 中插入圖片與創建簡單圖表,可應用於各種報表系統中。Excelize 作為 ,現已成為 Go 語言最受歡迎的 Excel 文檔基礎庫。
  • 『GCTT出品』接受 interface 參數,返回 struct 在 go 中意味著什麼
    Go 語言從結構中抽象出接口,這種處理方式會產生嵌入複雜性。遵循你並不需要它軟體設計理念,如果不需要就沒有理由增加複雜性。一個常見的返回接口的理由是讓用戶把注意力放在函數所提供的 API 上。在 Go 中因為隱含實現了接口,所以這並不需要。返回結構的公共函數就成為那個API。
  • 劉以豪《比悲傷》殺青慘遭全劇組惡整 崩潰大唱「Let it go」
    Yes娛樂11月29日綜合報導劉以豪真的慘遇「比悲傷更悲傷的故事!」演悲戲已經夠鬱悶,殺青當天還慘遭全劇組惡整,只能崩潰大唱「Let it go!」在片中生重病不能跟陳意涵在一起的劉以豪,殺青當天最後一場戲要拍「在攝影棚現場病倒」,明明早已拍好殺青,卻被劇組設計重拍,敬業的他再次為戲跌倒在地!讓他意想不到的是,工作人員突然亂入跑到以豪旁邊,半開玩笑地虧跪在地上的他「你腰不好嗎?」,劇組還故意打開下雪機,製造悲慘氛圍,以豪立刻意會到自己被整,還配合演出在飄落白雪中大唱「Let it go!」
  • 「對比Python學習Go」- 高級數據結構下篇
    本篇是「對比 Python 學習 Go」系列的第四篇,本篇文章我們來看下 Go 的高級數據結構,因文章偏長分為兩篇,此為下篇。本系列的其他文章可到 「對比 Python 學習 Go」- 開篇[1] 查看,下面我們開始今天的分享。
  • 『GCTT出品』Go 函數 -- Go 語言新手的帶圖教程
    funcLen(s string)int// 籤名:func(string)intfuncmultiply(n ...float64) []float64// 籤名:func(...float64) []float64Go 語言中的函數是一等公民
  • BERT是否完美,語言模型又是否真正地「理解了語言」呢?
    其中詞法 (Morphology) 規定了詞素如何組成詞 (Word),如"un-deni-able", "go-ing"。句法 (Syntax) 規定了詞如何組成句子 (Sentence)。語言是用來溝通的,因此符號及它們的組合規則都代表著一定的意義 (Meaning)。符號和規則與意義的聯繫是隨意的:中文用「我」表示第一人稱,英文用 "I",日文可以用「わたし」。
  • 《上古捲軸5:天際》官方中文簡繁漢化補丁v1.31
    你們或許會需要Microsoft AppLocale,來對Skse_loader.exe或Skyrim.exe程序的語言修改為簡體。修改以後就能進入遊戲了,否則可能會進遊戲就閃退。正版用戶:1、如果你是主體+3DLC非傳奇版散裝版,請在「Steam」中檢查遊戲版本為最新版「1.9.32」,在使用該漢化補丁。
  • 日語中的「は」和「へ」到底怎麼讀
    你好、午安:こんにちは(kon ni chi wa)早上好:おはようございます(o ha you go za i ma su)一樣是「は」,怎麼一個念「wa」,一個念「ha」呢? 在日文中,當「は」是助詞時發音須改為「wa」、「へ」為助詞時發音為「e」。 (其由來複雜,有機會再跟大家介紹,這裡只要先了解發音即可)例句: 「私は學生です。」
  • monkey是「猴子」,business是「生意」,那麼Monkey business啥意思?
    make a monkey out of me"make a monkey out of me"從字面翻譯好像很難說通,難不成是「造個猴子」?在 monkey around 這個短語裡,monkey 是當動詞用的。我們見到猴子老在東蹦西跳、攀上爬下似乎忙得不亦樂乎,卻又搞不出個名堂來。表示一個人老是做些傻事蠢事無用功,而不是去做一些正經事。他們樂在其中,卻是徒勞無益。不過有時候 monkey around 表示的意思比無聊還要嚴重。
  • 「語言實踐」Go語言文檔自動化之go-swagger
    Go 語言實踐為什麼需要文檔自動化?1 go-swagger依賴包獲取go-swagger中在github的倉庫下的依賴包如下,主要包含可以對語法進行校驗的govalidator,文檔化的標準specification的go-openapi,還有網絡處理的golang.org官網旗下的net和text。
  • 「Go 語言教程」 Go 語言基礎語法
    Go 語言教程最原始的語言,從人類使用的手語,到語音表達;從英語到漢語,以及到計算機能識別的各種程式語言,每個語言都有自己的語法。那麼Go語言也是類似的。Go語言的基礎語法主要由這幾個方面構成,Go語言的關鍵字,Go語言的標識符,行分隔符回車,變量聲明分割的空格,注釋,以及標記。
  • Github免費中文書《Go入門指南》,帶你從零學Go
    作者 | 無聞整理 | Jane出品 | AI科技大本營(ID:rgznai100)《Go 入門指南》是英文書《The Way to Go》的中文翻譯版,目前主要由志願者「無聞」在進行翻譯,並以開源的形式免費分享給有需要的 Go 語言愛好者。
  • Dickies collection exclusive for monkey time 造型特輯公開
    即便目前只是 3 月份,但 Travis Scott x Nike SB Dunk Low 應該也是今年 Dunk 系列中 Top 級別的存在。 父母們對孩子「下起手」來可從來不含糊,恨不得把全世界最好的都給寶貝們,潮爸潮媽的愛更是從穿著打扮上就能體現得淋漓盡致。家有寶貝的注意:屬於孩子們的「童話王國」又更新了。