前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >在 Go 语言中使用猴子补丁

在 Go 语言中使用猴子补丁

作者头像
李海彬
发布2018-08-16 15:44:40
1K0
发布2018-08-16 15:44:40
举报
文章被收录于专栏:Golang语言社区

最近写单元测试多亏了这个 monkey 包,昨天看到了官方的原理介绍,很受启发,翻译出来大伙一起看看。

  • Go 语言中函数值如何工作
  • 运行时替换函数
  • 封装到库中
  • 结论

很多人认为猴子补丁(A monkey patch is a way for a program to extend or modify supporting system software locally (affecting only the running instance of the program). 指可以在运行时动态修改或扩展程序的一种方法)是那些东西语言,比如 Ruby 和 Python 才有的东西。这并不对,计算机知识愚蠢的机器而我们总能让他们按照我们的想法工作!让我们来看看 Go 的函数如何工作,再看看我们如何在运行时修改它们。这篇文章将会使用 Intel 的汇编语法,所以我假设你了解过它或者在阅读的过程中参考官方文档。

如果你对猴子补丁的原理没有兴趣,只想使用猴子补丁,可以直接移步到代码仓库。

看看下面的代码反编译之后的结果:

代码语言:javascript
复制
1 package mainfunc a() int { return 1 }func main() {
2  print(a())}

编译完成后通过Hopper查看,上面的代码将会展示下面的汇编代码:

我将参考屏幕左侧显示的各种指令的地址。

我们的代码从过程main.main开始,指令 0x2010 到 0x2026 初始化了栈。你可以参考这些扩展阅读,下面的文章将会忽略那些代码。

0x202a 行调用了函数main.a,0x2000 行简单得把 0x1 压入栈返回。0x202f 到 0x2037 行把值传给了runtime.printint

够简单了!现在咱们一起看看 Go 里面的函数值是如何实现的。

Go 语言中函数值如何工作

看下面的代码:

代码语言:javascript
复制
1 package mainimport (
2  "fmt"
3  "unsafe")func a() int { return 1 }func main() {
4  f := a
5  fmt.Printf("0x%x\n", *(*uintptr)(unsafe.Pointer(&f)))}

在第11行把a赋值给了f,这就意味着调用f()将会调用a。接下来用unsafe包读取出存在f里面的值。如果你是有 C 语言背景的程序员你可能会认为简单得把指向函数a的指针打印出来将会得到 0x2000(就是上面汇编里面看到的地址)。当我运行上面的代码得到了 0x102c38,这个地址相差了十万八千里!反编译后,这是第11行的代码:

这里引用了main.a.f,我们看看那个位置,可以发现:

啊哈!main.a.f在 0x102c38 并且包含值 0x2000,它正好是main.a的地址。看起来f并不是指向函数的指针,而是指向函数的指针的指针。让我们修改代码证实:

代码语言:javascript
复制
1 package main
2 import (
3  "fmt"
4  "unsafe")
5 func a() int { return 1 }
6 func main() {
7  f := a
8  fmt.Printf("0x%x\n", **(**uintptr)(unsafe.Pointer(&f)))}

和我们期望的一样,将会打印 0x2000。在这里我们也能找到一些线索。Go 语言的函数值包含了额外的信息,这是闭包和丙丁实例实现的方式。

代码语言:javascript
复制
1 type funcval struct {
2    fn uintptr
3    // variable-size, fn-specific data here
4 }

接下来看看调用函数值的实现。把代码改成下面这样,给f赋值之后调用它。

代码语言:javascript
复制
1 package mainfunc a() int { return 1 }func main() {
2    f := a
3    f()}

反编译后可以得到下面的结果:

main.a.f加载到寄存器rdx里,然后把rdx寄存器指向的地址存入rbx里,最后调用。函数的地址值总是会加载到rdx寄存器里面,当代码调用的时候可以用来加载一些可能会用到的额外信息。这里的额外信息是指向绑定的实例和匿名函数闭包的指针。如果你想了解更多我建议你深入研究一下反编译代码!

让我们用新的知识实现 Go 语言里面的猴子补丁。

运行时替换函数

我们是想实现的是让下面的代码打印出来2:

代码语言:javascript
复制
1 package mainfunc a() int { return 1 }func b() int { return 2 }func main() {
2    replace(a, b)
3    print(a())}

如何实现replace?我们需要修改函数a,让它跳转到b的代码,跳过执行它自己的代码。实际上,我们需要通过这种方法来实现替换,加载函数b到寄存器rdx,然后执行时跳转到rdx上面。

