Skip to content

Commit 03333c6

Browse files
authored
Add ARM64 support (#13)
* Add arm64 support
1 parent 3f23ca2 commit 03333c6

9 files changed

+78
-16
lines changed

README.md

+8-1
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,16 @@ Go library for monkey patching
44
## Compatibility
55

66
- **Go version:** tested from `go1.7` to `go1.19`
7-
- **Architectures:** `x86`, `amd64`
7+
- **Architectures:** `x86`, `amd64`, `arm64` (ARM64 not supported on macos)
88
- **Operating systems:** tested in `macos`, `linux` and `windows`.
99

10+
### ARM64 support
11+
The support for ARM64 have some caveats. For example:
12+
- On Windows 11 ARM64 works like any other platform, we test it with tiny methods and worked correctly.
13+
- On Linux ARM64 the minimum target and source method size must be > 24 bytes. This means a simple 1 liner method will silently fail. In our tests we have methods at least with 3 lines and works correctly (beware of the compiler optimizations).
14+
This doesn't happen in any other platform because the assembly code we emit is really short (x64: 12 bytes / x86: 7 bytes), but for ARM64 is exactly 24 bytes.
15+
- On MacOS ARM64 the patching fails with `EACCES: permission denied` when calling `syscall.Mprotect`. There's no current workaround for this issue, if you use an Apple Silicon Mac you can use a docker container or docker dev environment.
16+
1017
## Features
1118

1219
- Can patch package functions, instance functions (by pointer or by value), and create new functions from scratch.

patcher.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func PatchMethod(target, redirection interface{}) (*Patch, error) {
4646
}
4747

4848
// Patches an instance func by using two parameters, the target struct type and the method name inside that type,
49-
//this func will be redirected to the "redirection" func. Note: The first parameter of the redirection func must be the object instance.
49+
// this func will be redirected to the "redirection" func. Note: The first parameter of the redirection func must be the object instance.
5050
func PatchInstanceMethodByName(target reflect.Type, methodName string, redirection interface{}) (*Patch, error) {
5151
method, ok := target.MethodByName(methodName)
5252
if !ok && target.Kind() == reflect.Struct {

patcher_arm64.go

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//go:build arm64
2+
// +build arm64
3+
4+
package mpatch
5+
6+
import "unsafe"
7+
8+
// Code from: https://github.com/agiledragon/gomonkey/blob/master/jmp_arm64.go
9+
10+
// Gets the jump function rewrite bytes
11+
//
12+
//go:nosplit
13+
func getJumpFuncBytes(to unsafe.Pointer) ([]byte, error) {
14+
res := make([]byte, 0, 24)
15+
d0d1 := uintptr(to) & 0xFFFF
16+
d2d3 := uintptr(to) >> 16 & 0xFFFF
17+
d4d5 := uintptr(to) >> 32 & 0xFFFF
18+
d6d7 := uintptr(to) >> 48 & 0xFFFF
19+
20+
res = append(res, movImm(0b10, 0, d0d1)...) // MOVZ x26, double[16:0]
21+
res = append(res, movImm(0b11, 1, d2d3)...) // MOVK x26, double[32:16]
22+
res = append(res, movImm(0b11, 2, d4d5)...) // MOVK x26, double[48:32]
23+
res = append(res, movImm(0b11, 3, d6d7)...) // MOVK x26, double[64:48]
24+
res = append(res, []byte{0x4A, 0x03, 0x40, 0xF9}...) // LDR x10, [x26]
25+
res = append(res, []byte{0x40, 0x01, 0x1F, 0xD6}...) // BR x10
26+
27+
return res, nil
28+
}
29+
30+
func movImm(opc, shift int, val uintptr) []byte {
31+
var m uint32 = 26 // rd
32+
m |= uint32(val) << 5 // imm16
33+
m |= uint32(shift&3) << 21 // hw
34+
m |= 0b100101 << 23 // const
35+
m |= uint32(opc&0x3) << 29 // opc
36+
m |= 0b1 << 31 // sf
37+
38+
res := make([]byte, 4)
39+
*(*uint32)(unsafe.Pointer(&res[0])) = m
40+
41+
return res
42+
}

patcher_test.go

+16-8
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
package mpatch
22

33
import (
4+
"math/rand"
45
"reflect"
56
"testing"
67
)
78

89
//go:noinline
9-
func methodA() int { return 1 }
10+
func methodA() int {
11+
x := rand.Int() >> 48
12+
y := rand.Int() >> 48
13+
return x + y
14+
}
1015

1116
//go:noinline
12-
func methodB() int { return 2 }
17+
func methodB() int {
18+
x := rand.Int() >> 48
19+
y := rand.Int() >> 48
20+
return -(x + y)
21+
}
1322

1423
type myStruct struct {
1524
}
@@ -29,15 +38,14 @@ func TestPatcher(t *testing.T) {
2938
if err != nil {
3039
t.Fatal(err)
3140
}
32-
if methodA() != 2 {
41+
if methodA() > 0 {
3342
t.Fatal("The patch did not work")
3443
}
35-
3644
err = patch.Unpatch()
3745
if err != nil {
3846
t.Fatal(err)
3947
}
40-
if methodA() != 1 {
48+
if methodA() < 0 {
4149
t.Fatal("The unpatch did not work")
4250
}
4351
}
@@ -48,15 +56,15 @@ func TestPatcherUsingReflect(t *testing.T) {
4856
if err != nil {
4957
t.Fatal(err)
5058
}
51-
if methodA() != 2 {
59+
if methodA() > 0 {
5260
t.Fatal("The patch did not work")
5361
}
5462

5563
err = patch.Unpatch()
5664
if err != nil {
5765
t.Fatal(err)
5866
}
59-
if methodA() != 1 {
67+
if methodA() < 0 {
6068
t.Fatal("The unpatch did not work")
6169
}
6270
}
@@ -78,7 +86,7 @@ func TestPatcherUsingMakeFunc(t *testing.T) {
7886
if err != nil {
7987
t.Fatal(err)
8088
}
81-
if methodA() != 1 {
89+
if methodA() < 0 {
8290
t.Fatal("The unpatch did not work")
8391
}
8492
}

patcher_unix.go

+3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
//go:build !windows
12
// +build !windows
23

34
package mpatch
45

56
import (
67
"reflect"
78
"syscall"
9+
"time"
810
"unsafe"
911
)
1012

@@ -41,5 +43,6 @@ func writeDataToPointer(ptr unsafe.Pointer, data []byte) error {
4143
if err := callMProtect(ptr, dataLength, readAccess); err != nil {
4244
return err
4345
}
46+
<-time.After(100 * time.Microsecond) // If we remove this line then it fails to unpatch on ARM64
4447
return nil
4548
}

patcher_unsupported.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
// +build !386
2-
// +build !amd64
1+
//go:build !386 && !amd64 && !arm64
2+
// +build !386,!amd64,!arm64
33

44
package mpatch
55

@@ -11,6 +11,7 @@ import (
1111
)
1212

1313
// Gets the jump function rewrite bytes
14+
//
1415
//go:nosplit
1516
func getJumpFuncBytes(to unsafe.Pointer) ([]byte, error) {
1617
return nil, errors.New(fmt.Sprintf("unsupported architecture: %s", runtime.GOARCH))

patcher_windows.go

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
//go:build windows
12
// +build windows
23

34
package mpatch

patcher_x32.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1+
//go:build 386
12
// +build 386
23

34
package mpatch
45

56
import "unsafe"
67

7-
const jumpLength = 7
8-
98
// Gets the jump function rewrite bytes
9+
//
1010
//go:nosplit
1111
func getJumpFuncBytes(to unsafe.Pointer) ([]byte, error) {
1212
return []byte{

patcher_x64.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1+
//go:build amd64
12
// +build amd64
23

34
package mpatch
45

56
import "unsafe"
67

7-
const jumpLength = 12
8-
98
// Gets the jump function rewrite bytes
9+
//
1010
//go:nosplit
1111
func getJumpFuncBytes(to unsafe.Pointer) ([]byte, error) {
1212
return []byte{

0 commit comments

Comments
 (0)