指针中的随机值

蠢萌的死法: 指针中的随机值

最近有人提出「是否可以将非指针放入 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中,您可能不会立即看到问题。你的测试可能不会显示任何问题。但有一天,总会有异常甩你脸上,而你却不知道为什么。

我的结论如下。

  1. 除非确实必要,否则不要使用unsafe.Pointer
  2. 除非确实有必要,否则不要保留unsafe.Pointer指针值。大多数使用unsafe.Pointer的安全操作仅将其用作瞬态值,同时将某些内容转换为其他内容。
  3. 你可能不需要。
  4. 仅将内存地址存储在unsafe.Pointer中。
  5. 也许只有在 Go 运行时之外分配的内存地址(例如通过直接调用 mmap 系统调用)。

参考

本文翻译于Dumb ways to die: Random Values in Pointers

本文阅读量 次, 总访问量 ,总访客数
Built with Hugo .   Theme Stack designed by Jimmy