Go unsafe.Pointer uintptr原理和玩法

本文转至:https://www.cnblogs.com/sunsky303/p/11820500.html

unsafe.Pointer

这个类型比较重要,它是实现定位和读写的内存的基础,Go runtime大量使用它。官方文档对该类型有四个重要描述:
(1)任何类型的指针都可以被转化为Pointer
(2)Pointer可以被转化为任何类型的指针
(3)uintptr可以被转化为Pointer
(4)Pointer可以被转化为uintptr

大多数指针类型会写成T,表示是“一个指向T类型变量的指针”。unsafe.Pointer是特别定义的一种指针类型(译注:类似C语言中的void类型的指针),它可以包含任意类型变量的地址。当然,我们不可以直接通过*p来获取unsafe.Pointer指针指向的真实变量的值,因为我们并不知道变量的具体类型。和普通指针一样,unsafe.Pointer指针也是可以比较的,并且支持和nil常量比较判断是否为空指针。
一个普通的T类型指针可以被转化为unsafe.Pointer类型指针,并且一个unsafe.Pointer类型指针也可以被转回普通的指针,被转回普通的指针类型并不需要和原始的T类型相同。

package main

import (
   "fmt"
   "unsafe"
   "reflect"
)
type W struct {
   b byte
   i int32
   j int64
}

