专栏首页码农桃花源在 Go 语言中 Patch 非导出函数

在 Go 语言中 Patch 非导出函数

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)

NAME
       nm - list symbols from object files

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

$cat hello.go
package main

func 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 main

import (
  "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 {
  _   uintptr
  ptr 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,to
    0xFF, 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

本文分享自微信公众号 - 码农桃花源(CoderPark),作者:曹春晖

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-09-06

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Golang之轻松化解defer的温柔陷阱

    defer是Go语言提供的一种用于注册延迟调用的机制:让函数或语句可以在当前函数执行完毕后(包括通过return正常结束或者panic导致的异常结束)执行。深受...

    梦醒人间
  • 三足鼎立 —— GPM 到底是什么?(一)

    G、P、M 是 Go 调度器的三个核心组件,各司其职。在它们精密地配合下,Go 调度器得以高效运转,这也是 Go 天然支持高并发的内在动力。今天这篇文章我们来深...

    梦醒人间
  • 深度解密Go语言之关于 interface 的 10 个问题

    这次文章依然很长,基本上涵盖了 interface 的方方面面,有例子,有源码分析,有汇编分析,前前后后写了 20 多天。洋洋洒洒,长篇大论,依然有些东西没有涉...

    梦醒人间
  • springboot jar注册成windows服务

    winsw(https://github.com/kohsuke/winsw/releases),winsw 是一个可以将任何应用程序注册成服务的软件。

    开发架构二三事
  • 芯片内部长啥样?牛人用1500张照片,一层层放给你

    导读:这些天,中兴事件持续发酵,关于国产芯片的讨论一直没有停歇。为什么小小的芯片,作用如此之大,售价如此之高?制造技术这么难?它到底集成了哪些技术?它到底是怎么...

    华章科技
  • 行业领导者所关注的云计算问题

    为了收集有关当前和未来云计算状态的见解,行业媒体与来自33家企业的IT主管讨论了他们及其客户关于云计算的使用情况。

    静一
  • 大型.NET ERP系统的20条数据库设计规范

    数据库设计规范是个技术含量相对低的话题,只需要对标准和规范的坚持即可做到。当系统越来越庞大,严格控制数据库的设计人员,并且有一份规范书供执行参考。在程序框架中,...

    逸鹏
  • 南桥和北桥

    现代 PC 机主板主要使用 2 个超大规模芯片构成的芯片组或芯片集(Chipsets)组成:北桥(Northbridge)芯片和南桥(Southbridge)芯...

    zy010101
  • 概念测量的网格 (CS)

    我们提出了一种新的数据集扩展方法,基于形式化概念分析中的标度量,即闭合系统之间的连续映射,并推导出一种规范的表示法。此外,我们还证明了所述的尺度度量是相对于闭合...

    管欣8078776
  • Go defer 会有性能损耗,尽量不要用?

    从结果上来,使用 defer 后的函数开销确实比没使用高了不少,这损耗用到哪里去了呢?

    sunsky

扫码关注云+社区

领取腾讯云代金券