Golang Patch 实现Golang单元测试 Mock


本文转自https://bou.ke/blog/monkey-patching-in-go/

前提

使用go build -gcflags=-l来构建,禁用内联。
对于本文,假设架构是 64 位,并且使用的是基于 Unix 的操作系统,如 Mac OSX 或 Linux

具体实现

让我们看看以下代码在反汇编时会产生什么:

package main

func a() int { return 1 }

func main() {
  print(a())
}

通过Hopper编译和查看时,上面的代码会产生这样的汇编代码:

代码main.main开始,其中指令0x20100x2026设置堆栈

0x202a是函数调用main.a0x2000简单地移动0x1到堆栈并返回。
0x202f0x2037再传给该值runtime.printint

函数值在 Go 中的工作原理

package main

import (
  "fmt"
  "unsafe"
)

func a() int { return 1 }

func main() {
  f := a
  fmt.Printf("0x%x\n", *(*uintptr)(unsafe.Pointer(&f)))
}

我在第 11 行所做的是分配af,这意味着f()将调用a。 然后我使用unsafe Go 包直接读出f。 如果有学习过C语言,您可能希望f只是一个指向的函数指针a,从而打印出此代码0x2000main.a我们在上面看到的位置)。
当我在我的机器上运行它时,我得到0x102c38,这是一个甚至不接近我们代码的地址!反汇编后,这就是上面第 11 行发生的情况:

这引用了一个叫做main.a.f的东西,当我们查看那个位置时,我们看到:

啊哈!main.a.f0x102c38包含了 0x2000,这是main.a的位置。
似乎f不是指向函数的指针,而是指向函数指针的指针。让我们修改代码来弥补这一点。

package main
 
import (
  "fmt"
  "unsafe"
)
 
func a() int { return 1 }
 
func main() {
  f := a
  fmt.Printf("0x%x\n", **(**uintptr)(unsafe.Pointer(&f)))
}

这将按0x2000预期打印。我们可以找到一个线索,说明为什么要按原样执行此操作。Go 函数值可以包含额外的信息,这就是闭包和绑定实例方法的实现方式。

让我们看看调用函数值是如何工作的。我会f在分配后更改要调用的代码。

package main

func a() int { return 1 }

func main() {
	f := a
	f()
}

当我们拆开它时,我们得到以下信息:

main.a.f被加载到rdx,然后rdx点被加载到rbx,最后被调用。
函数值的地址总是被加载到rdx,被调用的代码可以使用它来加载它可能需要的任何额外信息。
此额外信息是指向绑定实例方法的实例和匿名函数的闭包的指针。
如果你想了解更多,我建议你拿出一个反汇编器并深入研究!

让我们使用我们新获得的知识在 Go 中实现monkey patching

在运行时替换函数

我们想要实现的是用下面的代码打印出2

package main

func a() int { return 1 }
func b() int { return 2 }

func main() {
	replace(a, b)
	print(a())
}

现在我们如何实现replace?我们需要修改函数a以跳转到b的代码而不是执行它自己的函数体。
本质上,我们需要加载brdx,然后跳转到rdx指向的位置

mov rdx, main.b.f ; 48 C7 C2 ?? ?? ?? ??
jmp [rdx] ; FF 22

我已经将这些行在组装时生成的相应机器代码放在它旁边(您可以使用像这样的在线汇编器轻松地玩汇编)。编写将生成此代码的函数现在很简单,如下所示:

func assembleJump(f func() 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, funcVal
    0xFF, 0x22,          // JMP [rdx]
  }
}

我们现在实现了从a跳转到替换函数体b所需的一切!下面的代码试图将机器码直接复制到函数体的位置。

package main

import (
	"syscall"
	"unsafe"
)

func a() int { return 1 }
func b() int { return 2 }

func rawMemoryAccess(b uintptr) []byte {
	return (*(*[0xFF]byte)(unsafe.Pointer(b)))[:]
}

func assembleJump(f func() 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, funcVal
		0xFF, 0x22,          // JMP [rdx]
	}
}

func replace(orig, replacement func() int) {
	bytes := assembleJump(replacement)
	functionLocation := **(**uintptr)(unsafe.Pointer(&orig))
	window := rawMemoryAccess(functionLocation)
	
	copy(window, bytes)
}

func main() {
	replace(a, b)
	print(a())
}

但是,运行此代码不起作用,并且会导致分段错误。这是因为加载的二进制文件默认是不可写的。
我们可以使用系统调用mprotect来禁用这种保护,而这个最终版本的代码正是这样做的,实现 functiona被functionb替换,并打印2

package main

import (
	"syscall"
	"unsafe"
)

func a() int { return 1 }
func b() int { return 2 }

func getPage(p uintptr) []byte {
	return (*(*[0xFFFFFF]byte)(unsafe.Pointer(p & ^uintptr(syscall.Getpagesize()-1))))[:syscall.Getpagesize()]
}

func rawMemoryAccess(b uintptr) []byte {
	return (*(*[0xFF]byte)(unsafe.Pointer(b)))[:]
}

func assembleJump(f func() 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, funcVal
		0xFF, 0x22,          // JMP rdx
	}
}

func replace(orig, replacement func() 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)
}

func main() {
	replace(a, b)
	print(a())
}

封装成一个包

我把上面的代码放在一个库中。它支持 32 位、反向补丁和补丁实例方法。我写了几个例子并将它们放在自述文件中


文章作者: 江湖义气
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 江湖义气 !
  目录