【我的架构师之路】- 说一说go中的unsafe包

【转载请标明出处】https://blog.csdn.net/qq_25870633/article/details/83422886

在golang的原生库中有一个叫做unsafe的包,该包主要是做对内存位移的一些操作。

首先我们来看下unsafe包的成员:

是不是很简单,然后三个函数都是没有 func body 的,这里不是用 //go:linkname 的方式实现的,而是用了汇编

包名称暗示unsafe包是不安全的,预示着我们在现实中如果有十足的了解才可以用这个包的成员,不然总会带来奇奇怪怪的结果。

首先,我们看到ArbitraryType类型其实是int的别名。在golang中,对ArbitraryType赋予了特殊的意义,千万不要死磕这个后边的int类型。通常的说,我们把interface{}看作是任意类型,是比较浪荡的型号,具有老少通吃的特点,那么ArbitraryType这个类型,在golang系统中,是人兽皆不在话下的类型。比interface{}还要随意。也就是说:ArbitraryType 实际上代表任意的Go表达式的结果类型。ArbitraryType不是一个真正的类型,它只是一个占位符。

另外,还定义了一个Pointer的类型,其实为 ArbitraryType 的指针。

提示一下:golang的指针类型长度与int类型长度,在内存中占用的字节数是一样的哟。

下面我们来看四个与unsafe.Pointer类型的特殊转换:

  1. 一个任意类型的指针值可以转换成一个unsafe.Pointer类型值: p := unsafe.Pointer(&a)
  2. 一个unsafe.Pointer类型可以转成一个任意类型的指针值: vptr := (*int)(p)
  3. 一个unsafe.Pointer类型可以转换成 uintptr 类型: uptr := uintptr(p)
  4. 一个uintptr 类型可以转成 unsafe.Pointer 类型: p := unsafe.Pointer(uptr)

注意:

  • 任意类型的指针值不能和uintptr 互转。
  • unsafe.Pointer 类型使得程序绕过Go的类型系统检查并直接在内存地址上进行读写
  • 像 *int 和 *float 在内存中的布局是不一样的。(*int)(&f32) 是行不通的,但是借助unsafe.Pointer 作为中间类型是不会报错的,但是 在同一段内存上的数据,我们分别作为 int的值和 float的值结果是不一样的。 所以这时候 float 转int之后得到的是一个不正确的值。
  • uintptr是一个整数类型。
  • 即使uintptr变量仍然有效,由uintptr变量表示的地址处的数据也可能被GC回收。
  • unsafe.Pointer是一个指针类型
  • 但是unsafe.Pointer值不能被取消引用。
  • 如果unsafe.Pointer变量仍然有效,则由unsafe.Pointer变量表示的地址处的数据不会被GC回收。
  • 由于uintptr是一个整数类型,uintptr值可以进行算术运算。 所以通过使用uintptr和unsafe.Pointer,我们可以绕过限制,* T值不能在Golang中计算偏移量

可知出于安全原因,Golang不允许以下之间的直接转换:

  • 两个不同指针类型的值,例如 int64和 float64。

  • 指针类型和uintptr的值。

但是借助unsafe.Pointer,我们可以打破Go类型和内存安全性,并使上面的转换成为可能

滥用这种方式是很危险的。

举个例子:

package main

import (
    "fmt"
    "unsafe"
)
func main() {
    var n int64 = 5
    var pn = &n
    var pf = (*float64)(unsafe.Pointer(pn))
    // now, pn and pf are pointing at the same memory address
    fmt.Println(*pf) // 2.5e-323
    *pf = 3.14159
    fmt.Println(n) // 4614256650576692846
}

在这个例子中的转换可能是无意义的,但它是安全和合法的(为什么它是安全的?)。

关于unsafe包,Ian,Go团队的核心成员之一,已经确认:

  • 在unsafe包中的函数的签名将不会在以后的Go版本中更改,

  • 并且unsafe.Pointer类型将在以后的Go版本中始终存在。

所以,unsafe包中的三个函数看起来不危险。 go team leader甚至想把它们放在别的地方。 unsafe包中这几个函数唯一不安全的是它们调用结果可能在后来的版本中返回不同的值。 很难说这种不安全是一种危险。

