0

Go语言 unsafe.Pointer 包浅析

 2 years ago
source link: https://studygolang.com/articles/35592
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.

你必须非常努力,才能看起来毫不费力!

微信搜索公众号[ 漫漫Coding路 ],一起From Zero To Hero !

在写 Go 的过程中,我们不免会使用指针,但是大多数情况下使用的是类型安全的指针,类型安全的指针有助于我们写出安全的代码,但是却有诸多限制,比如不能对地址进行算数运算、不支持任意两个类型相互转换等。

Go 实际上是支持非类型安全的指针的,通过非类型安全指针,我们可以绕过诸多限制,在某些情况下甚至可以写出更高效的代码,但同时也可能会引入一些潜在的不容易发现的问题。其次,非类型安全指针没有受到 Go1兼容性保证 的保护,在后续的Go版本中,使用非类型安全指针的代码可能会无法编译通过。

即使会有上述的风险,但目前源码的很多地方都使用了非类型安全指针,同时官方给出了正确的使用方式,本篇文章我们就一起来学习下吧!

说明:本文中的示例,均是基于Go1.17 64位机器

类型安全指针

如何获得一个指针

我们有两种方式来获取类型安全的指针:

  1. 通过内置函数 new 获取某个类型值的指针
  2. 通过取地址符 & 获取某个变量的指针
func main() {
    // 通过 new 为int类型的值开辟一块内存,并返回指向内存起始地址的指针
    a := new(int)
    fmt.Printf("%p\n", a) // 0xc00034a4b8

    // 通过取地址符 & ,获取一个变量的指针
    b := int32(1)
    c := &b
    fmt.Printf("%p\n", c) //0xc00034a4c0
}

为什么需要使用指针

Go 中,所有的参数传递都是值传递,没有引用传递。

  1. 如果参数占用内存过大,每次函数传递都需要变量拷贝,比较耗费内存;
  2. 如果我们想要在函数内部修改变量的状态,并在调用完毕后看到这种修改,就需要使用指针。

比如我们想要调用 add 完成变量的加一操作,但是最终并没有达到期望的效果,原因就是值传递,即调用 add(b) 的时候,传入的参数是 变量b 的一份复制,并不会影响 main函数变量b 本身。

func add(a int) {
    a = a + 1
}

func main() {
    b := 1
    add(b)
    println(b) // 1
}

如果想要达到修改成功的目的,就需要传递指针:

func add(a *int) {
    *a = *a + 1
}

func main() {
    a := 1
    add(&a)
    println(a) // 2
}

类型安全指针的限制

  1. 不能对指针的地址进行算术运算

我们定义一个变量 a ,然后取地址,对地址算数运算 addr++ 会编译不通过;*addr++ 编译通过,最后输出 a=2,其实 *addr++ 被编译器解释为了(*addr)++,即解引用操作符 * 的优先级 高于 自增符++

func main() {
    a := 1
    addr := &a
    // addr++  编译不通过
    *addr++ // 编译通过
    fmt.Println(a) // 2
}
  1. 两个任意指针类型不能随意转换

只有两个类型的底层数据类型是一致的,才可以完成转换

type MyInt int64
type T1 *int64
type T2 *MyInt

func main() {

    var a *int64
    var myInt *MyInt

    var t1 T1
    t1 = a // t1 是 *int64类型,a 是 *int64 类型,可以隐式转换

    var t2 T2
    t2 = myInt       // t2 是 *MyInt类型,myInt 是 *MyInt类型,可以隐式转换
    t2 = (*MyInt)(a) // t2 的底层类型是 *int64,a 是 *int64 类型,需要显式转换

    t1 = (*int64)((*MyInt)(t2)) // t2 的底层类型是 *int64,t1 是 *int64类型,需要显式转换
}

但是这些类型,无论怎么转换,都转换不了 *uint64 类型

unsafe包

我们说的 非类型安全指针 就是指 unsafe 包中的 Pointer,它被类型定义为 type Pointer *ArbitraryTypeArbitraryType 在这里仅仅是用于表示任意类型,也就是说 Pointer 可以指向任意数据类型,可以和任意类型的指针相互转换。