代码语言:javascript
复制
1 mov rdx, main.b.f ; 48 C7 C2 ?? ?? ?? ??
2 jmp [rdx] ; FF 22

我在汇编代码旁边附上了相应的机器码(你可以用这种在线汇编工具来模拟测试)。编写一个生成上面汇编代码的函数就很简单了,类似于下面这样:

代码语言:javascript
复制
 1 func assembleJump(f func() int) []byte {
 2  funcVal := *(*uintptr)(unsafe.Pointer(&f))
 3  return []byte{
 4    0x48, 0xC7, 0xC2,
 5    byte(funcval >> 0),
 6    byte(funcval >> 8),
 7    byte(funcval >> 16),
 8    byte(funcval >> 24), // MOV rdx, funcVal
 9    0xFF, 0x22,          // JMP [rdx]
10  }
11 }

这样就能把a的函数体指向b了!下面的代码尝试复制机器代码到函数题上。

代码语言:javascript
复制
 1 package mainimport (
 2    "syscall"
 3    "unsafe")func a() int { return 1 }func b() int { return 2 }func rawMemoryAccess(b uintptr) []byte {
 4    return (*(*[0xFF]byte)(unsafe.Pointer(b)))[:]}func assembleJump(f func() int) []byte {
 5    funcVal := *(*uintptr)(unsafe.Pointer(&f))
 6    return []byte{
 7        0x48, 0xC7, 0xC2,
 8        byte(funcVal >> 0),
 9        byte(funcVal >> 8),
10        byte(funcVal >> 16),
11        byte(funcVal >> 24), // MOV rdx, funcVal
12        0xFF, 0x22,          // JMP [rdx]
13    }}func replace(orig, replacement func() int) {
14    bytes := assembleJump(replacement)
15    functionLocation := **(**uintptr)(unsafe.Pointer(&orig))
16    window := rawMemoryAccess(functionLocation)
17
18    copy(window, bytes)}func main() {
19    replace(a, b)
20    print(a())}

运行上面的代码并不会工作,结果会是 segementation fault 段错误。这是因为加载后的二进制文件默认不允许修改。我们可以使用系统调用mprotect来关掉这个保护,这个最终版的代码终于可以像期望的那样,通过调用替换后的函数来打印出来 2。

代码语言:javascript
复制
 1 package mainimport (
 2    "syscall"
 3    "unsafe")func a() int { return 1 }func b() int { return 2 }func getPage(p uintptr) []byte {
 4    return (*(*[0xFFFFFF]byte)(unsafe.Pointer(p & ^uintptr(syscall.Getpagesize()-1))))[:syscall.Getpagesize()]}func rawMemoryAccess(b uintptr) []byte {
 5    return (*(*[0xFF]byte)(unsafe.Pointer(b)))[:]}func assembleJump(f func() int) []byte {
 6    funcVal := *(*uintptr)(unsafe.Pointer(&f))
 7    return []byte{
 8        0x48, 0xC7, 0xC2,
 9        byte(funcVal >> 0),
10        byte(funcVal >> 8),
11        byte(funcVal >> 16),
12        byte(funcVal >> 24), // MOV rdx, funcVal
13        0xFF, 0x22,          // JMP rdx
14    }}func replace(orig, replacement func() int) {
15    bytes := assembleJump(replacement)
16    functionLocation := **(**uintptr)(unsafe.Pointer(&orig))
17    window := rawMemoryAccess(functionLocation)
18
19    page := getPage(functionLocation)
20    syscall.Mprotect(page, syscall.PROT_READ|syscall.PROT_WRITE|syscall.PROT_EXEC)
21
22    copy(window, bytes)}func main() {
23    replace(a, b)
24    print(a())}

封装到库中

我把上面的代码封装到了一个易用的库中。它支持32位,关闭补丁,对实例打方法补丁。我在 README 中写了一些例子。

结论

有志者事竟成!我们可以在运行时修改程序了,它能让我们做一些很酷的事情,例如猴子补丁。

我希望你读了本文之后能有所收获,我玩得很开心!

Hacker News:https://news.ycombinator.com/item?id=9290917

Reddit:https://github.com/bouk/monkey/blob/master/README.md

原文地址:http://bouk.co/blog/monkey-patching-in-go

版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2018-08-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Golang语言社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Go 语言中函数值如何工作
  • 运行时替换函数
  • 封装到库中
  • 结论
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档