Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Go unsafe 包探究 #43

Open
jinhailang opened this issue Dec 21, 2018 · 0 comments
Open

Go unsafe 包探究 #43

jinhailang opened this issue Dec 21, 2018 · 0 comments

Comments

@jinhailang
Copy link
Owner

jinhailang commented Dec 21, 2018

Go unsafe 包探究

unsafe 包的作用有两个:

  • 实现任意不同类型指针之间的转换;
  • 实现指针运算(偏移)操作;

包接口比较简单,包括 3 个函数:

  • func Alignof(x ArbitraryType) uintptr 变量对齐;
  • func Offsetof(x ArbitraryType) uintptr 计算结构体(struct)内属性值的偏移字节大小,即相对结构体起始地址的大小;
  • func Sizeof(x ArbitraryType) uintptr 类型变量自身占用字节大小,注意,不包括变量引用的地址;

unsafe 函数都是在编译时计算返回结果的,所以,可以直接用于常量赋值,也要注意,尽量不要将运行时变量类型(例如 slice)作为这些函数的参数出入,可能会导致非预期的结果。

包括 2 种类型:

  • type ArbitraryType 占位符,实际上表示任意的变量类型;
  • type Pointer 指向任意类型的指针类型。类似 C 中 void * 类型。

unsafe.Pointer 是本包的精华,也是被使用最多的功能点。Pointer 允许程序(开发者)跳脱 Go 的类型系统,(通过指针转换与运算)读写任意内存,所以要小心使用。
Pointer 总结下来就两个特性,也是实现前面说的目标(作用)的基础:

  • Pointer 能与任意类型的指针值互相转换;
  • Pointer 能与 uintpter 相互转换;

uintptr 是无符号整型,被用来存放指针值(地址)。unsafe.Pointer + uintptr 就能实现指针偏移计算了。因为 uintptr 变量存放的是某个变量的地址,因此,uintpter 变量值对应的内存地址块(对应的变量)可能会被 GC 回收掉。unsafe.Pointer 本质上就是指针,该类型变量指向的内存块则不会被回收,因此,应该使用 unsafe.Pointer 类型变量来保持变量地址不被回收。

// 不安全的使用

z := uintptr(unsafe.Pointer(&xx))
//todo ...
fmt.Println(z)

//正确使用

sp:=safe.Pointer(&xx)
z = uintptr(sp)
//todo ...
fmt.Println(z)

安全的使用场景

前面说过,使用 Pointer 必须要非常小心才行,官方定义了 6 种安全有效的使用场景。使用 go vet 工具可以检测出不符合这些场景的调用。

  • 将指针 *T1 转化成 *T2

如果 T2 大于 T1 变量类型的内存占用,并且两者共享等效的内存布局,则该转换允许将一种类型数据解释成为另一种类型。例如:int64 与 float64。

  • 将 Pointer 转成 uintptr,但是,不能转回到 Pointer

与 Pointer 不同,uintptr 保存的地址指向的变量是可以被 GC 回收的。

可以使用 runtime.KeepAlive 函数避免变量被 GC。

  • 将 Pointer 转成 uintptr,用于偏移运算,将计算结果转回成 Pointer

通常用于访问结构体或者数组等:

// equivalent to f := unsafe.Pointer(&s.f)
f := unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f))

// equivalent to e := unsafe.Pointer(&x[i])
e := unsafe.Pointer(uintptr(unsafe.Pointer(&x[0])) + i*unsafe.Sizeof(x[0]))

特别注意:因为上面说的原因,uintptr 不能放在临时变量内,所以下面这样分开使用也是无效的

u := uintptr(p)
p = unsafe.Pointer(u + offset)

另外,做地址偏移的时候,要注意越界的问题,比如:

a = []int{0,1,2,3}
p := unsafe.Pointer(uintptr(unsafe.Pointer(&a)) + len(a) * unsafe.Sizeif(a[0]))

此时,变量 p 指向的地址是未知的,可能会出现不声明,直接偷偷的读写未知内存地址的情况,对系统运行稳定性影响很大。

  • 使用函数 syscall.Syscall 时,将 Pointer 值转成 uintptr

syscall.Syscall(SYS_READ, uintptr(fd), uintptr(unsafe.Pointer(p)), uintptr(n))

  • 将函数 reflect.Value.Pointer 或 reflect.Value.UnsafeAddr 返回值,从 uintptr 转成 Pointer,最终转成具体的类型值
p := (*int)(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer()))
  • 将 reflect.SliceHeader 或 reflect.StringHeader 的数据字段(Data)转成 Pointer,或者从 Pointer 转成 Data 字段

Data 字段返回的也是 uintptr:

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

reflect.SliceHeader 的使用:

package main

import "fmt"
import "unsafe"
import "reflect"
import "runtime"

func main() {
        bs := []byte("Golang")
        var pa *[2]byte // an array pointer
        hdr := (*reflect.SliceHeader)(unsafe.Pointer(&bs))
        pa = (*[2]byte)(unsafe.Pointer(hdr.Data))
        runtime.KeepAlive(&bs)
        fmt.Printf("%s\n", pa) // &Go
        pa[1] = 'a'
        fmt.Printf("%s\n", bs) // Galang
}

如果最后一行的 Printf 不存在的话,runtime.KeepAlive 的调用是必须的。
另外,最好不要像下面这样,直接从 StringHeader 或 StringHeader 直接创建对象:

// Assume p points to a sequence of byte and
// n is the number of bytes in the sequence.
var hdr reflect.StringHeader
hdr.Data = uintptr(unsafe.Pointer(new([5]byte)))
// Now the just allocated byte array has lose all
// references and it can be garbage collected now.
hdr.Len = 5
s := *(*string)(unsafe.Pointer(&hdr))

小结

在有些场景下,使用 unsafe.Poniter 可以帮助我们写出高效的代码,例如在 sync/atomic 包内的使用。而且一些底层或 C 调用,必须要用到 Poniter。

unsafe 包用于有经验的开发者绕过 Go 类型系统的安全性限制,一定要深入理解上面的六种使用场景,谨慎使用,否则很容易引起严重的内存问题,有经验的开发者都知道,这类问题通常是很难定位的,对系统的稳定性影响很大。

参考

Go 101 : https://go101.org/article/unsafe.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant