go-kiss / monkey Goto Github PK
View Code? Open in Web Editor NEWGo语言猴子补丁框架
Home Page: https://taoshu.in/go/monkey/
License: MIT License
Go语言猴子补丁框架
Home Page: https://taoshu.in/go/monkey/
License: MIT License
刚刚睡不着,画了大概10多分钟看了go夜读的
#119 Go monkey patch 的原理及应用
录播,发现都有一个问题对于类似这种情况见: cch123/supermonkey#12
我看了原理以后感觉应该还是有办法能解决的,例如给开始加个汇编的判断,然后强制走回原函数,不过这个判断的值就比较麻烦了,如果引用一个全局变量倒是比较简单,但是这样频繁修改也不是太合理,要是能直接判断出父级方法是指定的方法然后调用原函数就好了。
顺便放一下应用场景: https://blog.zeromake.com/pages/replace-now-offset/
项目中使用了 bou.ke/monkey
,但由于其不再维护需要寻找一个替代方案。
另外,项目代码中大量使用了 PatchInstanceMethod
函数,但是这个函数在 重新支持全局打桩 PR#7 被移除了。
所以想问,能否恢复 PatchInstanceMethod
函数,保持向前兼容,这样其他项目只需要替换引用的包就可以了?
// main.go
package main
type TreeNode struct {
Name string
ID int
Children []*TreeNode
A int
B int
C int
// 注释掉 go test 可以正常运行
A1 int
B2 int
C1 int
}
func first[T any](a, b T) T { return a }
func second[T any](a, b T) T { return b }
// main_test.go
package main
import (
"fmt"
"reflect"
"testing"
"github.com/go-kiss/monkey"
)
func Test_main(t *testing.T) {
t1 := TreeNode{ ID: 1 }
t2 := TreeNode{ ID: 2 }
monkey.Patch(first[TreeNode], second[TreeNode], monkey.OptGeneric)
if reflect.DeepEqual(t2, first(t1, t2)) {
fmt.Println("equal")
} else {
fmt.Println("not equal")
t.Fatalf("not equal")
}
}
使用monkey配合plugin,实现一个简单的热修复功能会发生崩溃,刚开始patching之后一切正常,运行一会以后就会发生错误
另外: agiledragon/gomonkey/#132 存在同样的问题
测试环境:
main.go
package main
import (
"fmt"
"io"
"net/http"
"os"
"plugin"
"reflect"
"sync/atomic"
"time"
"github.com/go-kiss/monkey"
"gocase/gopatch/route"
)
var HotfixVersion = "main"
func IndexHandler() func(http.ResponseWriter, *http.Request) {
//return route.Index
var counter int32
return func(writer http.ResponseWriter, request *http.Request) {
fmt.Fprintln(writer, "plugin handle:", atomic.AddInt32(&counter, 1))
}
}
func main() {
http.HandleFunc("/", route.Index)
// 热修复 route.Index 逻辑
{
p, err := plugin.Open("http_v1.so")
if nil != err {
panic(err)
}
sym, err := p.Lookup("IndexHandler")
if nil != err {
panic(err)
}
fn := sym.(func() func(http.ResponseWriter, *http.Request))
handler := fn()
fmt.Println("patching...", reflect.ValueOf(route.Index).Pointer(), " => ", reflect.ValueOf(handler).Pointer())
monkey.Patch(route.Index, handler, monkey.OptGlobal)
}
// 等3s后开始启动客户端访问接口
time.AfterFunc(time.Second*3, func() {
fmt.Println("start client...")
for i := 0; i < 3; i++ {
go startClient()
}
})
// 启动http服务器
fmt.Println("http server listen...")
err := http.ListenAndServe(":8080", nil)
fmt.Println("http server listen failed: ", err)
}
func startClient() {
for {
resp, err := http.Get("http://127.0.0.1:8080/")
if nil != err {
fmt.Println("http.Get: ", err.Error())
return
}
io.Copy(os.Stdout, resp.Body)
resp.Body.Close()
}
}
route.go
var counter int32
func Index(writer http.ResponseWriter, request *http.Request) {
fmt.Fprintln(writer, "main: hello gopatch/", atomic.AddInt32(&counter, 1))
}
run_http.sh
#!/bin/bash
set -e
echo "build main program..."
go build -gcflags=all=-l -ldflags="-X main.HotfixVersion=main" -o http_main main.go
echo "please modify v1 plugin, press enter key to continue..."
read input
echo "build plugin v1..."
go build -gcflags=all=-l -buildmode=plugin -ldflags="-X main.HotfixVersion=v1" -o http_v1.so main.go
echo "run main program..."
./http_main
输出结果
patching... 8645408 => 140521878991296
http server listen...
start client...
plugin handle: 1
plugin handle: 3
plugin handle: 2
plugin handle: 4
plugin handle: 6
plugin handle: 7
plugin handle: 5
plugin handle: 8
plugin handle: 9
plugin handle: 11
plugin handle: 10
plugin handle: 12
plugin handle: 13
plugin handle: 14
plugin handle: 15
plugin handle: 16
plugin handle: 17
plugin handle: 18
plugin handle: 19
plugin handle: 20
plugin handle: 22
plugin handle: 21
plugin handle: 23
plugin handle: 24
plugin handle: 25
plugin handle: 26
plugin handle: 27
plugin handle: 28
plugin handle: 30
plugin handle: 29
plugin handle: 31
plugin handle: 32
plugin handle: 33
plugin handle: 34
plugin handle: 35
plugin handle: 36
plugin handle: 37
plugin handle: 38
plugin handle: 41
plugin handle: 39
plugin handle: 40
plugin handle: 12346
plugin handle: 12347
plugin handle: 12348
plugin handle: 12349
plugin handle: 12350
plugin handle: 12351
unexpected fault address 0x931cc0
fatal error: fault
[signal SIGSEGV: segmentation violation code=0x2 addr=0x931cc0 pc=0x931cc0]
goroutine 182 [running]:
runtime.throw({0x8e39fd?, 0xc00022a000?})
/usr/local/go/src/runtime/panic.go:1047 +0x5d fp=0xc0004f4a70 sp=0xc0004f4a40 pc=0x5906dd
runtime.sigpanic()
/usr/local/go/src/runtime/signal_unix.go:851 +0x1e5 fp=0xc0004f4aa0 sp=0xc0004f4a70 pc=0x5a7465
net/http.HandlerFunc.ServeHTTP(0x688a65?, {0x934080?, 0xc00058a0e0?}, 0x92ceb8?)
/usr/local/go/src/net/http/server.go:2123 +0x2f fp=0xc0004f4ac8 sp=0xc0004f4aa0 pc=0x80644f
net/http.(*ServeMux).ServeHTTP(0x0?, {0x934080, 0xc00058a0e0}, 0xc0001d2300)
/usr/local/go/src/net/http/server.go:2500 +0xc2 fp=0xc0004f4b00 sp=0xc0004f4ac8 pc=0x8079a2
net/http.serverHandler.ServeHTTP({0xc000482a80?}, {0x934080?, 0xc00058a0e0?}, 0xc0001d2300?)
/usr/local/go/src/net/http/server.go:2936 +0x23c fp=0xc0004f4b98 sp=0xc0004f4b00 pc=0x808fdc
net/http.(*conn).serve(0xc0004847e0, {0x934488, 0xc00011af30})
/usr/local/go/src/net/http/server.go:1995 +0xad0 fp=0xc0004f4fb8 sp=0xc0004f4b98 pc=0x8055b0
net/http.(*Server).Serve.func3()
/usr/local/go/src/net/http/server.go:3089 +0x2e fp=0xc0004f4fe0 sp=0xc0004f4fb8 pc=0x80992e
runtime.goexit()
/usr/local/go/src/runtime/asm_amd64.s:1598 +0x1 fp=0xc0004f4fe8 sp=0xc0004f4fe0 pc=0x5c4861
created by net/http.(*Server).Serve
/usr/local/go/src/net/http/server.go:3089 +0x45f
...
猴子补丁听起来怪怪的
比较官方的叫法应该为猴补丁
使用go1.21.1和go1.20.8编译(编译命令禁止了内联和优化go build -gcflags '-N -l' -o generic-go1.21.1
)运行examples中的generic时报错,使用go1.18.8和go1.19.8编译后执行可以正常输出结果为2。
error:
panic: Can not find CALL instruction
goroutine 1 [running]:
github.com/go-kiss/monkey.getFirstCallFunc(0x487be0)
/home/ruo/go/src/monkey/monkey_amd64.go:118 +0x277
github.com/go-kiss/monkey.patchValue({0x48fd60?, 0x4a9cc8?, 0x1?}, {0x48fe20?, 0x4a9cd0?, 0x48f300?}, 0xc0000a6000)
/home/ruo/go/src/monkey/monkey.go:145 +0x58a
github.com/go-kiss/monkey.Patch({0x48fd60?, 0x4a9cc8?}, {0x48fe20?, 0x4a9cd0?}, {0xc00004a6f0, 0x1, 0x7fb6b70ee5b8?})
/home/ruo/go/src/monkey/monkey.go:54 +0x19e
main.main()
/home/ruo/go/src/monkey/examples/generic/generic.go:26 +0xb2
package main
import (
"fmt"
"github.com/go-kiss/monkey"
)
type S1[T int | float64] struct {
i T
}
func (s *S1[T]) Get() T {
return s.i
}
type S1__monkey__[T int | float64] struct {
S1[T]
}
func (s *S1__monkey__[T]) Get() T {
return s.i + 1
}
func main() {
monkey.Patch((*S1[int]).Get, (*S1__monkey__[int]).Get, monkey.OptGeneric)
s := S1[int]{i: 1}
fmt.Println(s.Get()) // display 2
}
明确了问题出现的场景及看了getFirstCallFunc
的实现后,我用gdb对go1.18.8编译出的generic-go1.18.8和go1.21.1编译出的generic-go1.21.1进行了调试
go1.18.8反编译如下所示:
而在getFirstCallFunc
中的判断逻辑是要求call指令的上一条指令使用RAX寄存器
if i.Op == x86asm.CALL && lastLea.Args[0].(x86asm.Reg) == x86asm.RAX
所以问题应该出现在这里,我将这里的判断逻辑修改为使用RAX或RBX寄存器后可以正常执行了,但是还不确定go1.20进行了什么改动导致的这一问题
=== RUN TestName1
--- FAIL: TestName1 (0.00s)
panic: permission denied [recovered]
panic: permission denied
goroutine 83 [running]:
testing.tRunner.func1.2({0x100ff16a0, 0x1018edb88})
/usr/local/go/src/testing/testing.go:1545 +0x48a
testing.tRunner.func1()
/usr/local/go/src/testing/testing.go:1548 +0x63f
panic({0x100ff16a0?, 0x1018edb88?})
/usr/local/go/src/runtime/panic.go:920 +0x290
github.com/go-kiss/monkey.mprotectCrossPage(0x100ee1c00, 0xd, 0x7)
/Users/amos/go/pkg/mod/github.com/go-kiss/[email protected]/replace_unix.go:16 +0xf9
github.com/go-kiss/monkey.copyToLocation(0x100ee1c00, {0xc00052fe30, 0xd, 0xd})
/Users/amos/go/pkg/mod/github.com/go-kiss/[email protected]/replace_unix.go:27 +0x75
github.com/go-kiss/monkey.(*patch).Apply(0xc00022d090)
/Users/amos/go/pkg/mod/github.com/go-kiss/[email protected]/monkey.go:269 +0x1be
github.com/go-kiss/monkey.patchValue({0x100f92de0, 0x101190540, 0x13}, {0x100f92de0, 0x101190520, 0x13}, 0xc00024f6a0)
/Users/amos/go/pkg/mod/github.com/go-kiss/[email protected]/monkey.go:150 +0x6fc
github.com/go-kiss/monkey.Patch({0x100f92de0, 0x101190540}, {0x100f92de0, 0x101190520}, {0x0, 0x0, 0x0})
/Users/amos/go/pkg/mod/github.com/go-kiss/[email protected]/monkey.go:54 +0x1e5
data-factory/core/manager.TestName1(0xc000501520)
/Users/amos/Code/104-Golang/data-factory/core/manager/import_test.go:24 +0x5f
testing.tRunner(0xc000501520, 0x101190460)
/usr/local/go/src/testing/testing.go:1595 +0x1c2
created by testing.(*T).Run in goroutine 1
/usr/local/go/src/testing/testing.go:1648 +0x7e5
已经添加 -gcflags='all=-N -l' 参数
目前monkey
中getg
函数的实现在windows平台下是
mov r13,QWORD PTR gs:0x28
mov r12,QWORD PTR [r13]
在linux平台下的实现是
mov r12,QWORD PTR fs:0xfffffffffffffff8
我在 https://go.googlesource.com/go/+/refs/heads/dev.regabi/src/cmd/compile/internal-abi.md 和自己的测试中发现go使用r14寄存器
存储当前的goroutine指针,那么为何不能直接获取r14的值来用呢?
我将getg
在windows和linux的实现修改为了
func getg() []byte {
return []byte{
// mov r12, r14
0x4D, 0x89, 0xF4,
}
}
都是可以正常跑通测试的,并不会有问题,那么之前的这种写法是有什么别的考虑吗?如果没有的话用这种方法应该可以统一win/linux(应该也包括intel版mac,但是m1芯片的mac是arm应该不行)的getg
实现,且解决之前的windows平台下panic的问题。
@taoso FYI
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.