//通过将float64类型指针转化为uint64类型指针,我们可以查看一个浮点数变量的位模式。
func Float64bits(f float64) uint64 {
   fmt.Println(reflect.TypeOf(unsafe.Pointer(&f)))  //unsafe.Pointer
   fmt.Println(reflect.TypeOf((*uint64)(unsafe.Pointer(&f))))  //*uint64
   return *(*uint64)(unsafe.Pointer(&f))
}
func Uint(i int)uint{
   return *(*uint)(unsafe.Pointer(&i))
}
type Uint6 struct {
   low [2]byte
   high uint32
}
//func (u *Uint6) SetLow() {
// fmt.Printf("i=%d\n", this.i)
//}
//
//func (u *Uint6) SetHigh() {
// fmt.Printf("j=%d\n", this.j)
//}
func writeByPointer(){
   uint6 := &Uint6{}
   lowPointer:=(*[2]byte)(unsafe.Pointer(uint6))
   *lowPointer = [2]byte{1,2}
   //unsafe.Offsetof会计算padding后的偏移距离
   //必须将unsafe.Pointer转化成 uintptr类型才能进行指针的运算,uintptr 与 unsafe.Pointer 之间可以相互转换。
   highPointer:=(*uint32)(unsafe.Pointer(uintptr(unsafe.Pointer(uint6))+unsafe.Offsetof(uint6.high)))
   fmt.Printf("addr %x addr %x size %v size %v size %v align %v offset %v \n", uintptr(unsafe.Pointer(uint6)),uintptr(unsafe.Pointer(uint6))+unsafe.Sizeof(uint6.low),unsafe.Sizeof([2]byte{1,2}),unsafe.Sizeof(uint6.low), unsafe.Sizeof(uint6.high), unsafe.Alignof(uint6.low), unsafe.Offsetof(uint6.high))
   *highPointer = uint32(9)
   //借助于 unsafe.Pointer,我们实现了像 C 语言中的指针偏移操作。可以看出,这种不安全的操作使得我们可以在任何地方直接访问结构体中未公开的成员,只要能得到这个结构体变量的地址。
   // fmt.Printf("%+v %v %v %v \n", uint6, &uint6,&uint6.low[0], &uint6.high)
   fmt.Printf("%+v %p %p %v \n", uint6, uint6, &uint6.low[0], &uint6.high)
    // 输出结果【转者加的注释,并修改了代码,可以直观了解指针首地址指向哪里】
    // &{low:[1 2] high:9} 0xc0000a2008 0xc0000a2008 0xc0000a200c
    // 结构体值,结构体指针地址,&uint6.low[0]指针地址[结构体指针首地址],&uint6.high指针地址
}
type T struct {
   t1 byte
   t2 int32
   t3 int64
   t4 string
   t5 bool
}
func main() {
   fmt.Printf("%#x  %#b \n", Float64bits(11.3), Float64bits(4)) // "0x3ff0000000000000"
   var intA int =99
   uintA:=Uint(intA)
   fmt.Printf("%#v %v  %v \n", intA, reflect.TypeOf(uintA), uintA)
   var w W = W{}
   //在struct中,它的对齐值是它的成员中的最大对齐值。
   fmt.Printf("%v, %v, %v, %v, %v, %v, %v, %v\n", unsafe.Alignof(w), unsafe.Alignof(w.b), unsafe.Alignof(w.i), unsafe.Alignof(w.j), unsafe.Sizeof(w),unsafe.Sizeof(w.b),unsafe.Sizeof(w.i),unsafe.Sizeof(w.j), )

   fmt.Println(unsafe.Alignof(byte(0)))
   fmt.Println(unsafe.Alignof(int8(0)))
   fmt.Println(unsafe.Alignof(uint8(0)))
   fmt.Println(unsafe.Alignof(int16(0)))
   fmt.Println(unsafe.Alignof(uint16(0)))
   fmt.Println(unsafe.Alignof(int32(0)))
   fmt.Println(unsafe.Alignof(uint32(0)))
   fmt.Println(unsafe.Alignof(int64(0)))
   fmt.Println(unsafe.Alignof(uint64(0)))
   fmt.Println(unsafe.Alignof(uintptr(0)))
   fmt.Println(unsafe.Alignof(float32(0)))
   fmt.Println(unsafe.Alignof(float64(0)))
   //fmt.Println(unsafe.Alignof(complex(0, 0)))
   fmt.Println(unsafe.Alignof(complex64(0)))
   fmt.Println(unsafe.Alignof(complex128(0)))
   fmt.Println(unsafe.Alignof(""))
   fmt.Println(unsafe.Alignof(new(int)))
   fmt.Println(unsafe.Alignof(struct {
      f  float32
      ff float64
   }{}))
   fmt.Println(unsafe.Alignof(make(chan bool, 10)))
   fmt.Println(unsafe.Alignof(make([]int, 10)))
   fmt.Println(unsafe.Alignof(make(map[string]string, 10)))

   t := &T{1, 2, 3, "", true}
   fmt.Println("sizeof :")
   fmt.Println(unsafe.Sizeof(*t))
   fmt.Println(unsafe.Sizeof(t.t1))
   fmt.Println(unsafe.Sizeof(t.t2))
   fmt.Println(unsafe.Sizeof(t.t3))
   fmt.Println(unsafe.Sizeof(t.t4))
   fmt.Println(unsafe.Sizeof(t.t5))
    //这里以0x0作为基准内存地址。打印出来总共占用40个字节。
    // t.t1 为 char,对齐值为 1,0x0 % 1 == 0,从0x0开始,占用一个字节;
    // t.t2 为 int32,对齐值为 4,0x4 % 4 == 0,从 0x4 开始,占用 4 个字节;
    // t.t3 为 int64,对齐值为 8,0x8 % 8 == 0,从 0x8 开始,占用 8 个字节;
    // t.t4 为 string,对齐值为 8,0x16 % 8 == 0,从 0x16 开始, 占用 16 个字节(string 内部实现是一个结构体,包含一个字节类型指针和一个整型的长度值);
    // t.t5 为 bool,对齐值为 1,0x32 % 8 == 0,从 0x32 开始,占用 1 个字节。从上面分析,可以知道 t 的对齐值为 8,最后 bool 之后会补齐到 8 的倍数,故总共是 40 个字节。

   fmt.Println("Offsetof : ")
   fmt.Println(unsafe.Offsetof(t.t1))
   fmt.Println(unsafe.Offsetof(t.t2))
   fmt.Println(unsafe.Offsetof(t.t3))
   fmt.Println(unsafe.Offsetof(t.t4))
   fmt.Println(unsafe.Offsetof(t.t5))

   writeByPointer()
   //CPU看待内存是以block为单位的,就像是linux下文件大小的单位IO block为4096一样,
   //是一种牺牲空间换取时间的做法, 我们一定要注意不要浪费空间,
   //struct类型定义的时候一定要将占用内从空间小的类型放在前面, 充足利用padding, 才能提升内存、cpu效率
}