因此,资源在unsafe包中的作用是为Go编译器服务,unsafe.Pointer类型的作用是绕过Go类型系统和内存安全。

下面我们再来说一说三个函数:

与Golang中的大多数函数不同,上述三个函数的调用将始终在编译时求值,而不是运行时。 这意味着它们的返回结果可以分配给常量


// 返回变量对齐字节数量
func Alignof(x ArbitraryType) uintptr

// 返回变量指定属性的偏移量
/**
函数虽然接收的是任何类型的变量,
但是这个又一个前提,就是变量要是一个struct类型,
且还不能直接将这个struct类型的变量当作参数,
只能将这个struct类型变量的属性当作参数
*/
func Offsetof(x ArbitraryType) uintptr

// 返回变量在内存中占用的字节数
/**
切记,如果是slice,则不会返回这个slice在内存中的实际占用长度
*/
func Sizeof(x ArbitraryType) uintptr


通过分析发现,这三个函数的参数均是ArbitraryType类型,就是接受任何类型的变量。 

unsafe中,通过这两个兼容万物的类型 (ArbitraryTypePointer),将其他类型都转换过来,然后通过这三个函数,分别能取长度偏移量对齐字节数,就可以在内存地址映射中,来回游走尽情的去放纵。

另外提一点: 

uintptr这个类型,在golang中,字节长度也是与int一致。通常Pointer不能参与运算,比如你要在某个指针地址上加上一个偏移量,Pointer是不能做这个运算的,那么谁可以呢?就是uintptr类型了,只要将Pointer类型转换成uintptr类型,做完加减法后,转换成Pointer,通过*操作,取值,修改值,随意。

unsafe.Pointer的骚操作:

type Person struct {
    Name    string    `json:"name"`
    Age     uint8     `json:"age"`
    Address string    `json:"address"`         
}


func main (){
    
    // 获取 person 结构体的指针
    pp := &Person{"Robert",    32,    "Beijing"}

    // 获取 person 的内存地址
    vptr := uintptr(unsafe.Pointer(pp))

    // 获取 person 结构体中的Name所对应的内存地址
    // Offsetof()函数返回,内存中从存储该结构体的起始位置到
    // 存储其中某个字段的值的起始位置之间的距离
    // 存储偏移量(也就是这个距离)的单位为 byte,距离的类型为 uintptr
    nptr := vptr + unsafe.Offsetof(pp.Name)   
    
    /**
    事实上,同一个结构体类型的值,在内存的存储布局是固定的,就是说:
    对于同一个结构体类型和题的同一个字段来说,这个存储偏移量总是相同的
    
    从上面求偏移量的描述中我们也知道了 结构体的存储其实是连续的一段内存,
    是不是联想到了之前两篇文章中 chan 和select 的存储做法一样?
    现有头,然后关联的东西内存都接在头内存后面存储
    */



    // 还原成Name字段的指针类型值
    var namePtr *string = (*string)(unsafe.Pointer(nptr)) 
    
    fmt.Println(*namePtr)  // Robert
}

这里有一个恒等式:

uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f) == uintptr(unsafe.Pointer(&s.f))

原则上,我们只要获取了存储某个值的内存地址,就可以通过一定运算得到存储在其他内存地址上的值甚至程序

 

再看一些例子:

func main() {
    a := [4]int{0, 1, 2, 3}
    
    // 先求 a[1] 元素的Pointer 类型
    p1 := unsafe.Pointer(&a[1])

    // 根据 a[1] 元素的内存地址 + 2 * int类型的字节数 == a[3] 的内存地址
    p3 := unsafe.Pointer(uintptr(p1) + 2 * unsafe.Sizeof(a[0]))
    *(*int)(p3) = 6
    fmt.Println("a =", a) // a = [0 1 2 6]

    // ...

    type Person struct {
        name   string
        age    int
        gender bool
    }

    who := Person{"John", 30, true}
    pp := unsafe.Pointer(&who)
    // 求出 name 的指针
    pname := (*string)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(who.name)))
    // 求 age 的指针
    page := (*int)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(who.age)))
    // 求 gender 的指针
    pgender := (*bool)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(who.gender)))
    *pname = "Alice"
    *page = 28
    *pgender = false
    fmt.Println(who) // {Alice 28 false}
}

