kevinyan815 / gocookbook

go cook book
MIT License
789 stars 167 forks source link

unsafe.Pointer和uintptr #42

Open kevinyan815 opened 3 years ago

kevinyan815 commented 3 years ago

unsafe 包用于编译阶段可以绕过 Go 语言的类型系统,直接操作内存。例如,利用 unsafe 包操作一个结构体的未导出成员。unsafe 包让我可以直接读写内存的能力。

unsafe包只有两个类型,三个函数,但是功能很强大。

type ArbitraryType int
type Pointer *ArbitraryType

func Sizeof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Alignof(x ArbitraryType) uintptr

ArbitraryTypeint的一个别名,在Go中ArbitraryType有特殊的意义。代表一个任意Go表达式类型。 Pointerint指针类型的一个别名,在Go中可以把任意指针类型转换成unsafe.Pointer类型。

三个函数的参数均是ArbitraryType类型,就是接受任何类型的变量。

注意以上三个函数返回的结果都是 uintptr 类型,这和 unsafe.Pointer 可以相互转换。三个函数都是在编译期间执行

unsafe.Pointer

unsafe.Pointer称为通用指针,官方文档对该类型有四个重要描述:

  1. 任何类型的指针都可以被转化为Pointer
  2. Pointer可以被转化为任何类型的指针
  3. uintptr可以被转化为Pointer
  4. Pointer可以被转化为uintptr

unsafe.Pointer是特别定义的一种指针类型(译注:类似C语言中的void类型的指针),在Go 语言中是用于各种指针相互转换的桥梁,它可以持有任意类型变量的地址

什么叫"可以持有任意类型变量的地址"呢?意思就是使用 unsafe.Pointer 转换的变量,该变量一定要是指针类型,否则编译会报错。

a := 1
b := unsafe.Pointer(a) //报错
b := unsafe.Pointer(&a) // 正确

和普通指针一样,unsafe.Pointer 指针也是可以比较的,并且支持和 nil 比较判断是否为空指针。

unsafe.Pointer 不能直接进行数学运算,但可以把它转换成 uintptr,对 uintptr 类型进行数学运算,再转换成 unsafe.Pointer 类型

    // uintptr、unsafe.Pointer和普通指针之间的转换关系
     uintptr <==> unsafe.Pointer <==> *T

uintptr

uintptr是 Go 语言的内置类型,是能存储指针的整型,在64位平台上底层的数据类型是 uint64。

// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr

typedef unsigned long long int  uint64;
typedef uint64          uintptr;

一个unsafe.Pointer指针也可以被转化为uintptr类型,然后保存到uintptr类型的变量中(注:这个变量只是和当前指针有相同的一个数字值,并不是一个指针),然后用以做必要的指针数值运算。(uintptr是一个无符号的整型数,足以保存一个地址)这种转换虽然也是可逆的,但是将uintptr转为unsafe.Pointer指针可能会破坏类型系统,因为并不是所有的数字都是有效的内存地址。

还有一点要注意的是,uintptr 并没有指针的语义,意思就是存储 uintptr 值的内存地址在Go发生GC时会被回收。而 unsafe.Pointer 有指针语义,可以保护它所指向的对象在“有用”的时候不会被垃圾回收。

使用unsafe.Pointer进行指针类型转换

import (
    "fmt"
    "reflect"
    "unsafe"
)

func main() {

    v1 := uint(12)
    v2 := int(13)

    fmt.Println(reflect.TypeOf(v1)) //uint
    fmt.Println(reflect.TypeOf(v2)) //int

    fmt.Println(reflect.TypeOf(&v1)) //*uint
    fmt.Println(reflect.TypeOf(&v2)) //*int

    p := &v1
    p = (*uint)(unsafe.Pointer(&v2)) //使用unsafe.Pointer进行类型的转换

    fmt.Println(reflect.TypeOf(p)) // *unit
    fmt.Println(*p) //13
}

使用unsafe.Pointer 读写结构体的私有成员

通过 Offsetof 方法可以获取结构体成员的偏移量,进而获取成员的地址,读写该地址的内存,就可以达到改变成员值的目的。

这里有一个内存分配相关的事实:结构体会被分配一块连续的内存,结构体的地址也代表了第一个成员的地址。

package main

import (
    "fmt"
    "unsafe"
)

func main() {

    var x struct {
        a int
        b int
        c []int
    }

    // unsafe.Offsetof 函数的参数必须是一个字段,  比如 x.b,  方法会返回 b 字段相对于 x 起始地址的偏移量, 包括可能的空洞。

    // 指针运算 uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)。

    // 和 pb := &x.b 等价
    pb := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))

    *pb = 42
    fmt.Println(x.b) // "42"
}

上面的写法尽管很繁琐,但在这里并不是一件坏事,因为这些功能应该很谨慎地使用。不要试图引入一个uintptr类型的临时变量,因为它可能会破坏代码的安全性

如果改为下面这种用法是有风险的:

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

随着程序执行的进行,当前goroutine会经常发生栈扩容或者栈缩容,会把旧栈内存的数据拷贝到新栈区然后更改所有指针的指向。一个unsafe.Pointer是一个指针,因此当它指向的数据被移动到新栈区后指针也会被更新。但是uintptr类型的临时变量只是一个普通的数字,所以其值不会该被改变。上面错误的代码因为引入一个非指针的临时变量 tmp,导致系统无法正确识别这个是一个指向变量 x 的指针。当第二个语句执行时,变量 x 的数据可能已经被转移,这时候临时变量tmp也就不再是现在的 &x.b 的地址。第三个语句向之前无效地址空间的赋值语句将让整个程序崩溃。

string 和 []byte 零拷贝转换

这是一个非常精典的例子。实现字符串和 bytes 切片之间的零拷贝转换。

string和[]byte 在运行时的类型表示为reflect.StringHeaderreflect.SliceHeader

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

type StringHeader struct {
    Data uintptr
    Len  int
}

只需要共享底层 []byte 数组就可以实现零拷贝转换。

代码比较简单,不作详细解释。通过构造reflect.StringHeaderreflect.SliceHeader,来完成 string 和 []byte 之间的转换。

import (
    "fmt"
    "reflect"
    "unsafe"
)

func main() {
    s := "Hello World"
    b := string2bytes(s)
    fmt.Println(b)
    s = bytes2string(b)
    fmt.Println(s)

}

func string2bytes(s string) []byte {
    stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))

    bh := reflect.SliceHeader{
        Data: stringHeader.Data,
        Len: stringHeader.Len,
        Cap: stringHeader.Len,
    }

    return *(*[]byte)(unsafe.Pointer(&bh))
}

func bytes2string(b []byte) string {
    sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b))

    sh := reflect.StringHeader{
        Data: sliceHeader.Data,
        Len:  sliceHeader.Len,
    }

    return *(*string)(unsafe.Pointer(&sh))
}