// 表示任意类型
type ArbitraryType int

type Pointer *ArbitraryType

在上篇文章中Go语言内存对齐详解,我们也简单了解了 unsafe 包中有如下三个函数:

  1. func Sizeof(x ArbitraryType) uintptr

    返回一个变量占用的内存字节数

  1. func Offsetof(x ArbitraryType) uintptr

    返回结构体某个字段的地址相对于此结构体起始地址的偏移量

  1. func Alignof(x ArbitraryType) uintptr

    返回对齐系数

这三个函数的返回值的类型均为内置类型 uintptruintptr 是一个整数值,来保存变量的内存地址,可以和 Pointer 相互转换。

Pointer 表示指向任意类型的指针,对于该类型有四种合法的操作:

  • 任意类型的指针可以转为 Pointer
  • Pointer 可以转为任意类型的指针
  • uintptr 可以转为 Pointer
  • Pointer 可以转为 uintptr
func main() {

    a := int(1)

    b := (*int64)(unsafe.Pointer(&a)) // 将 *int 先转为 Pointer,再转为 *int64

    c := uintptr(unsafe.Pointer(&a)) // 将 *int 先转为 Pointer,再转为 uintptr

    fmt.Printf("%p\n", b) // 打印地址 0xc0003cdbb0
    fmt.Printf("%x\n", c) // 地址 c0002124b8


    type T struct {
        a string
        b int
    }
    t := T{a: "abc", b: 1}

    /*
        1. 将 t 的地址转为 Pointer:符合第一种
        2. 将 Pointer 转为 uintptr 后得到地址的整数值:符合第四种
        3. 加上 t.b 的offset,得到 t.b 的地址整数值:uintptr是整数,可以直接相加
        4. 将 uintptr 转为 Pointer:符合第三种
        5. 将 Pointer 转为 *int :符合第二种
        6. 最后解引用,得到具体的值
    */
    d := *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&t)) + unsafe.Offsetof(t.b)))
    fmt.Println(d) // 1
}

Pointer 越过了类型检查,可以直接操作底层的内存,因此使用时需要格外小心。对于 Pointer的操作,只有如下六种是合法的,其余的使用方式均为非法,我们一起来看下。

正确使用非类型安全指针

使用方式一:利用 Pointer 作为中介,完成 T1 类型 到 T2 类型的转换

T1T2 是任意类型,如果 T1 的内存占用大于等于 T2,并且 T1 和 T2 的内存布局一致,可以利用 Pointer 作为中介,完成 T1类型 到 T2类型的转换。(如果T1 的内存占用小于 T2,那么 T2 剩余部分没法赋值,就会有问题)

math 包中的 Float64bits 函数将一个 float64 值转换为一个 uint64值,Float64frombits 为此转换的逆转换,即 Float64bits(Float64frombits(x)) == x。

func Float64bits(f float64) uint64 {
    return *(*uint64)(unsafe.Pointer(&f)) 
}

func Float64frombits(b uint64) float64 {
    return *(*float64)(unsafe.Pointer(&b)) 
}

如下所示,slicestring 结构的底层布局类似,且 slice 的内存占用大于 string,我们可以利用此种方式完成 slice 到 string 的正确转换,但是无法正确完成 string 到 slice 的转换。

// slice 和 string 的底层结构
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

type stringStruct struct {
    str unsafe.Pointer
    len int
}
func main() {

    // slice 转 string,可以正确转换
    sli := []byte{'a', 'b', 'c'}
    str := *(*string)(unsafe.Pointer(&sli))
    fmt.Println(str)      // abc
    fmt.Println(len(str)) // 3

    // string 转 slice,cap 字段无法赋值,无法正确转换
    str = "1234"
    b := *(*[]byte)(unsafe.Pointer(&str))
    fmt.Println(string(b)) // 1234
    fmt.Println(len(b))    // 4
    fmt.Println(cap(b))    // 824634066744
}

