Skip to content

Commit b32f304

Browse files
committed
feat: redefine.Func on arm64
Implement redefine.Func on arm64. There's nothing to handle cloning functions right now, so this also re-organizes the code so Original isn't compiled on arm64.
1 parent d81554e commit b32f304

15 files changed

+241
-91
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
strategy:
1515
matrix:
1616
go-version: ['1.25', '1.26']
17-
os: [ubuntu-latest, macos-15-intel, windows-latest]
17+
os: [ubuntu-latest, macos-15-intel, windows-latest, ubuntu-24.04-arm, windows-11-arm, macos-latest]
1818

1919
steps:
2020
- name: Checkout code

asm_amd64.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func insertJump(buf []byte, dest uintptr) error {
3636
diff32 := int32(dest - src)
3737
binary.LittleEndian.PutUint32(buf[1:], uint32(diff32))
3838

39-
// Pad the rest of the buffer INT3 opcodes to match what the compiler does
39+
// Pad the rest of the buffer with INT3 opcodes to match what the compiler does
4040
for i := instructionSize; i < len(buf); i++ {
4141
buf[i] = opcodeINT3
4242
}

asm_arm64.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package redefine
2+
3+
import (
4+
"encoding/binary"
5+
"errors"
6+
"unsafe"
7+
)
8+
9+
func insertJump(buf []byte, dest uintptr) error {
10+
if len(buf) < 4 {
11+
return errors.New("buffer too small")
12+
}
13+
14+
addr := uintptr(unsafe.Pointer(unsafe.SliceData(buf)))
15+
offset := int32(dest - addr)
16+
17+
// Encode the instruction:
18+
// -----------------------------------
19+
// | 000101 | ... 26 bit address ... |
20+
// -----------------------------------
21+
inst := (5 << 26) | (uint32(offset>>2) & (1<<26 - 1))
22+
binary.LittleEndian.PutUint32(buf, inst)
23+
24+
// Pad the rest of the buffer with nulls
25+
for i := 4; i < len(buf); i++ {
26+
buf[i] = 0
27+
}
28+
29+
return nil
30+
}

cacheflush_arm64.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//go:build arm64
2+
3+
package redefine
4+
5+
import "unsafe"
6+
7+
import "C"
8+
9+
func cacheflush(buf []byte) {
10+
start := unsafe.Pointer(unsafe.SliceData(buf))
11+
end := unsafe.Pointer(uintptr(len(buf)) + uintptr(start))
12+
C.__builtin___clear_cache(start, end)
13+
}

cacheflush_fallback.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
//go:build !arm64
2+
3+
package redefine
4+
5+
// This isn't needed on amd64. The arm64 version uses the C builtin which is a
6+
// no-op, but avoiding cgo makes cross-compiling much easier for non-Linux
7+
// OS's.
8+
func cacheflush(buf []byte) {}

clone.go

Lines changed: 9 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"runtime"
99
"sync"
1010
"syscall"
11-
"unsafe"
1211

1312
"github.com/pboyd/malloc"
1413
)
@@ -26,39 +25,16 @@ func cloneFunc[T any](fn T) (*clonedFunc[T], error) {
2625
return nil, err
2726
}
2827

29-
//fmt.Println(disassemble(originalCode))
30-
31-
cloneAllocator.BeginMutate()
32-
defer cloneAllocator.EndMutate()
33-
34-
newCode, err := cloneAllocator.Allocate(len(originalCode))
28+
cf, err := _cloneFunc(fn, originalCode)
3529
if err != nil {
3630
return nil, err
3731
}
3832

