您的位置 首页 golang

面试官:说说unsafe.Pointer和uintptr的区别和联系

前几天 Go语言中文网 公众号发了一篇文章:unsafe 真就不安全吗?其中谈到了 unsafe 包的初衷和功能。不过那篇文章还有一个功能没讲,那就是 unsafe.Pointer,今天就详细介绍它,包括其他相关类型和实际使用情况。同时还会设计到社区对 unsafe 的看法以及建议。希望看完这篇文章,你能较好的掌握 unsafe.Pointer。当然,请在必要时使用它。

type Pointer

此类型表示指向任意类型的指针,这意味着,unsafe.Pointer 可以转换为任何类型或 uintptr 的指针值。你可能会想: 有什么限制吗?没有,是的… 你可以转换 Pointer 为任何你想要的,但你必须处理可能的后果。为了减少可能出现的问题,你可以使用某些模式:

你也可以使用 go vet,但是它不能解决所有的问题。因此,我建议你遵循这些模式,因为这是减少错误的唯一方法。

快速拷贝

如果两种类型的内存布局相同,为了避免内存分配,你可以通过以下机制将类型 *T1 的指针转换为类型 *T2 的指针,将类型 T1 的值复制到类型 T2 的变量中:

 ptrT1 := &T1{}
ptrT2 = (*T2)(unsafe.Pointer(ptrT1))
  

但是要小心,这种转换是有代价的,现在两个指针指向同一个内存地址,所以每个指针的改变也会反应到另一个指针上。可以通过 这里验证 [1]

unsafe.Pointer != uintptr

我已经提到过,指针可以转换为 uintptr 并转回来,但是转回来是有一些特殊的条件限制的。unsafe.Pointer 是一个真正的指针,它不仅保持内存地址,包括动态链接的地址,但 uintptr 只是一个数字,因此它更小,但有代价。如果你转换 unsafe.Pointer 为 uintptr 后,指针不再引用指向的变量,而且在将 uintptr 转换回 unsafe.Pointer 变量之前,垃圾收集器可以轻松地回收该内存。至少有两种解决方案可以避免此问题。第一个更复杂的,但也真正显示了,为了使用 unsafe 包,你必须牺牲什么。有一个特殊的函数,runtime.KeepAlive 可以避免 GC 不恰当的回收。它听起来很复杂,而且使用起来更加复杂。这里为你准备了 实际例子 [2]

指针算法

还有另一种方法避免 GC 不恰当回收。即在同一个语句中做以下事情:将 unsafe.Poniter 转为 uintptr,以及将 uintptr 做其他运算,最后转回 unsafe.Pointer 。因为 uintptr 只是一个数字,我们可以做所有特殊的算术运算,比如加法或减法。我们如何使用它?指针算法通过了解内存布局和算术运算,可以得到任何需要的数据。让我们来看看下一个例子:

 x := [4]byte{10, 11, 12, 13}
elPtr := unsafe.Pointer(uintptr(unsafe.Pointer(&x[0])) + 3*unsafe.Sizeof(x[0]))
  

有了指向字节数组第一个元素的指针,我们就可以在不使用索引的情况下获得最后一个元素。如果将指针移动三个字节,我们就可以得到最后一个元素。

因此,在一个表达式中执行所有转换可以省去 GC 清理的麻烦。上述三种模式说明了如何在不同情况下正确地转换 unsafe.Pointer 为其他数据类型的指针。

Syscalls

在包 syscall 中,有一个函数 syscall.Syscall 接收 uintptr 格式的指针的系统调用,我们可以通过 unsafe.Pointer 得到 uintptr。重要的是,你必须进行正确的转换:

 a := &A{1}
b := &A{2}
syscall.Syscall(0, uintptr(unsafe.Pointer(a)), uintptr(unsafe.Pointer(b))) // Right

aPtr := uintptr(unsafe.Pointer(a)
bPtr := uintptr(unsafe.Pointer(b)
syscall.Syscall(0, aPtr, bPtr) // Wrong
  

reflect.Value.Pointer 和 reflect.Value.UnsafeAddr

reflect 包中有两个方法: Pointer 和 UnsafeAddr,它们返回 uintptr,因此我们应该立即将结果转换为 unsafe.Pointer,因为我们需要时刻“提防”我们的 GC 朋友:

 p1 := (*int)(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer())) // Right

ptr := reflect.ValueOf(new(int)).Pointer() // Wrong
p2 := (*int)(unsafe.Pointer(ptr) // Wrong
  

reflect.SliceHeader 和 reflect.StringHeader

reflect 包中有两种类型: SliceHeader 和 StringHeader,它们都具有字段 Data uintptr。正如你所记得的那样,uintptr 通常与 unsafe.Pointer 联系在一起,见下面代码:

 var s string
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s))
hdr.Data = uintptr(unsafe.Pointer(p))
hdr.Len = n
  