slice 转为 string 后,两者对应的指针指向的是同一个字节数组,因此修改底层的数组值,string 相应的也会跟着改变。

func main() {

    // 字节数组转字符串
    sli := []byte{'a', 'b', 'c'}
    str := *(*string)(unsafe.Pointer(&sli))
    fmt.Println(str)      // abc
    fmt.Println(len(str)) // 3

    sli[0] = 'd'
    sli[1] = 'e'
    fmt.Println(str) // dec
}

使用方式二:将 Pointer 转为 uintptr (不再转回 Pointer)

Pointer 转为 uintptr,并且不再转回 Pointer,此方式用处不大,通常我们只用来打印值。

此方式相当于取变量的内存地址,由于 uintptr 是个变量值,而非引用,后续该变量被移动到其他位置,其对应的uintptr值不会更新;其次,如果后续没有使用该变量,随时可能会被垃圾回收掉。

// 每次运行得到的内存地址,可能不一样
func main() {
    a := int(10)
    fmt.Printf("%p\n", &a)                          // 0xc0001184b8
    fmt.Printf("%x\n", uintptr(unsafe.Pointer(&a))) // c0001184b8
}

因此,将 uintptr 转回 Pointer 是存在风险的,只有接下来我们列举的几种转换方式合法的。

使用方式三:将Pointer转为 uintptr,然后再通过算数方式将 uintptr 转回 Pointer

我们可以将一个变量的 Pointer 转为 uintptr,然后再加上一定的偏移量转回 Pointer,这种方式通常用来获取结构体中的成员变量地址或者数组中第i个元素的地址。

结构体:我们可以先拿到结构体变量 e 的地址,然后加上 成员b 的偏移量,就可以得到 e.b 的地址,再转回 Pointer 就能够拿到对应的值了。

func main() {

    type Example struct {
        a int32
        b string
    }

    e := Example{
        a: 1,
        b: "test",
    }

    // 等价于 *(*string)(unsafe.Pointer(&e.b))
    c := *(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&e)) + unsafe.Offsetof(e.b)))

    fmt.Println(c, d)
}

数组:拿到了数组第一个元素 a[0] 的地址,转为 uintptr 后,加上 2倍 个元素类型占用的内存大小,就可以得到第 3 个元素的地址值,再转回 Pointer,最后转为 int,就得到了第三个元素的值。

func main() {
    a := []int{1, 2, 3, 4}
    b := *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&a[0])) + 2*unsafe.Sizeof(a[0])))
    fmt.Println(b)
}

同理,获取一个成员或元素的地址,然后减去相应的偏移量,也是合法操作。但是无论怎么操作,需要保证最后得到的地址,是在当前变量占用的地址范围内,不能超出,如下几种就是非法的操作:

  • 非法操作一:超出变量内存范围
// 从初始地址,最多加  unsafe.Sizeof(s)-1
var s thing
end = unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Sizeof(s))
// 声明了 n 个字节的长度,从初始地址最多加 n-1
b := make([]byte, n)
end = unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(n))
  • 非法操作二:使用变量保存 uintptr 的值

在将 uintptr 类型转为 Pointer 类型之前,不能将 uintptr 的的值赋值给变量

// 非法操作示例
func main() {

    type Example struct {
        a int32
        b string
    }

    e := Example{
        a: 1,
        b: "test",
    }

    // 正确操作 c := *(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&e)) + unsafe.Offsetof(e.b)))
    addr := uintptr(unsafe.Pointer(&e)) + unsafe.Offsetof(e.b)

    // 到这里,变量 e 没有任何引用了,因此可能随时被垃圾回收器回收,一旦被回收,再使用 e.b 原来的地址将是非常危险的
    c := *(*string)(unsafe.Pointer(addr))

    fmt.Println(c)
」
  • 非法操作三:Pointer 指向 nil

Pointer 需要指向一个分配过内存的变量,不能指向 nil

// Pintere指向nil是非法的
u := unsafe.Pointer(nil)
p := unsafe.Pointer(uintptr(u) + offset)

使用方式四:将 Pointer 转为 uintptr, 传递给系统调用 syscall.Syscall

我们知道 uintptr 是一个整数,获取到了一个变量的 uintptr 值,并不能保证变量不被垃圾回收掉,如果变量被垃圾回收掉,使用原先的 uintptr 值将是非常危险的。

下面这个函数是危险的原因在于,函数本身不能保证传递进来的地址对应的内存块一定没有被回收。 如果此内存块已经被回收了或者被重新分配给了其它变量,那么此函数内部的操作将是非法和危险的。

func DoSomething(addr uintptr) {
    // 对处于传递进来的地址处的值进行读写...
}

然而系统调用则有这种特权,保证了地址对应的内存块在函数执行过程中不被回收和移动。例如 syscall 标准库包中的 Syscall 函数的原型为:

func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)

