前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >在 Go 语言中 Patch 非导出函数

在 Go 语言中 Patch 非导出函数

作者头像
梦醒人间
发布于 2020-09-10 07:16:47
发布于 2020-09-10 07:16:47
1.1K00
代码可运行
举报
文章被收录于专栏:码农桃花源码农桃花源
运行总次数:0
代码可运行

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

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func LoadConfig() string {
    jsonBytes, err := redis.Get("xxxx")
    return string(jsonBytes)
}

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 的函数地址:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
NM(1)  GNU Development Tools  NM(1)

NAME
       nm - list symbols from object files

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
$cat hello.go
package main

func say() {
  println("yyyy")
}

func main() {
  say()
}

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

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

用 nm 找找这个 say 的地址:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
$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:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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

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

本文分享自 码农桃花源 微信公众号,前往查看

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

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

评论
登录后参与评论
暂无评论
推荐阅读
解构 Solidity 合约 #3:函数包装器
号外,今天我们的登链社区网站做了一点小更新, 作者们可以关联自己的社交账号,关联后,在文章右侧的作者区域就可以看到点亮的小图标,让更多的小伙伴通过内容交朋友,也欢迎大家关注登链社区的账号。
Tiny熊
2023/01/09
7330
解构 Solidity 合约 #3:函数包装器
解构 Solidity 合约 #4: 函数体
这是解构系列另一篇。如果你没有读过前面的文章[4],请先看一下。我们正在解构一个简单的Solidity 合约[5]的EVM 字节码[6]。
Tiny熊
2023/01/09
8480
解构 Solidity 合约 #4: 函数体
最详细的解释EVM的函数选择原理
在我们开始前,这篇文章假定读者具备 solidity 的基础知识,以及了解它是如何部署在以太坊网络的。本文将简要地讨论这部分知识,如果你想对这些知识进行系统复习,请看这篇文章[2]众所周知,solidity 代码在部署到以太坊网络之前需要被编译成字节码。这个字节码对应的是 evm 所解析的一系列操作码指令。本系列文章主要分析编译后的字节码特定部分,并阐明它们的工作原理。在阅读完每篇文章后,你应该对每个组件的功能有一个更清晰的了解。在这一过程中,你会学到很多与 evm 相关的基础概念。我们先来看一个基本的 solidity 合约,以及它部分字节码/操作码,以展示 evm 是如何选择函数的。由 solidity 合约创建的运行态(runtime)字节码是整个合约的内容总结(reoresentation)。在合约中,你可能写有多个函数,一旦部署在链上,就可以被调用。学习 evm 和合约的一个常见问题是,EVM 是如何知道根据合同的哪个函数被调用来执行哪一块字节码?这个问题是我们用来帮助理解 evm 的底层机制以及如何处理这种特殊情况的第一个问题。
Tiny熊
2022/04/08
6890
通过逆向和调试深入EVM #6 - 完整的智能合约布局
在这个合约中,我们将逆向一个完整的智能合约。这一部分的目标是全面了解智能合约布局,全面了解智能合约的布局,并通过手动的方式对其进行反编译。
Tiny熊
2023/01/09
7110
如何调试EVM智能合约(第1篇): 理解汇编
你可能已经知道,当一个智能合约在区块链中没有被验证时,你无法读取它的实体代码,只有字节代码被显示。
Tiny熊
2022/11/07
1.2K0
如何调试EVM智能合约(第1篇): 理解汇编
Solidity 优化 - 隐藏的 Gas 成本
本文将研究以太坊虚拟机(EVM)的内部工作,以说明如何 "利用 "EVM 的特殊特性,为用户最小化 solidity 智能合约的执行成本。社区发布许多关于 solidity 开发者可以利用的知识来设计和开发更安全、更节省 Gas 的智能合约。
Tiny熊
2023/01/09
8350
Solidity 优化 - 隐藏的 Gas 成本
Solidity 0.8.5 发布
Solidity v0.8.5[4]允许从bytes转换为bytesNN值,增加了verbatim内置函数以在 Yul 中注入任意字节码,并修复了几个较小的错误。
Tiny熊
2021/07/14
4790
EVM 学习手册
一组博客文章,深入 EVM 的特定部分,让你从 solidity 代码到 EVM 的操作代码。
Tiny熊
2022/11/07
6740
深入EVM-合约分类这件小事背后的风险
本文从合约为什么要分类出发,结合每个场景可能面对怎样的恶意攻击,最终给出一套达成相对安全的合约分类分析算法。
十四君
2023/09/01
3080
深入EVM-合约分类这件小事背后的风险
深入理解EVM操作码,让你写出更好的智能合约
你的一些编程“好习惯”反而会让你写出低效的智能合约。对于普通编程语言而言,计算机做运算和改变程序的状态顶多只是费点电或者费点时间,但对于 EVM 兼容类的编程语言(例如 Solidity 和 Vyper),执行这些操作都是费钱 的!这些花费的形式是区块链的原生货币(如以太坊的 ETH,Avalanche 的 AVAX 等等...),想象成你是在用原生货币购买计算资源。
Tiny熊
2023/01/09
1.4K0
深入理解EVM操作码,让你写出更好的智能合约
专栏开篇:破解以太坊 EVM 谜题
前段时间翻译了 Ethernaut 题库闯关系列文章[2],发布为了专栏, 效果还不错,自己也有很大提高。
Tiny熊
2022/11/07
3610
专栏开篇:破解以太坊 EVM 谜题
以太坊合约 ABI 和 EVM 字节码
本文解释以太坊中的合约 ABI[2] 和 EVM[3] 字节码。由于以太坊使用 EVM(Ethereum Virtual Machine - 以太坊虚拟机)作为系统的核心,因此用高级语言编写的智能合约代码需要编译成 EVM 字节码和合约 ABI 才能运行。在与智能合约交互时,有必要先了解它们。
Tiny熊
2022/05/25
1.5K0
以太坊合约 ABI 和 EVM 字节码
搞定EVM中的内存数据区,学他!
在第一部分[2],我们分析了 remix 的第一个合约示例 1_Storage.sol。
Tiny熊
2022/04/08
9960
搞定EVM中的内存数据区,学他!
阐述DAPP智能合约流动性质押挖矿分红系统开发技术详细及代码分析
因为EVM是基于栈的虚拟机,它根据操作的内容来计算gas,所以如果牵涉到十分复杂的计算,把运算过程放在EVM中执行就可能十分地低效,同时消耗非常多的gas。
VX_I357O98O7I8
2022/12/15
5710
智能合约Gas 优化的几个技术
每次交易被发送到区块链上,必须支付 Gas 费用。消耗的 Gas 与交易所需的计算量有关,即:EVM 执行交易所需的计算量(如果交易不涉及 EVM,例如简单的以太币转账,Gas 的数量是固定的)。
Tiny熊
2022/11/07
1.4K0
智能合约Gas 优化的几个技术
通过调试理解EVM(#4):结束/中止执行的5种指令
在 EVM 中,总共有 5 种方式来结束智能合约的执行。我们将在这篇文章中详细研究它们。让我们现在就开始吧!
Tiny熊
2023/01/09
9940
通过调试理解EVM(#4):结束/中止执行的5种指令
通过逆向和调试深入EVM #5 - EVM如何处理 if/else/for/functions
在这篇文章中,我们将讨论执行流程。像 if/for 或嵌套函数这样的语句是如何被 EVM 在汇编中处理的?
Tiny熊
2023/01/09
5730
为将傅恒与魏璎珞的爱情上链,作为技术小白的我读了EVM上百行代码,终于搞定了
延禧攻略最近大火,傅恒和魏璎珞求而不得的爱情也令很多人觉得惋惜。那么傅恒到底为什么爱上魏璎珞呢?有网友真相了。
区块链大本营
2018/09/21
9420
为将傅恒与魏璎珞的爱情上链,作为技术小白的我读了EVM上百行代码,终于搞定了
如何调试EVM智能合约 #2 :部署智能合约
在第二部分(本文)中,我们将分析当你在区块链中部署一个智能合约时发生了什么,例如,在点击 remix 中的 "部署 "按钮时。
Tiny熊
2022/11/07
7750
如何调试EVM智能合约 #2 :部署智能合约
深入Solidity数据存储位置
文章较长,内容很详细、很深入。但是不要吓到,坐下来,喝杯咖啡或你最喜欢的饮料,慢慢体会。
Tiny熊
2022/11/07
1.2K0
深入Solidity数据存储位置
推荐阅读
相关推荐
解构 Solidity 合约 #3:函数包装器
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档