【Golang】unsafe包使用方法总结——“不安全”的指针操作

一、概述

Go虽然具有指针,但出于安全性的设计,与C/C++的指针相比,存在诸多限制:

  • Go的指针不能进行数学计算。
  • 不同类型的指针之间无法相互转换,也不能相互赋值。
  • 不同类型的指针不能使用==和!=比较

但Go团队并没有完全没收编程人员操纵指针的自由,提供了unsafe包的unsafe.Pointer类型作为非安全的指针,并提供了少量但足够的方法,让我们能像在C/C++上一样进行“无视类型”、“放飞自我”的指针操作。虽然,像包名一样,这种操作是不安全的、不被官方推荐的,但在某些场景中,直接操作指针将更加高效,在源码中,可以看到大量unsafe包的使用。

这个包的存在,也体现出Go团队努力兼顾安全效率的实用主义设计理念。

因为实现直接关系到编译器源码,在此只总结一下该包的使用方法。

二、使用方法

unsafe包中共包含了两个类型与三个函数:

type ArbitraryType int     //代表着Go中任意类型
type Pointer *ArbitraryType  //代表指向任意类型的指针,可以与任意*Type类型相互转换
func Sizeof(x ArbitraryType) uintptr  //返回变量在内存中所占的字节数
func Offsetof(x ArbitraryType) uintptr //返回变量指定属性的偏移量
func Alignof(x ArbitraryType) uintptr  //返回变量对齐字节数量

三个函数都是在编译期间执行,它们的结果可以直接复制给const类型变量,另外,因为三个函数执行的结果和操作系统、编译器相关,所以是不可移植的。

1、unsafe.Pointer类型的使用

unsafe.Pointer类型可以与任何指针类型相互转换,并支持指针判等操作。

type object struct{
	f float64
	i int32
}
func main(){
	obj:=&object{
		f: 1.0,
		i: 1,
	}
	f:=(*float64)(unsafe.Pointer(obj))
	fmt.Println(*f)  //1
	fmt.Println(unsafe.Pointer(obj)==unsafe.Pointer(f))  //true
}

unsafe.Pointer类型仍然不支持数学运算,但可以将unsafe.Pointer类型转换为uintptr类型进行自由的数学计算。

// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr
  • uintptr并不是一个指针类型,是一个整数类型,Go中用该类型描述一个指针指向的真实内存地址,其单位是byte。
  • 因为本质上是一个整数类型,因此,即使uintptr变量仍然有效,由uintptr变量表示的地址处的数据也可能被GC回收。

普通指针类型、unsafe.Pointer、uintptr存在如下的转换关系:

【Golang】unsafe包使用方法总结——“不安全”的指针操作_第1张图片

我们可以通过上述类型转换来达到指针运算的目的。

  1. 将任意指针型*Type转换为通用指针类型unsafe.Pointer
  2. 将通用指针unsafe.Pointer转换为uintptr,得到真实的内存地址
  3. 对uintptr进行操作,加减偏移量,得到想要的地址。
  4. 将uintptr转化为unsafe.Pointer,再转化为想要的指针类型*Type

在下面的代码中,我们可以使用这种方式,用一个object实例的指针,得到第二个字段i的指针。

type object struct{
	f float64
	i int32
}
func main(){
	obj:=&object{
		f: 2.0,
		i: 1,
	}
	fmt.Println(uintptr(unsafe.Pointer(obj)))   //obj指向第一个字节的的内存实际地址824634236704
	i:=(*int32)(unsafe.Pointer(uintptr(unsafe.Pointer(obj))+8))   //在第一个字节的基础上,加上一个fload64偏移量8
	fmt.Println(*i)    //1     
} 

即使在包外,即使字段是非导出的(首字母小写)或者说私有的,我们仍可以通过这种方式,从底层指针绕开Go的类型检查来操作变量。

2、unsafe.Sizeof函数的使用

unsafe.Sizeof函数返回变量在内存中占用的字节数,返回值为uintptr类型,可以直接用于偏移量的计算。该函数只会对变量基本结构进行分析,对于指针操作仅得指针大小,不能得到其指向数据的大小,对于slice、map、channel也是一样,并不能得到类型背后数据的总大小。

type object struct{
	f float64
	i int32
}
func main(){
	obj:=object{
		f: 2.0,
		i: 1,
	}
	integer:=1
	float:=1.0
	// 对于非指针类型,获得这个类型在内存中占用空间大小
	fmt.Println(unsafe.Sizeof(integer)," ",unsafe.Sizeof(float)," ",unsafe.Sizeof(obj))   //8 8 16
	obj_ptr:=&obj
	int_ptr:=&integer
	f_ptr:=&float
	//对于指针的操作,都将得到8字节(与操作系统相关)的指针大小
	fmt.Println(unsafe.Sizeof(int_ptr)," ",unsafe.Sizeof(f_ptr)," ",unsafe.Sizeof(obj_ptr))   //8   8   8

	//make新建的map,返回的是一个hmap的指针,因此长度仍然是一个指针长度
	m:=make(map[int]int)
	fmt.Println(unsafe.Sizeof(m))  //8

	//新建的channel是一个hchan指针
	ch:=make(chan int)
	fmt.Println(unsafe.Sizeof(ch))  //8

	//新建的slice将返回一个slice header,大小固定,与slice长度无关
	s1:=make([]int,10)
	s2:=make([]int,20)
	fmt.Println(unsafe.Sizeof(s1))   //24
	fmt.Println(unsafe.Sizeof(s2))   //24

	//对于一个数组将返回占用的真实空间
	array:=[24]int{}
	fmt.Println(unsafe.Sizeof(array))   //192
}