那么此函数是如何保证传递给它的地址参数值a1a2a3处的内存块在执行过程中一定没有被回收和被移动呢? 此函数无法做出这样的保证,事实上,是编译器做出了这样的保证。 这是 syscall.Syscall 这样函数的特权,其它自定义函数无法享受到这样的待遇。

正确的使用姿势为:

// 将 p 对应的 Pointer 值转为 uintptr
syscall.Syscall(SYS_READ, uintptr(fd), uintptr(unsafe.Pointer(p)), uintptr(n))

同时需要注意的是,我们也不能先将 uintptr 的值赋值给一个变量,然后再传入 syscall.Syscall

u := uintptr(unsafe.Pointer(p))
// 此时 p 可能被回收或者移动
syscall.Syscall(SYS_READ, uintptr(fd), u, uintptr(n))

使用方式五:将 reflect.Value.Pointer 或者 reflect.Value.UnsafeAddruintptr 值转为 unsafe.Pointer

reflect包中,Value 类型的 PointerUnsafeAddr方法都返回一个 uintptr 值,而不是 unsafe.Pointer 值,这样做是为了避免用户在没有引入 unsafe 包的条件下,就可以将这两个方法的返回值转为任意类型安全的指针。(比如返回值 a 是 unsafe.Pointer 类型,不引入unsafe包,可以直接进行(*int32)(a),将其转为 int32 类型的指针 )。

因此,这种设计需要我们在调用完 reflect.Value.Pointer 或者 reflect.Value.UnsafeAddr后,立即调用 unsafe.Pointer 转为 Pointer 类型,否则在调用的空窗期,变量可能被移动或者回收。

func main() {

    type Example struct {
        a int32
        b string
    }

    e := Example{
        a: 1,
        b: "test",
    }

    // 1. 正确使用方式
    b := *(*string)(unsafe.Pointer(reflect.ValueOf(&e.b).Pointer()))
    fmt.Println(b) // test

  // 2. 错误使用方式
    p := reflect.ValueOf(&e.b).Pointer()
    // 此时变量可能被移动或者回收
    b = *(*string)(unsafe.Pointer(p))
    fmt.Println(b) 
}

使用方式六:将 reflect.SliceHeader 或者 reflect.StringHeaderData 域对应的 uintptr 转为 Pointer,或者将其他 Pointer 转为 uintptr 赋值给 Data

slicestring 底层的数据结构如下:其中 slice 结构的 array 字段和 string 结构的 str 字段底层其实都指向 字节数组

SliceHeaderStringHeader 分别是 slicestring 结构的运行时表示,对于任意一个 slice 或者 string,我们可以拿到它的运行时表示,然后修改其 Data 值,达到修改其底层数据的目的。即我们可以将一个字符串的指针值 转换为 *reflect.StringHeader ,进而可以对此字符串的内部进行修改。类似,我们也可以将一个切片的指针值转换为 *reflect.SliceHeader ,从而对此切片的内部进行修改。

这样做的好处是,在不重新分配内存的情况下,将 stringslice 的底层数据改变。

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

type stringStruct struct {
    str unsafe.Pointer
    len int
}

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

type StringHeader struct {
    Data uintptr
    Len  int
}