39-
newCode, err = relocateFunc(originalCode, newCode)
40-
if err != nil {
41-
return nil, err
42-
}
43-
44-
//fmt.Println(disassemble(newCode))
45-
46-
// This seems too complicated. The idea is to take our newly allocated
47-
// buffer of machine instructions and convince Go that it's really a
48-
// function pointer of type T.
49-
codeData := unsafe.SliceData(newCode)
50-
cf := clonedFunc[T]{
51-
clonedCode: newCode,
52-
// Keep a reference to codeData so it stays around.
53-
ref: &codeData,
54-
}
55-
cf.Func = *(*T)(unsafe.Pointer(uintptr(unsafe.Pointer(&cf.ref))))
56-
5733
// Make a copy of the code so that no matter what it can be restored.
5834
cf.originalCode = make([]byte, len(originalCode))
5935
copy(cf.originalCode, originalCode)
6036

61-
return &cf, nil
37+
return cf, nil
6238
}
6339

6440
type allocator struct {
@@ -240,10 +216,14 @@ func (cf *clonedFunc[T]) Free() {
240216
cloneAllocator.BeginMutate()
241217
defer cloneAllocator.EndMutate()
242218

243-
cloneAllocator.Free(cf.clonedCode)
219+
if cf.clonedCode != nil {
220+
cloneAllocator.Free(cf.clonedCode)
221+
}
244222

245223
cf.clonedCode = nil
246-
*cf.ref = nil
247-
cf.ref = nil
224+
if cf.ref != nil {
225+
*cf.ref = nil
226+
cf.ref = nil
227+
}
248228
cf.originalCode = nil
249229
}

clone_amd64.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//go:build amd64
2+
3+
package redefine
4+
5+
import "unsafe"
6+
7+
func _cloneFunc[T any](fn T, originalCode []byte) (*clonedFunc[T], error) {
8+
originalCode, err := funcSlice(fn)
9+
if err != nil {
10+
return nil, err
11+
}
12+
13+
//fmt.Println(disassemble(originalCode))
14+
15+
cloneAllocator.BeginMutate()
16+
defer cloneAllocator.EndMutate()
17+
18+
newCode, err := cloneAllocator.Allocate(len(originalCode))
19+
if err != nil {
20+
return nil, err
21+
}
22+
23+
newCode, err = relocateFunc(originalCode, newCode)
24+
if err != nil {
25+
return nil, err
26+
}
27+
28+
//fmt.Println(disassemble(newCode))
29+
30+
// This seems too complicated. The idea is to take our newly allocated
31+
// buffer of machine instructions and convince Go that it's really a
32+
// function pointer of type T.
33+
codeData := unsafe.SliceData(newCode)
34+
cf := clonedFunc[T]{
35+
clonedCode: newCode,
36+
// Keep a reference to codeData so it stays around.
37+
ref: &codeData,
38+
}
39+
cf.Func = *(*T)(unsafe.Pointer(uintptr(unsafe.Pointer(&cf.ref))))
40+
41+
return &cf, nil
42+
}

clone_fallback.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
//go:build !amd64
2+
3+
package redefine
4+
5+
func _cloneFunc[T any](fn T, originalCode []byte) (*clonedFunc[T], error) {
6+
return &clonedFunc[T]{}, nil
7+
}

clone_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//go:build amd64
2+
13
package redefine
24

35
import (

example_amd64_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//go:build amd64
2+
3+
package redefine_test
4+
5+
import (
6+
"encoding/json"
7+
"fmt"
8+
9+
"github.com/pboyd/redefine"
10+
)
11+
12+
func ExampleOriginal() {
13+
redefine.Func(json.Marshal, func(v any) ([]byte, error) {
14+
// Pass strings through
15+
if _, ok := v.(string); ok {
16+
return redefine.Original(json.Marshal)(v)
17+
}
18+
19+
return []byte(`{"nah": true}`), nil
20+
})
21+
defer redefine.Restore(json.Marshal)
22+
23+
buf, _ := json.Marshal("A string")
24+
fmt.Println(string(buf))
25+
26+
buf, _ = json.Marshal(123)
27+
fmt.Println(string(buf))
28+
// Output:
29+
// "A string"
30+
// {"nah": true}
31+
}

0 commit comments

Comments
 (0)