3、unsafe.Offsetof函数使用

前面我们可以看到,我们可以使用unsafe包操作一个结构体指针,获得指向其具体属性的指针:

type object struct{
	f1 float64
	f2 float64
	s string
}
func main(){
	obj_ptr:=&object{
		f1: 2.0,
		f2:2.0,
		s:"helloworld",
	}
	s_ptr:=(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(obj_ptr))+
		unsafe.Sizeof(float64(0))+unsafe.Sizeof(float64(0))))
	fmt.Println(*s_ptr)  //helloworld
}

对于一个特定位置的字段,我们需要将前面所有的字段都考虑到来计算偏移量,这种工作比较繁杂。

对于可访问(在同一包内或者被导出)的字段类型,unsafe包提供了一种更优雅的方式:

type object struct{
	f1 float64
	f2 float64
	s string
}
func main(){
	obj_ptr:=&object{
		f1: 2.0,
		f2:2.0,
		s:"helloworld",
	}
	s_ptr:=(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(obj_ptr))+unsafe.Offsetof(obj_ptr.s)))
	fmt.Println(*s_ptr)  //helloworld
}

unsafr.Offsetof函数返回变量指定属性的偏移量,即指定字段在当前结构体中偏移大小,该函数的参数必需是struct类型的一个属性,因此,无法在包外对一个没有被导出的属性使用该方法。

4、unsafe.Alignof函数使用

修改上面的代码,为object字段设置一个int型属性,再尝试通过计算偏移量,获得s的指针。

type object struct{
	f float64
	i int32
	s string
}
func main(){
	obj_ptr:=&object{
		f: 2.0,
		i:1,
		s:"helloworld",
	}
	s_ptr:=(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(obj_ptr))+
		unsafe.Sizeof(float64(0))+unsafe.Sizeof(int32(0))))
	fmt.Println(*s_ptr)
}

 结果将输出一个panic

runtime: VirtualAlloc of 42949672960 bytes failed with errno=1455
fatal error: out of memory

这是因为数据在内存中的存储并不是简单的排列 ,偏移量并不是简单的属性大小相加。

熟悉计算机组成和内存结构的人都知道,这里需要考虑一个对齐量的问题,其原理这里不再赘述。

使用unsafe.Alignof函数可以获得变量的对齐字节数量:

	fmt.Println(unsafe.Alignof(obj_ptr))//8

根据对齐量来考虑属性的储存情况,才能计算到真正的偏移量。

type object struct{
	f float64
	i int32
	s string
}
func main(){
	obj_ptr:=&object{
		f: 2.0,
		i:1,
		s:"helloworld",
	}
	fmt.Println(unsafe.Alignof(*obj_ptr))//8
	fmt.Println(unsafe.Sizeof(float64(0)))//8
	fmt.Println(unsafe.Sizeof(int32(0)))//4   //不足8,需要补齐 
	s_ptr:=(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(obj_ptr))+
		8+8))
	fmt.Println(*s_ptr)   //helloworld
}

reflect包也提供了函数来获得类型的对齐值:

	fmt.Println(reflect.TypeOf(obj_ptr).Elem().Align())

三、进阶使用实例

1、获取slice的长度并更改

runtime/slice.go中slice的结构:

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

了解这个结构后,虽然len和cap并不是可以被访问的,我们仍然可以通过unsafe包修改:

func main(){
	s:=make([]int,10)
	fmt.Println(len(s))  //10
	slen:=(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s))+uintptr(8)))
	fmt.Println(*slen)  //10
	*slen=5
	fmt.Println(len(s))   //5
}

2、获取 map 长度

map.go中hmap定义如下:

type hmap struct {
	// Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.
	// Make sure this stays in sync with the compiler's definition.
	count     int // # live cells == size of map.  Must be first (used by len() builtin)
	flags     uint8
	B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
	noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
	hash0     uint32 // hash seed

	buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
	oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
	nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)

	extra *mapextra // optional fields
}

可以看到,count字段被放在了hmap的第一个属性,因此,可以很容易通过unsafe包获得map的长度:

func main(){
	m:=make(map[int]int)
	m[0]=0
	m[1]=0
	m[2]=0
	count:=**(**int)(unsafe.Pointer(&m))
	fmt.Println(count)   //3
}

可以看到,如果要转换的类型不能被识别为一个指针,我们可以通过二级指针来处理。

3、string和slice的相互转换

利用reflect包中的StringHeader和SliceHeader与string、slice的结构一致,我们可以巧妙地完成一次“高端转换”。

func string2bytes(s string) []byte {
	stringHeader:=(*reflect.StringHeader)(unsafe.Pointer(&s))
	byteSlice:=reflect.SliceHeader{
		Data: stringHeader.Data,
		Len:  stringHeader.Len,
		Cap:  stringHeader.Len,
	}
	return *(*[]byte)(unsafe.Pointer(&byteSlice))
}
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))
}

 

你可能感兴趣的:(Golang)