和上面第五条同样的原因,为了避免用户没有引入 unsafe包 就可以直接转换, reflect.SliceHeader 或者 reflect.StringHeaderData 域都是 uintptr 类型。

// 修改字符串对应的Data域
func main() {

    str := "test"

  // 字节数组,修改后字符串底层数据指向这个数组
    a := [3]byte{'a', 'b', 'c'}

    strHeader := (*reflect.StringHeader)(unsafe.Pointer(&str))
    strHeader.Data = uintptr(unsafe.Pointer(&a))
    strHeader.Len = len(a)

    fmt.Println(str) // abc
}
func main() {

    sli := []byte{'h', 'e', 'l', 'l', 'o'}

    array := [4]byte{'1', '2', '3', '4'}

    // 将切片转为 reflect.SliceHeader 结构
    sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&sli))

    // 修改对应的字段数据,修改后 sli 底层的数据指向了 array
    sliceHeader.Data = uintptr(unsafe.Pointer(&array))

  // 先设置长度为2
    sliceHeader.Len = 2
    sliceHeader.Cap = len(array)
    fmt.Printf("%s\n", sli) // 12

    // 修改 sli 的长度
    sli = sli[:cap(sli)]
    fmt.Printf("%s\n", sli) // 1234

}

一般来说,我们应该从一个已经存在的字符串得到 *reflect.StringHeader,或者从一个已经存在的切片得到 *reflect.SliceHeader,不能直接声明 reflect.SliceHeaderreflect.StringHeader 变量:

// 错误使用方式
var hdr reflect.StringHeader
hdr.Data = uintptr(unsafe.Pointer(new([5]byte)))
 // 在此时刻,上一行代码中刚开辟的数组内存块已经不再被任何值所引用,所以它可以被回收
hdr.Len = n
s := *(*string)(unsafe.Pointer(&hdr)) // 危险

使用 reflect.SliceHeaderreflect.StringHeader,我们可以在不重新分配底层数据内存的情况下,完成 slicestring 类型互换:

// 字节切片转 string
func ByteSlice2String(slice []byte) (s string) {
    sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&slice))
    stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
    stringHeader.Data = sliceHeader.Data
    stringHeader.Len = sliceHeader.Len
    return
}

// string 转字节切片
func String2ByteSlice(s string) (slice []byte) {
    stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
    sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&slice))

    sliceHeader.Data = stringHeader.Data
    sliceHeader.Len = stringHeader.Len
    sliceHeader.Cap = stringHeader.Len
    return
}

func main() {

    b := []byte{'h', 'e', 'l', 'l', 'o'}
    fmt.Println(ByteSlice2String(b)) // hello

    s := "hello"
    fmt.Println(String2ByteSlice(s)) // [104 101 108 108 111]

}

由于默认字符串内存是分配在不可修改区的,使用上述的 String2ByteSlicestring 转为 slice 后,只能进行读取,不能修改其底层数据值:

func main() {

    s1 := "Goland" // 官方标准编译器会将 s1 的字节开辟在不可修改内存区

    b1 := String2ByteSlice(s1) // 转为字节数组
    fmt.Printf("%s\n", b1) // Goland

    // 由于字符串 s1 底层指向的字节数组在不可修改区,此时不能修改值,否则会panic
    // b1[5] = 'a'

    // 这种方式不会存放在不可修改区,转为字节数组后,可以修改值
    s2 := strings.Join([]string{"Go", "land"}, "")
    b2 := String2ByteSlice(s2)
    fmt.Printf("%s\n", b2) // Goland
    b2[5] = 'g' // 相当于修改底层数组的值,原字符串的值也会随之改变
    fmt.Println(s2) // Golang
}

本篇文章从类型安全指针切入,介绍了如何获取指针、为什么需要使用指针以及类型安全指针的局限性,然后进一步介绍了 unsafe 包中对于非类型安全指针类型 Pointer 的定义以及使用方法,最后通过具体示例详细介绍了六种正确使用 Pointer 的场景。

个人博客: https://lifelmy.github.io/

微信公众号:漫漫Coding路


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK