Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MacOS m3 打桩失败bug #171

Open
liracle opened this issue Nov 23, 2024 · 2 comments
Open

MacOS m3 打桩失败bug #171

liracle opened this issue Nov 23, 2024 · 2 comments

Comments

@liracle
Copy link

liracle commented Nov 23, 2024

环境

MacOS: 14.6
GO版本: go1.21.13 (也测试了go1.18版本也有问题)
M3芯片
gomonkey版本: v2.12.0

问题描述

在一个测试用例中对同一个函数进行打桩、取消、再打桩,后续的打桩可能会失败。以下为测试代码,可以直接拷贝执行验证的

type funcValue struct {
	_ uintptr
	p unsafe.Pointer
}

func getPointer(v reflect.Value) unsafe.Pointer {
	return (*funcValue)(unsafe.Pointer(&v)).p
}

func entryAddress(p uintptr, l int) []byte {
	return *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{Data: p, Len: l, Cap: l}))
}

func target() {
	fmt.Println("origin func invoked")
}

// func24Byte 此函数将函数对应的代码code前24个字节打印出来,方便观察打桩是否生效了
func func24Byte(fn interface{}) {
	v := reflect.ValueOf(fn)
	pp := *(*uintptr)(getPointer(v))
	addr := entryAddress(pp, 24)
	fmt.Printf("代码code:%x\n", addr)
}

func TestBBB(t *testing.T) {
	p := gomonkey.NewPatches()
	pause := true  // 控制每次打桩后是否需要sleep,sleep会影响打桩是否生效,sleep后打桩稳定生效,不sleep打桩不一定会生效
        // 输出原始函数
	fmt.Println("expect origin func:") 
	target()
	func24Byte(target) 

        // 第一次打桩
	p.ApplyFunc(target, func() {
		fmt.Println("double func invoked")
	})
	if pause {
		time.Sleep(time.Millisecond)
	}
	fmt.Println("expect double func")
	target()
	func24Byte(target)

        // 恢复
	p.Reset()
	if pause {
		time.Sleep(time.Millisecond)
	}
	fmt.Println("expect origin func")
	target()
	func24Byte(target)
	
        // 第二次打桩
	p2 := gomonkey.NewPatches()
	p2.ApplyFunc(target, func() {
		fmt.Println("three func invoked")
	})
	if pause {
		time.Sleep(time.Millisecond)
	}
	fmt.Println("expect three func")
	target()
	func24Byte(target)

        // 恢复
	p2.Reset()
	if pause {
		time.Sleep(time.Millisecond)
	}
	fmt.Println("expect origin func")
	target()
	func24Byte(target)
}

当使用pause=true 执行测试时,测试完美执行,执行结果如下:
image
但是当运行pause=false时,测试结果如下:
image

我耗费两天观察这个问题,依然百思不得其解,为什么代码区字节码都被改写了,funcValue的调用依然不生效呢,辛苦各位大佬帮忙看下@agiledragon

@qiyue51
Copy link

qiyue51 commented Dec 9, 2024

遇到了相同的二次打桩失败问题。

环境:
MacOS: 14.5
GO版本: go1.22.9
芯片: apple M1 pro
gomonkey版本: v2.12.0

@LubyRuffy
Copy link

LubyRuffy commented Dec 19, 2024

原因是因为apple silicon芯片默认会有指令缓存的功能,在Reset里面需要增加清空指令缓存,可以通过CGO使用__builtin___clear_cache函数刷新缓存,也可以使用arm指令ic ivau进行刷新。

/*
#include <stdlib.h>

// 使用 Clang 的内置函数刷新指令缓存
void flush_instruction_cache(void *addr, size_t size) {
    __builtin___clear_cache((char*)addr, (char*)addr + size);
}
*/
import "C"
import (
	"reflect"
	"unsafe"
)

// 需要使用 cgo 或其他方法调用底层汇编指令
func flushInstructionCache(v any, method string, size uintptr) {
	// 实现汇编指令刷新缓存
	m, _ := reflect.TypeOf(v).MethodByName(method)
	addr := unsafe.Pointer(m.Func.Pointer())
	C.flush_instruction_cache(addr, C.size_t(size))
}

实验:

未刷新

type BBB struct {
	v string
}

func (b *BBB) Value() string {
	return b.v
}

func TestBBBValue(t *testing.T) {
	b1 := &BBB{
		v: "b1",
	}
	// 没有修改
	assert.Equal(t, "b1", b1.Value())
	// 修改
	p := gomonkey.ApplyMethodReturn(b1, "Value", "test_b1")
	assert.Equal(t, "test_b1", b1.Value())
	p.Reset()
	//flushInstructionCache(b1, "Value", 24)
	// 再次修改
	p = gomonkey.ApplyMethodReturn(b1, "Value", "test_b2")
	assert.Equal(t, "test_b2", b1.Value())
	p.Reset()
	//flushInstructionCache(b1, "Value", 24)
	assert.Equal(t, "b1", b1.Value())
}

会报错:

=== RUN   TestBBBValue
    main_test.go:64: 
        	Error Trace:	main_test.go:64
        	Error:      	Not equal: 
        	            	expected: "b1"
        	            	actual  : "test_b2"
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -1 +1 @@
        	            	-b1
        	            	+test_b2
        	Test:       	TestBBBValue
--- FAIL: TestBBBValue (0.00s)

Expected :b1
Actual   :test_b2
<Click to see difference>


FAIL

这个不固定,因为有两次Reset都可能随机出现问题,比如还有可能:

=== RUN   TestBBBValue
    main_test.go:61: 
        	Error Trace:	main_test.go:61
        	Error:      	Not equal: 
        	            	expected: "test_b2"
        	            	actual  : "test_b1"
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -1 +1 @@
        	            	-test_b2
        	            	+test_b1
        	Test:       	TestBBBValue
    main_test.go:64: 
        	Error Trace:	main_test.go:64
        	Error:      	Not equal: 
        	            	expected: "b1"
        	            	actual  : "test_b1"
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -1 +1 @@
        	            	-b1
        	            	+test_b1
        	Test:       	TestBBBValue
--- FAIL: TestBBBValue (0.00s)

Expected :b1
Actual   :test_b1
<Click to see difference>


FAIL

把对应刷新的注释关闭,怎么测试都可以了:

func TestBBBValue(t *testing.T) {
	b1 := &BBB{
		v: "b1",
	}
	// 没有修改
	assert.Equal(t, "b1", b1.Value())
	// 修改
	p := gomonkey.ApplyMethodReturn(b1, "Value", "test_b1")
	assert.Equal(t, "test_b1", b1.Value())
	p.Reset()
	flushInstructionCache(b1, "Value", 24) // <--- 这里
	// 再次修改
	p = gomonkey.ApplyMethodReturn(b1, "Value", "test_b2")
	assert.Equal(t, "test_b2", b1.Value())
	p.Reset()
	flushInstructionCache(b1, "Value", 24) // <--- 这里
	assert.Equal(t, "b1", b1.Value())
}

测试通过:

=== RUN   TestBBBValue
--- PASS: TestBBBValue (0.00s)
PASS

@liracle @agiledragon 供参考。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants