TLDR; 使用 supermonkey[1] 可以 patch 任意导出/非导出函数。

目前在 Go 语言里写测试还是比较麻烦的。

除了传统的 test double,也可以通过把一个现成的对象的成员方法 Patch 掉,以达成测试执行时的特殊目的。

举个例子,我的业务逻辑是从远端获取一段数据,在测试环节没有网络,所以我需要把和网络交互的环节 mock 掉:

func LoadConfig() string {jsonBytes, err := redis.Get("xxxx")return string(jsonBytes)
}

这里的 redis.Get 中有网络操作,写测试时,我们的目的是为了验证 Get 之后的逻辑是否正常,所以我们可以把这个 Get 替换为直接返回内容,不走网络,社区中有 monkey patch 来达成这个目的:

monkey.Patch(redis.Get, func(input string) ([]byte, error) {return []byte("{"key" : 12345}"), nil
})

Patch 之后,redis.Get 就会按照我们替换之后的函数来执行了,还是比较方便的。

monkey patch 的基本原理不复杂,就是把进程中 .text 段中的代码(你可以理解成 byte 数组)替换为用户提供的替换函数。

patchvalue

读取 target 的地址使用了 reflect.ValueOf(funcVal).Pointer() 获取函数的虚拟地址,然后把替换函数的内容以 []byte 的形式覆盖进去。

一方面是因为 reflect 本身没有办法读取非导出函数,一方面是从 Go 的语法上来讲,我们没法在包外部以字面量对非导出函数进行引用。所以目前开源的 monkey patch 是没有办法 patch 那些非导出函数的。

如果我们想要 patch 那些非导出函数,理论上并不需要对这个函数进行引用,只要能找到这个函数的虚拟地址就可以了,在这里提供一个思路,可以使用 nm 来找到我们想要 patch 的函数地址:

NM(1)  GNU Development Tools  NM(1)NAMEnm - list symbols from object files

nm 可以查看一个二进制文件中的所有符号的名字、虚拟地址、大小。还是举个例子:

$cat hello.go
package mainfunc say() {println("yyyy")
}func main() {say()
}

build 需要带 -l 的 gcflags,防止内联优化:

go build -gcflags="-l" hello.go

用 nm 找找这个 say 的地址:

$nm hello | grep main
000000000044e3f0 T main
0000000000401070 T main.init
00000000004d5620 B main.initdone.
0000000000401050 T main.main
0000000000401000 T main.say ------> 这里
0000000000423620 T runtime.main
0000000000488c78 R runtime.main.f
0000000000442740 T runtime.main.func1
0000000000488c60 R runtime.main.func1.f
0000000000442780 T runtime.main.func2
0000000000488c68 R runtime.main.func2.f
00000000004b1e70 B runtime.main_init_done
0000000000488c70 R runtime.mainPC

有了虚拟地址,也就有了拷贝的 target。

在 monkey 代码的基础上,再结合 nm 命令得到的符号地址,组合一下就是下面这样的 demo:

package mainimport ("os""os/exec""reflect""strconv""strings""syscall""unsafe"
)//go:noinline
func HeiHeiHei() {println("hei")
}//go:noinline
func heiheiPrivate() {println("oh no")
}func Replace() {println("fake")
}func generateFuncName2PtrDict() map[string]uintptr {fileFullPath := os.Args[0]cmd := exec.Command("nm", fileFullPath)contentBytes, err := cmd.Output()if err != nil {println(err)return nil}var result = map[string]uintptr{}content := string(contentBytes)lines := strings.Split(content, "\n")for _, line := range lines {arr := strings.Split(line, " ")if len(arr) < 3 {continue}funcSymbol, addr := arr[2], arr[0]addrUint, _ := strconv.ParseUint(addr, 16, 64)result[funcSymbol] = uintptr(addrUint)}return result
}func main() {m := generateFuncName2PtrDict()heiheiPrivate()replaceFunction(m["_main.heiheiPrivate"], (uintptr)(getPtr(reflect.ValueOf(Replace))))heiheiPrivate()
}type value struct {_   uintptrptr unsafe.Pointer
}func getPtr(v reflect.Value) unsafe.Pointer {return (*value)(unsafe.Pointer(&v)).ptr
}// from is a pointer to the actual function
// to is a pointer to a go funcvalue
func replaceFunction(from, to uintptr) (original []byte) {jumpData := jmpToFunctionValue(to)f := rawMemoryAccess(from, len(jumpData))original = make([]byte, len(f))copy(original, f)copyToLocation(from, jumpData)return
}// Assembles a jump to a function value
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), // movabs rdx,to0xFF, 0x22,     // jmp QWORD PTR [rdx]}
}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)}}
}// this function is super unsafe
// aww yeah
// It copies a slice to a raw memory location, disabling all memory protection before doing so.
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 pageStart(ptr uintptr) uintptr {return ptr & ^(uintptr(syscall.Getpagesize() - 1))
}