一些 非法案例:

// case A: unsafe.Pointer和uintptr之间的转换不会出现在同一个表达式中
func illegalUseA() {
   

    pa := new([4]int)

    // 将合法用途 p1:= unsafe.Pointer(uintptr(unsafe.Pointer(pa))+ unsafe.Sizeof(pa [0]))分成两个表达式(非法使用):
    ptr := uintptr(unsafe.Pointer(pa))
    p1 := unsafe.Pointer(ptr + unsafe.Sizeof(pa[0]))
    // “go vet”会对上述行发出警告:
    //  可能滥用 unsafe.Pointer
    // unsafe 包文档,https://golang.org/pkg/unsafe/#Pointer,
    // 认为上面的分裂是非法的。 
    // 但是当前的Go编译器和运行时(1.7.3)无法检测到这种非法使用。 
    // 但是,为了使您的程序在以后的Go版本中运行良好,最好遵守不安全的软件包文档。

    *(*int)(p1) = 123
    fmt.Println("*(*int)(p1)  :", *(*int)(p1)) //
}    

// case B: 指针指向未知地址
func illegalUseB() {
   
    a := [4]int{0, 1, 2, 3}
    p := unsafe.Pointer(&a)
    p = unsafe.Pointer(uintptr(p) + uintptr(len(a)) * unsafe.Sizeof(a[0]))
    // 现在p指向值a占用的内存的末尾。 
    // 到目前为止,虽然p无效,但没问题。 但如果我们修改p指向的值,则是非法的
    *(*int)(p) = 123
    fmt.Println("*(*int)(p)  :", *(*int)(p)) // 123 or not 123
    // 当前的Go编译器/运行时(1.7.3)和“go vet”将不会检测到此处的非法使用。

    // 但是,当前的Go运行时(1.7.3)将检测以下代码的非法使用和恐慌。
    p = unsafe.Pointer(&a)
    for i := 0; i <= len(a); i++ {
        *(*int)(p) = 123 // Go runtime(1.7.3)在测试中从未出现过恐慌
        fmt.Println(i, ":", *(*int)(p))
        // 当i == 4时,在上一行的最后一次迭代中发生恐慌。 
        // 运行时错误:无效的内存地址或无指针取消引用

        p = unsafe.Pointer(uintptr(p) + unsafe.Sizeof(a[0]))
    }
}

func main() {
    illegalUseA()
    illegalUseB()
}

编译器很难检测Go程序中非法的unsafe.Pointer使用。 运行“go vet”可以帮助找到一些潜在的错误,但不是所有的都能找到。 同样是Go运行时,也不能检测所有的非法使用。 非法unsafe.Pointer使用可能会使程序崩溃或表现得怪异(有时是正常的,有时是异常的)。 这就是为什么使用不安全的包是危险的。

再看:

type MyInt int

func main() {
    type MyInt int

    a := []MyInt{0, 1, 2}
    // b := ([]int)(a) // error: cannot convert a (type []MyInt) to type []int
    b := *(*[]int)(unsafe.Pointer(&a))

    b[0]= 3

    fmt.Println("a =", a) // a = [3 1 2]
    fmt.Println("b =", b) // b = [3 1 2]

    a[2] = 9

    fmt.Println("a =", a) // a = [3 1 9]
    fmt.Println("b =", b) // b = [3 1 9]
}

结论

  • unsafe包用于Go编译器,而不是Go运行时。
  • 使用unsafe.Pointer并不总是一个坏主意,有时我们必须使用它。
  • Golang的类型系统是为了安全和效率而设计的。 但是在Go类型系统中,安全性比效率更重要。 通常Go是高效的,但有时安全真的会导致Go程序效率低下。 unsafe包用于有经验的程序员通过安全地绕过Go类型系统的安全性来消除这些低效。
  • unsafe包可能被滥用并且是危险的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值