输出结果:

go run PLAY.go
unsafe.Pointer
*uint64
unsafe.Pointer
*uint64
0x402699999999999a 0b100000000010000000000000000000000000000000000000000000000000000 
99 uint 99 
8, 1, 4, 8, 16, 1, 4, 8
1
1
1
2
2
4
4
8
8
8
4
8
4
8
8
8
8
8
8
8
sizeof :
40
1
4
8
16
1
Offsetof : 
0
4
8
16
32
addr c00008e038 addr c00008e03a size 2 size 2 size 4 align 1 offset 4 
&{low:[1 2] high:9} 0xc00008a010 0xc00008e038 0xc00008e03c
image.png

uintptr

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

typedef unsigned long long int  uint64;
typedef uint64          uintptr;

一个unsafe.Pointer指针也可以被转化为uintptr类型,然后保存到指针型数值变量中(注:这只是和当前指针相同的一个数字值,并不是一个指针),然后用以做必要的指针数值运算。(uintptr是一个无符号的整型数,足以保存一个地址)这种转换虽然也是可逆的,但是将uintptr转为unsafe.Pointer指针可能会破坏类型系统,因为并不是所有的数字都是有效的内存地址。
许多将unsafe.Pointer指针转为原生数字,然后再转回为unsafe.Pointer类型指针的操作也是不安全的。比如下面的例子需要将变量x的地址加上b字段地址偏移量转化为*int16类型指针,然后通过该指针更新x.b:

package main

import (
    "fmt"
    "unsafe"
)

func main() {

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

    /**
    unsafe.Offsetof 函数的参数必须是一个字段 x.f, 然后返回 f 字段相对于 x 起始地址的偏移量, 包括可能的空洞.
    */

    /**
    uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
    指针的运算
    */
    // 和 pb := &x.b 等价
    pb := (*int16)(unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
    *pb = 42
    fmt.Println(x.b) // "42"
}

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

下面段代码是错误的:

// NOTE: subtly incorrect!
tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
pb := (*int16)(unsafe.Pointer(tmp))
*pb = 42

产生错误的原因很微妙。有时候垃圾回收器会移动一些变量以降低内存碎片等问题。这类垃圾回收器被称为移动GC。当一个变量被移动,所有的保存改变量旧地址的指针必须同时被更新为变量移动后的新地址。从垃圾收集器的视角来看,一个unsafe.Pointer是一个指向变量的指针,因此当变量被移动是对应的指针也必须被更新;但是uintptr类型的临时变量只是一个普通的数字,所以其值不应该被改变。上面错误的代码因为引入一个非指针的临时变量tmp,导致垃圾收集器无法正确识别这个是一个指向变量x的指针。当第二个语句执行时,变量x可能已经被转移,这时候临时变量tmp也就不再是现在的&x.b地址。第三个向之前无效地址空间的赋值语句将彻底摧毁整个程序!

总结

第一是 unsafe.Pointer 可以让你的变量在不同的指针类型转来转去,也就是表示为任意可寻址的指针类型。第二是 uintptr 常用于与 unsafe.Pointer 打配合,用于做指针运算,和C (*void)指针一样。

unsafe是不安全的,所以我们应该尽可能少的使用它,比如内存的操纵,这是绕过Go本身设计的安全机制的,不当的操作,可能会破坏一块内存,而且这种问题非常不好定位。

当然必须的时候我们可以使用它,比如底层类型相同的数组之间的转换;比如使用sync/atomic包中的一些函数时;还有访问Struct的私有字段时;该用还是要用,不过一定要慎之又慎。

还有,整个unsafe包都是用于Go编译器的,不用运行时,在我们编译的时候,Go编译器已经把他们都处理了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容