以上就是所有可能关于 unsafe.Pointer 使用的模式,所有不遵循这些模式或从这些模式派生的情况很可能是无效的。但是 unsafe 包不仅在代码中而且在代码之外都会带来问题。让我们回顾一下其中的几个。

兼容性

Go 有 兼容性指南 [3] ,保证版本更新的兼容性。简单地说,它保证你的代码在升级后仍然可以工作,但是不能保证你已经导入了 unsafe 的包。unsafe 包的使用可能会破坏你的代码的每个版本: major,minor,甚至安全修补程序。所以在导入之前,试着想一下这样一种情况:你的客户问你为什么我们不能通过升级 Go 版本来消除漏洞,或者为什么在更新之后什么都不能工作了。

不同的行为

你知道所有的 Go 数据类型吗?你听说过 int 吗?如果我们已经有 int32 和 int64,为什么还有 int?实际上 int 类型是根据计算机体系结构(x32 或 x64)将其转换为 int32 或 int64 类型。所以请记住,unsafe 的函数结果和内存布局在不同的架构上可能是不同的,例如:

 var s string
unsafe.Sizeof(s) // x32 上是 8,而 x64 上是 16
  

社区的情况

我想知道:如果这个包如此危险,有多少冒险者在使用它。我已经在 GitHub [4] 上搜索过了。与 crypto [5] math [6] 相比,数量并不多。其中超过一半的内容是关于使用 unsafe 的方法的技巧和可能的偏差,而不是一些真正的用法。

Rust 社区有一个事件:一个叫 Nikolay Kim 的,他是 activex [7] 项目的创始人,在社区的巨大压力下,将 activex 库变成了私有。后来再公开该仓库时,将其中一个贡献者提升为所有者,然后 离开 [8] 。所有这一切的发生都是因为一些人认为使用了 unsafe 包,这太危险不应该使用。我知道 Go 社区目前没有这种情况,而且 Go 社区里也没有唯一正确的观点。我想要提醒的是,如果你在代码中导入了 unsafe 的代码,请做好准备,社区可能会。。。

爱好者

有很多人和很多想法, 这篇文章 [9] 展示了使用 int 和使用指针操作的新方法,简而言之,它看起来像这样:

 var foo int
fooslice = (*[1]int)(unsafe.Pointer(&foo))[:]
  

对此,我不发表意见,我只会提到,你应该注意导入 unsafe 可能的问题。

最后

我个人试着去思考 unsafe 带来问题的可能性,这里有一个使用 unsafe 的例子。假设你导入了一些执行某些有用操作的第三方包,比如将 DB 客户端对象和日志记录器包装到一个实体中,以使所有操作的日志记录更加容易,或者像我的例子中那样,导入一些返回对象的动物的函数…

 package main

import (
 "fmt"
 "third-party/safelib"
)

func main() {
 a := safelib.NewA("#34;, "1234") // Url and password
 fmt.Println("My spiritual animal is: ", safelib.DoSomeHipsterMagic(a))
 a.Show()
}
  

在这个函数中,我们将 interface{} 断言为一些已知类型,并快速复制到一些 Malicious 类型,这些 Malicious 类型具有获取和设置私有字段的方法,如 url 和密码。所以这个包可以提取出所有有趣的数据,甚至替换 url,这样下次你尝试连接到 DB 时,有人会获得你的凭证。

 func DoSomeHipsterMagic(any interface{}) string {
 if a, ok := any.(*A); ok {
  mal := (*Malicious)(unsafe.Pointer(a))
  mal.setURL("#34;)
 }

 return "Cool green dragon, arrh "
}
  

最后的最后,切记所有的技术都有一定的代价,但是 unsafe 技术尤其“昂贵”,所以我的建议是在使用它之前要三思。

参考资料

[1]

这里验证:

[2]

实际例子:

[3]

兼容性指南:

[4]

GitHub:

[5]

crypto:

[6]

math:

[7]

activex:

[8]

离开:

[9]

这篇文章:

文章来源:智云一二三科技

文章标题:面试官:说说unsafe.Pointer和uintptr的区别和联系

文章地址:https://www.zhihuclub.com/97529.shtml

关于作者: 智云科技

热门文章

网站地图