在Go语言中,什么时候通过`uintptr`来引用一个对象是安全的?

huangapple go评论70阅读模式
英文:

When is it safe in Go to reference an object only through a `uintptr`?

问题

《Go编程语言》在第13.2节中说,这段代码是安全的,x将始终对垃圾收集器可见:

pb := (*int16)(unsafe.Pointer(
  uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
*pb = 42

而这段代码是不安全的,因为x在暂时不对垃圾收集器可见时,可能会被移动,导致pb成为悬空指针:

tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
pb := (*int16)(unsafe.Pointer(tmp))
*pb = 42

但是我看不出这两个示例之间的区别。

在被描述为安全的情况下,调用了uintptr之后,对x的唯一引用是uintptr值,对吗?同一行中有一个指向它的Pointer,但它是uintptr的参数,已经执行完毕,所以没有引用这些参数,因此Pointer不是活动的,而uintptr是对该对象的唯一引用。

我看不出将uintptr存储在局部变量中而不是作为表达式中间值会使其更安全。局部变量像tmp不是在编译器阶段被移除,变成匿名数据流边缘吗?因此生成的代码在语义上应该是等价的。或者Go语言是否有一些关于垃圾收集运行时的规则?比如只在语句之间有安全点?但是第一个示例中的代码有方法调用,所以我认为它们应该始终是安全点?

英文:

The Go Programming Language says in Section 13.2 that this is code is safe and x will always
be visible to the garbage collector:

pb := (*int16)(unsafe.Pointer(
  uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
*pb = 42

And that this code is unsafe, because x is temporarily not visible to the
garbage collector, which could move it, making pb a dangling pointer:

tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
pb := (*int16)(unsafe.Pointer(tmp))
*pb = 42

But I can't see the difference between these two examples.

In the case described as safe, after uintptr has been called, the only
reference to the x is the uintptr value, isn't it? There's a Pointer
to it on the same line, but it was an argument to uintptr, which has run,
so nothing is referencing the arguments, and so the Pointer is not live and the uintptr is the only reference to the object.

I can't see how storing the uintptr in a local variable instead of as an
expression intermediate value makes it any more safe. Aren't local variables
like tmp removed in compiler phases anyway, becoming anonymous dataflow edges,
so that the generated code should be semantically equivalent? Or does Go have
some rules for when garbage collection can run? Such as having safepoints only
between statements? But the code in the first example has method calls so I
would presume they would always be safepoints?

答案1

得分: 2

我找到了你在评论中提到的参考资料这里

uintptr 是一个整数,而不是引用。将 Pointer 转换为 uintptr 会创建一个没有指针语义的整数值。即使 uintptr 持有某个对象的地址,垃圾回收器也不会在对象移动时更新该 uintptr 的值,也不会阻止该 uintptr 对象被回收。

这意味着以下表达式是安全的:

pb := (*int16)(unsafe.Pointer(
  uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
*pb = 42

因为你创建了一个 uintptr,它被视为整数而不是引用,但它被立即赋值(除非在其他地方存在竞争条件,x 引用的对象不会被垃圾回收),直到赋值之后。uintptr(再次强调:整数类型)也立即转换为指针,将其转换为引用,以便垃圾回收器可以管理 pb。这意味着:

  • uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)):都是安全的,因为 x 明显是对一个对象的引用。
  • pb 被赋予一个整数,该整数(通过转换)被标记为对一个 int16 对象的引用。

然而,当你写下这段代码时:

tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
pb := (*int16)(unsafe.Pointer(tmp))

在赋值 tmp(记住是整数,而不是引用)和实际内存中的对象之间存在一定的机会。正如文档中所说:tmp 不会被更新。因此,当你赋值 pb 时,你可能会得到一个无效的指针。将 tmp 在这种情况下视为第一种情况中的 x。它不是对一个对象的引用,而是相当于你写的:

tmp := 123456 // 一个随机整数
pb := (*int16)(unsafe.Pointer(tmp)) // 显然不安全

例如:

var pb *int16
tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
go func() {
    time.Sleep(1 * time.Second)
    pb = (*int16)(unsafe.Pointer(tmp))
}()
// 在 goroutine 开始之前,或者 time.Sleep 调用返回之前,x 的原始值可能会被垃圾回收
x = TypeOfX{
    b: 123,
}
英文:

Found the reference I hinted at in my comments here

> A uintptr is an integer, not a reference. Converting a Pointer to a uintptr creates an integer value with no pointer semantics. Even if a uintptr holds the address of some object, the garbage collector will not update that uintptr's value if the object moves, nor will that uintptr keep the object from being reclaimed.

What this means is that this expression:

pb := (*int16)(unsafe.Pointer(
  uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
*pb = 42

Is safe because you're creating a uintptr, which is seen as an integer, not a reference, but it's immediately assigned (unless there's a race condition somewhere else, the object that x references cannot be GC'ed) until after the assignment). The uintptr (again: integer type) is also immediately cast to a pointer, turning it into a reference so the GC will manage pb. This means that:

  • uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)): all safe, because x clearly is an existing reference to an object
  • pb is assigned an integer that is (through the cast) marked as a reference to an int16 object

However, when you write this:

tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
pb := (*int16)(unsafe.Pointer(tmp))

There is a chance that, between assigning tmp (remember integer, not reference), the actual object in memory is moved. As it says in the docs: tmp will not be updated. Thus, when you assign pb, you could end up with an invalid pointer.<br>
think of tmp in this case as x in the first case. Rather than being a reference to an object, it's as if you wrote

tmp := 123456 // a random integer
pb := (*int16) (unsafe.Pointer(tmp)) // not safe, obviously

For example:

var pb *int16
tmp := uintptr(unsafe.Pointer(&amp;x)) + unsafe.Offsetof(x.b)
go func() {
    time.Sleep(1 * time.Second)
    pb = (*int16)(unsafe.Pointer(tmp))
}()
// original value of x could be GC&#39;ed here, before the goroutine starts, or the time.Sleep call returns
x = TypeOfX{
    b: 123,
}

huangapple
  • 本文由 发表于 2017年2月6日 20:10:44
  • 转载请务必保留本文链接:https://go.coder-hub.com/42067478.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定