函数值 首先查看下面代码:
1 2 3 4 5 6 7 8 func a () int {return 1 }func main () { fmt.Printf("%p\n" , a) fn := a fmt.Printf("0x%x\n" ,*(*uintptr )(unsafe.Pointer(&fn))) fmt.Printf("0x%x\n" , **(**uintptr )(unsafe.Pointer(&fn))) }
根据上面的输出我们可以发现,函数值fn
并没有直接持有函数a
的地址,这是因为Go
的函数值可以包含一些额外的上下文信息 ,这是实现闭包和绑定实例方法的关键,我们可以在源码 中找到点函数值类型的线索:
1 2 3 4 type funcval struct { fn uintptr }
我们代码中的函数变量,实际上应该*funcval
类型
闭包实现原理 接下来,我们探究一下golang
中闭包的实现原理,我们首先写一个简单的闭包demo,然后从编译后的汇编代码来探究其实现
1 2 3 4 5 6 7 8 9 10 11 func main () { f := fn() f() } func fn () func () { var a = 10 return func () { fmt.Println(a) } }
接下来将上面代码编译成汇编:
1 $ go tool compile -S -N main.go > asm.s
下面是生成的汇编,只保留主要的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 "".main STEXT size=72 args=0x0 locals=0x18 0x0000 00000 (demo.go:7) TEXT "".main(SB), $24-0 0x0024 00036 (demo.go:8) CALL "".fn(SB) # 调用fn函数获取 0x0029 00041 (demo.go:8) MOVQ (SP), DX # 保存返回的函数值指针到DX,这里的DX是关键 0x002d 00045 (demo.go:8) MOVQ DX, "".f+8(SP) # 把DX的值赋给局部变量f 0x0032 00050 (demo.go:9) MOVQ (DX), AX # 从上面funcval结构可知,(DX)为实际函数地址,也就是下面的"".fn.func1 0x0035 00053 (demo.go:9) CALL AX # 调用实际的函数 0x0040 00064 (demo.go:10) RET "".fn STEXT size=136 args=0x8 locals=0x28 0x0000 00000 (demo.go:12) TEXT "".fn(SB), $40-8 0x0036 00054 (demo.go:14) LEAQ type.noalg.struct { F uintptr; "".a int }(SB), AX # 这里表示实际funcval的类型 0x003d 00061 (demo.go:14) MOVQ AX, (SP) 0x0041 00065 (demo.go:14) CALL runtime.newobject(SB) # new一个funcval 0x0046 00070 (demo.go:14) MOVQ 8(SP), AX # 返回值 0x0050 00080 (demo.go:14) LEAQ "".fn.func1(SB), CX # 取实际函数地址 0x0057 00087 (demo.go:14) MOVQ CX, (AX) # 保存实际地址 0x0061 00097 (demo.go:14) MOVQ "".a+16(SP), CX # 保存变量a到funcval 0x0066 00102 (demo.go:14) MOVQ CX, 8(AX) 0x006f 00111 (demo.go:14) MOVQ AX, "".~r0+48(SP) # 设置返回值 0x007d 00125 (demo.go:14) RET "".fn.func1 STEXT size=258 args=0x0 locals=0x88 0x0000 00000 (demo.go:14) TEXT "".fn.func1(SB), NEEDCTXT, $136-0 0x0036 00054 (demo.go:14) MOVQ 8(DX), AX # [DX+8]实际上存储的就是闭包引用外部的变量a 0x003a 00058 (demo.go:14) MOVQ AX, "".a+48(SP) # 将AX赋值给变量a 0x003f 00063 (demo.go:15) MOVQ AX, ""..autotmp_2+56(SP) 0x0056 00086 (demo.go:15) LEAQ type.int(SB), AX # fmt.Println函数实际接收的是[]interface{},这里需要先将a转换成interface{}类型 0x005d 00093 (demo.go:15) MOVQ AX, (SP) 0x0061 00097 (demo.go:15) MOVQ ""..autotmp_2+56(SP), AX 0x0066 00102 (demo.go:15) MOVQ AX, 8(SP) 0x006b 00107 (demo.go:15) CALL runtime.convT2E64(SB)
从上面我们可以看到,go
的闭包是通过funcval
携带额外的上下文信息来实现的。
当创建闭包函数时,将被闭包捕获的变量的地址保存到funcval
,当调用闭包函数时,会将funcval
的地址保存到DX
寄存器,执行闭包函数时,可以通过DX
寄存器来访问这些变量。
实现猴子补丁 现在,我们要在go
中实现猴子补丁,所想要实现的效果是:
1 2 3 4 5 6 7 8 9 10 11 12 func a () { fmt.Println("run a" ) } func b () { fmt.Println("run b" ) } func main () { a() replace(a, b) a() }
我们要在replace
方法中,将对函数a
的调用替换成对函数b
的调用。
具体实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 func replace (a, b func () ) { replaceFunction(**(**uintptr )(unsafe.Pointer(&a)), *(*uintptr )(unsafe.Pointer(&b))) } func replaceFunction (from, to uintptr ) { if unsafe.Sizeof(uintptr (1 )) != 8 { panic ("only support amd64" ) } jumpData := jmpToFunctionValue(to) copyToLocation(from, jumpData) return } func jmpToFunctionValue (to uintptr ) []byte { return []byte { 0x48 , 0xBA , byte (to), byte (to >> 8 ), byte (to >> 16 ), byte (to >> 24 ), byte (to >> 32 ), byte (to >> 40 ), byte (to >> 48 ), byte (to >> 56 ), 0xFF , 0x22 , } } func copyToLocation (location uintptr , data []byte ) { f := rawMemoryAccess(location, len (data)) mprotectCrossPage(location, len (data), syscall.PROT_READ|syscall.PROT_WRITE|syscall.PROT_EXEC) copy (f, data[:]) mprotectCrossPage(location, len (data), syscall.PROT_READ|syscall.PROT_EXEC) } func rawMemoryAccess (p uintptr , length int ) []byte { return *(*[]byte )(unsafe.Pointer(&reflect.SliceHeader{ Data: p, Len: length, Cap: length, })) } func mprotectCrossPage (addr uintptr , length int , prot int ) { pageSize := syscall.Getpagesize() for p := pageStart(addr); p < addr+uintptr (length); p += uintptr (pageSize) { page := rawMemoryAccess(p, pageSize) err := syscall.Mprotect(page, prot) if err != nil { panic (err) } } } func pageStart (ptr uintptr ) uintptr { return ptr & ^(uintptr (syscall.Getpagesize() - 1 )) }
当执行replace(a,b)
时,会动态将函数a
的指令替换成movabs rdx,to; jmp QWORD PTR [rdx]
之后调用函数a
时,会执行call
指令,这时候会把传递给函数a
的参数保存到栈上,并且将返回地址保存到指定的寄存器RA
中;
因为函数a
被替换成上诉两条指令,因此会跳转到函数b
执行,这时候函数b
可以直接使用栈上的参数(这就要求两个函数要有相同的函数签名);因为morestack
操作是在函数开始执行的时候进行检查的,因此不会有栈溢出的问题。
当函数b
执行完成时,会执行ret
指令,这时候会把返回值保存到栈上,同时将RA
中的返回地址弹出到PC
寄存器中;
对于函数调用者来说,整个过程是透明的。
refer monkey patching in Go
bouk/monkey