go run -gcflags="-l" yourfile.go

上面的 demo 不跨平台,建议还是直接试试开头说的 lib 中的 example。

该思路已被封装至 https://github.com/cch123/supermonkey 中。

参考资料

[1]

supermonkey: https://github.com/cch123/supermonkey

在 Go 语言中 Patch 非导出函数相关推荐

  1. C 语言中 char[] 的操作函数

    C语言中char[]的操作函数 1.赋值操作 在C语言中,char型数组是不可以直接赋值的.例如在如下的代码中,会得到错误: char c1[20] = "women"; char ...

  2. c语言printf函数的作用,c语言中printf用法及其函数定义

    c语言中printf用法及其函数定义 发布时间:2020-04-09 10:51:14 来源:亿速云 阅读:354 作者:小新 今天小编给大家分享的是c语言中printf用法及其函数定义,很多人都不太 ...

  3. c语言中val是什么函数,val(val是什么意思)

    有这么一个C语言程序 : int i; long val; for(i=0;i 在C语言中Val是一个将由数字符号(包括正负号.小数点)组成的字符型数据转换成相应的数值型数据的函数,语法格式是Val( ...

  4. c语言中fmod()函数和log10()函数用法

    C语言中fmod函数的功能是x/y的求余运算,适用于double ,float,long double,如果y=0的话,那么返回 值得一提的是%,'%'同样是取余,但是% 适用于整数取余,%是整数的取 ...

  5. c语言getchar函数的作用,c语言中getchar的用法函数用法

    当程序调用getchar时.程序就等着用户按键.用户输入的字符被存放在键盘缓冲区中.直到用户按回车为止.下面小编就跟你们详细介绍下c语言中getchar的用法,希望对你们有用. c语言中getchar ...

  6. c语言中tgx是什么函数,《高等数学》课后练习题

    第1章 函数.极限与连续 1.已知函数2,02 ()2,24 x f x x ≤≤?=? - 2.设函数()y f x =的定义域是[]0,8,试求3()f x 的定义域. 3.已知函数[]()12f ...

  7. C语言中itoa和atoi函数的用法

    1.itoa函数的用法 (1) 函数说明 itoa是广泛应用的非标准C语言扩展函数.由于它不是标准C语言函数,所以不能在所有的编译器中使用.但是,大多数的编译器(如Windows上 的)通常在< ...

  8. c语言中开yroot的函数,C语言中sqrt是什么意思

    满意答案 jinzhaoning 2013.06.02 采纳率:41%    等级:12 已帮助:12860人 sqrtabbr. 开平方根(square root) 其-他-释-义: 平方,根 开根 ...

  9. c语言中mul是什么函数,mul函数的具体用法

    匿名用户 1级 2010-12-26 回答 PHP基础 Author:陈庆平 (Andych) E-mail:ahut9923@126.com 一.PHP入门 二.PHP变量 1.php变量的命名 变 ...

最新文章

  1. NGUI的技能冷却实现
  2. 用Leangoo看板工具做办公室采购流程管理
  3. java核心技术面试精讲
  4. HTML知识点梳理1
  5. 点评互联网创业的“南派”和“北派”
  6. c++怎么打印出句子中的各个单词_小学英语单词汇总篇 身体 食品、饮料 蔬菜...
  7. 【微软之--起源】(转载自腾讯科技)
  8. 因式分解题目及过程_两道新定义题目的对比分析
  9. c语言随机数猜数游戏
  10. 计算机论文中期报告进展情况,自动化毕业论文中期报告进展情况怎么写
  11. java中国象棋棋子走法,JS 中国象棋(1):校验棋子走法
  12. 计算机 去掉快捷方式箭头,去掉桌面快捷方式小箭头方法(无需修改注册表)
  13. XXE(外部实体注入)| PortSwigger(burpsuite官方靶场)| Part 3
  14. VirtualBox虚拟机使用Vagrant连接win(甲骨文Oracle VM )
  15. D. Deleting Divisors
  16. Python 汉字转化成拼音
  17. tws蓝牙耳机p10双耳连接方法
  18. input输入框获取到焦点删除黑色框 css
  19. 加密软件的新品类:环境加密
  20. ASP.NET二手商品交易系统VS开发sqlserver数据库web结构c#编程计算机网页目

热门文章

  1. python抓取网站访客手机号_点击了一个教育网站,马上就有老师打电话过来,他们是怎么获取我的手机号?...
  2. python寻找列表最大值最小值及其下标
  3. 210312阶段三通过sqlite3源码安装sqlite3
  4. 200806C阶段一结构体
  5. Servlet的第一个程序HelloWorld
  6. 自然语言处理hanlp的入门基础
  7. **16.app后端如何保证通讯安全--url签名
  8. == 捕获对象时的模式切换 ==
  9. c#操作Xml(四)
  10. HDU - 6582 Path(最短路+最大流)