蠢萌的死法: 指针中的随机值
最近有人提出「是否可以将非指针放入 unsafe.Pointer 变量」的问题。普遍的反应是「NO,这是一个坏主意」。我同意,但如果我们从不探索糟糕的想法,永远不会…嗯,实际上,如果从不探索糟糕的想法,绝对不会出问题。
让我们探讨一下这个 Bad idea
为什么是坏主意? 主要是可能是会让 Go 的垃圾收集器崩溃。Go GC 会查看程序可见的每个指针,以查看哪些内存仍在使用,以及哪些内存可以释放。如果它跟随的指针未指向有效的内存地址,则可能会崩溃。
让我们尝试一下,分配十亿个 unsafe.Pointers 并将他们全部设置为无效指针的值。
1
2
3
4
5
6
7
8
9
10
11
|
func TestRandomUnsafePointers(t *testing.T) {
x := make([]unsafe.Pointer, 1e9)
for i := range x {
// Possible misuse of unsafe.Pointer? Definite misuse of unsafe.Pointer!
x[i] = unsafe.Pointer(uintptr(i * 8))
}
runtime.GC()
runtime.KeepAlive(x)
}
|
此代码创建一个包含 10 亿个unsafe.Pointer
的切片,然后强制 GC 运行。它不会崩溃。
我们可以用真正的随机值再试一次,并且做一些傻事。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
func TestRandomUnsafePointers2(t *testing.T) {
x := make([]unsafe.Pointer, 1e9)
for i := range x {
// Possible misuse of unsafe.Pointer? Definite misuse of unsafe.Pointer!
x[i] = unsafe.Pointer(uintptr(rand.Int64()))
}
runtime.GC()
for range 10 {
for i := range x {
// Possible misuse of unsafe.Pointer? Definite misuse of unsafe.Pointer!
x[i] = unsafe.Add(x[i], 3)
}
runtime.GC()
}
runtime.KeepAlive(x)
}
|
仍旧未崩溃。
如果我们不够聪明怎么办?
Go 可能会查看这些值并思考「啊嘞」,然后忽略他。 Go 可以与 C 交互,因此它需要能够处理在其控制之外分配的内存。多年来,我还使用 Go 直接通过系统调用分配的内存,没有任何问题(祈祷)。
如果指针的值看起来像它应该关心的内存,Go 可能会发现它更困难。如果我们存储的值曾经是 Go 本身分配的有效内存地址,但我们知道它不再有效怎么办?
这里我们分配一个足够大的切片,以便始终分配在堆上。然后,我们获取支持该切片的数组的地址,并将其放入 uintptr 中。我们知道 Go 不会将 uintptr 视为指针,因此将值保存在 uintptr 中不应导致 Go 保留分配。
如果我们随后删除对切片的引用并强制执行 GC,则应该释放内存。
1
2
3
4
|
y := make([]int, 1e4)
yptr := uintptr(unsafe.Pointer(unsafe.SliceData(y)))
y = nil
runtime.GC()
|
现在,如果我们将此值存储在unsafe.Pointer
中并再次运行 GC,我们可能会遇到麻烦。这是完整的测试。
1
2
3
4
5
6
7
8
9
10
11
12
|
func TestUnsafePointerBadNumber(t *testing.T) {
y := make([]int, 1e4)
yptr := uintptr(unsafe.Pointer(unsafe.SliceData(y)))
y = nil
runtime.GC()
runtime.GC()
x := unsafe.Pointer(yptr)
runtime.GC()
runtime.GC()
runtime.KeepAlive(x)
}
|
事实上,这确实引起了 panic 。
1
2
3
4
5
6
7
|
runtime: pointer 0xc000162000 to unallocated span span.base()=0xc000288000 span.limit=0xc000290000 span.state=0
runtime: found in object at *(0xc00005ff58+0x0)
object=0xc00005ff58 s.base()=0xc00005e000 s.limit=0xc000066000 s.spanclass=0 s.elemsize=2048 s.state=mSpanManual
:
:
...
fatal error: found bad pointer in Go heap (incorrect use of unsafe or cgo?)
|
如果您想知道为什么要多次调用runtime.GC()
,我也是。这似乎使它更可靠地崩溃。
这意味着什么?
Go 的垃圾收集器非常强大,可以处理很多滥用情况。您可以将值存储在 GC 不知道的指针中,或者甚至不是有效的内存地址。
但如果你存储了 GC 认为它能控制的内存地址,那么它就会发生 Panic。如果您将非指针值存储在unsafe.Pointer
中,您可能不会立即看到问题。你的测试可能不会显示任何问题。但有一天,总会有异常甩你脸上,而你却不知道为什么。
我的结论如下。
- 除非确实必要,否则不要使用
unsafe.Pointer
。
- 除非确实有必要,否则不要保留
unsafe.Pointer
指针值。大多数使用unsafe.Pointer
的安全操作仅将其用作瞬态值,同时将某些内容转换为其他内容。
- 你可能不需要。
- 仅将内存地址存储在
unsafe.Pointer
中。
- 也许只有在 Go 运行时之外分配的内存地址(例如通过直接调用 mmap 系统调用)。
参考
本文翻译于Dumb ways to die: Random Values in Pointers