1 规范库中结构体属性偏移量和指针转化
咱们知道go是类型安全的言语(大部分)。
所谓类型安全是一块内存数据一旦被特定类型解说,该内存数据与该类型变量树立关联。
在c言语这样的非类型安全言语中,能够声明为 int 类型,运用时经过c代码的内存地址的置换,解说为字符串类型。
类型安全是树立在编译器的静态检查以及 运转时runtime, 运用类型信息进行的运转时检查的。
在语法层面,为了完结常规类型安全,Go言语做了如下约束:
1,不支持隐式类型转化,所有类型转化有必要显式履行,如下过错把int 给 uint64 赋值
var i int = 17
var j uint64 = i
只需底层类型 underlying type 相同的两个类型的指针之间才能进行类型转化。
如下 过错 *int类型不能转化为 *uint64,而Newint底层类型为int,因而能够与int彼此转化。
失败转化:
var i int = 11
var p *uint64 = (*uint64)(&i)
成功转化:
type Newint int
var p *Newint = (*Newint)(&i)
2 不支持指针运算
如下过错,*int类型和int类型无法相加,不能跨过数组元素的鸿沟
var a [100]int
var p *int = &a[0]
*(p+1) = 10
Go的 类型安全 , 兼顾了功能和完结,而且与操作体系,以及C代码怎么交互操作。
示例的变量i,一旦被解说为int类型 a := 0x12345678, 它就与某个内存(开始地址为 &i, 长度为int类型的巨细) 树立了关联。
那么这块 &i 就不能再与其他类型 如 uint64 变量树立关联。
1.1 规范库包的偏移量和指针转化
- Offsetof
Offsetof 用于获取结构体中某字段的地址偏移量(相对于结构体变量的地址)。
仅用于求结构体某字段的偏移量。
回来x所代表的字段在结构中的偏移量。有必要是structValue.field的形式。
换句话说,它回来的是结构的开端和字段的开端之间的字节数。
假如参数x的类型不是可变巨细的,Offsetof的回来值是一个Go常数。
不具有可变巨细。
(关于可变巨细类型的界说,请参见[Sizeof]的描绘)。
偏移量
t.Log(unsafe.Offsetof(f.a)) // 开始偏移量 0
t.Log(unsafe.Offsetof(f.b)) //
t.Log(unsafe.Offsetof(f.c))
t.Log(unsafe.Offsetof(f.d))
t.Log(unsafe.Offsetof(f.e)) // 空结构体偏移量
t.Log(unsafe.Offsetof(f.ff)) // 空结构体偏移量为0,与上一个相同
t.Log(unsafe.Offsetof(f.fbs)) // 切片字段结构体偏移量 24
t.Log(unsafe.Offsetof(f.fb)) // 字符字段结构体偏移量 24
- Pointer
unsafe.Pointer 代表一个指向恣意类型的指针。
可用于其他类型的指针,而其他类型的指针则不可用。 有四个特殊的操作
-
A 任何类型的指针值都能够被转化为一个Pointer。
方针类型成果是相同的。
-
B 一个Pointer能够被转化为任何类型的指针值。
需求留意挑选对转化为的方针指针类型,不然或许在运转时才发现报错。 go vet 辅佐。
-
C 一个uintptr能够被转化为一个Pointer。
var i uintptr = 0x80010203 p := unsafe.Pointer(i)
-
D 一个Pointer能够被转化为一个uintptr。
p := unsafe.Pointer(&a) var i = uintptr(p)
比如A:
func TestPointer(t *testing.T) {
var (
a int = 5
b float64 = 5.89
arr [10]string
f FuncFoo
)
defPointer := (unsafe.Pointer)(nil)
p1 := (unsafe.Pointer)(&a)
if reflect.TypeOf(p1) != reflect.TypeOf(defPointer) {
ErrorHandler(fmt.Sprintf("Pointer: %v not type %v", reflect.TypeOf(p1), reflect.TypeOf(defPointer)), t)
}
p2 := (unsafe.Pointer)(&b)
if reflect.TypeOf(p2) != reflect.TypeOf(defPointer) {
ErrorHandler(fmt.Sprintf("Pointer: %v not type %v", reflect.TypeOf(p2), reflect.TypeOf(defPointer)), t)
}
p3 := (unsafe.Pointer)(&arr)
if reflect.TypeOf(p3) != reflect.TypeOf(defPointer) {
ErrorHandler(fmt.Sprintf("Pointer: %v not type %v", reflect.TypeOf(p3), reflect.TypeOf(defPointer)), t)
}
p4 := (unsafe.Pointer)(&f)
if reflect.TypeOf(p4) != reflect.TypeOf(defPointer) {
ErrorHandler(fmt.Sprintf("Pointer: %v not type %v", reflect.TypeOf(p4), reflect.TypeOf(defPointer)), t)
}
}
因而,Pointer答应一个程序绕过类型体系,并读写恣意的内存。运用它时应该非常当心。
如上它能够把恣意类型的指针,转化为 Pointer
也能够把Pointer 转化为恣意类型的指针,需求留意找到类型:
Pointer 转化为恣意类型的指针
var pa = (*int)(p2)
t.Logf("Pointer -> *int:%#v \n", pa)
var paf = (*float64)(p2)
t.Logf("Pointer -> *float64:%#v \n", paf)
设置能够把实践上是int的 指向一个 自界说结构体,编译器不会报错,可是运转将报错
var pas = (*FuncFoo)(p2)
t.Logf("Pointer -> *FuncFoo:%#v \n", pas)
假如转化过错,编译器不会发现,只需在履行运转的时候将报错:
unexpected fault address 0x20202020
fatal error: fault
[signal 0xc0000005 code=0x0 addr=0x20202020 pc=0x37a102]
goroutine 8 [running]:
runtime.throw({0x41777d?, 0x0?})
....
以下涉及指针的形式是有用的。
不运用这些形式的代码今天或许是无效的,或在将来变得无效。
即便是下面这些有用的形式,也有一些重要的留意事项。
运转 “go vet “能够帮助找到不符合这些形式的Pointer的运用。 但 “go vet “的变量遮蔽并不能保证代码是有用的。
go vet -c 1 -v -x .\main_test.go
1.2 将一个* T1转化为* T2的指针。
只需T2不比T1大,而且二者共享一个等效的这种转化答应将一种类型的数据从头解说为另一种类型的数据。
一个比如是完结 math.Float64bits。
应用实例,类型转化 float64 到 uint64
func Float64bits(f float64) uint64 {
return *(*uint64)(unsafe.Pointer(&f))
}
var vp uint32 = 0x12345678
输出 0x12345678
fmt.Printf("vp:0x%x\n", vp)
运用unsafe Pointer性质,恣意类型转化为Pointer
p := (unsafe.Pointer)(&vp)
运用unsafe.Pointer 性质2, Pointer转化为恣意类型
bp := (*[4]byte)(p)
bp[0] = 0x23
bp[1] = 0x45
bp[2] = 0x67
bp[3] = 0x8a
fmt.Printf("vp:0x%x\n", vp) // 0x12345678
运转输出:
vp:0x12345678
vp:0x8a674523
这儿原本被解说为 uint32类型的一段内存,开始地址为 &vp,长度4字节,经过unsafe.Pointer 被从头解说为 [4]byte 而且经过变量 b *[4]byte 类型 能够对该内存进行修改。
1.3 将一个Pointer 转化为一个uintptr(但不回来到指针)。
将一个Pointer转化为一个uintptr会产生所指向的值的内存地址。
指向的内存地址,作为一个整数。这样一个 uintptr 的一般用处是打印它。
将一个uintptr转化回Pointer在一般状况下是无效的。
uintptr是一个整数,而不是一个引证。
将一个 Pointer 转化为一个 uintptr 会创立一个整数值。
没有指针的语义。即便一个 uintptr 持有某个目标的地址。
垃圾收集器将不会更新该uintptr的值假如该目标移动,uintptr也不会使该目标不被收回。
剩余的形式列举了唯一有用的转化方法,从 uintptr 到 Pointer。
1.4 将一个指针转化为一个uintptr,再转化回来,并进行算术运算。
假如p指向一个被分配的目标,它能够经过转化为uintptr,加上一个偏移量,再转化回Pointer,在该目标中行进。
经过转化为uintptr,加上一个偏移量,再转化回Pointer。
p = unsafe.Pointer(uintptr(p) + offset)
这种形式最常见的用处是拜访一个结构中的字段或数组中的元素或数组中的元素。
相当于
f := unsafe.Pointer(&s.f)
f := unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f))
也相当于
e := unsafe.Pointer(&x[i])
e := unsafe.Pointer(uintptr(unsafe.Pointer(&x[0]) ) + i*unsafe.Sizeof(x[0]))
以这种方法从指针中增加和减去偏移量都是有用的。
运用&^对指针进行取整也是有用的,一般是为了对齐。
在所有状况下,成果有必要持续指向原来分配的目标。
与C言语不同的是,将一个指针推进到其原始分配的终点之后是无效的。它遵守原始分配。
非法操作:端点在分配空间之外。
var s thing
end = unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Sizeof(s))
非法操作: 端点在分配的空间之外。
b := make([]byte, n)
end = unsafe.Pointer(uintptr(unsafe.Pointer(&b[0]) ) + uintptr(n))
留意,这两种转化有必要出现在同一个表达式中,以核算它们之间的算术。
非法操作: 在转化回指针之前,uintptr不能被存储在变量中。
u := uintptr(p)
p = unsafe.Pointer(u + offset)
留意,这个指针有必要指向一个已分配的目标,所以它不或许是nil。
非法操作: 转化为nil的指针
u := unsafe.Pointer(nil)
p := unsafe.Pointer(uintptr(u) + offset)
1.5 调用syscall.Syscall,将一个Pointer转化为一个uintptr。
在软件包syscall中的Syscall函数直接将它们的uintptr参数传递给操作体系。
直接传递给操作体系,操作体系或许会根据调用的细节,将一些参数从头解说为指针。
也就是说,体系调用的完结是隐含地将某些参数从uintptr转回指针。
假如一个指针参数有必要被转化为uintptr才能作为参数运用。
这种转化有必要出现在调用表达式自身。
syscall.Syscall(SYS_READ, uintptr(fd), uintptr(unsafe.Pointer(p)), uintptr(n) )
编译器会处理在调用一个在SYS_READ中完结的函数的参数列表中转化为uintptr的Pointer。
对一个用汇编完结的函数的调用,组织被引证的目标,假如有的话,被分配的目标将被保留,而且在调用完结之前不被移动。
即便仅从类型上看,该目标在调用过程中不再需求。
为了使编译器能够识别这种形式。 转化有必要出现在参数列表中。
非法操作: uintptr不能被存储在变量中。
在体系调用时,在隐式转化回指针之前。
u := uintptr(unsafe.Pointer(p))
syscall.Syscall(SYS_READ, uintptr(fd), u, uintptr(n) )
1.6 值指针和地址的转化 reflect.Value.Pointer或reflect.Value.UnsafeAddr 。
从 uintptr 到 Pointer。
封装 reflect 的名为 Pointer 和 UnsafeAddr 的 Value 方法回来 uintptr 类型。
而不是 unsafe.Pointer,以避免调用者在没有导入 “unsafe.Pointer “的状况下,将成果改为恣意的类型。
以避免调用者在没有导入 “unsafe “的状况下将成果改为恣意类型。
然而,这意味着成果是脆弱的,有必要在调用后立即转化为Pointer。
在同一个表达式中。
p := (*int)(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer() )
和上面的状况相同,在转化之前存储成果是无效的。
非法操作: 在转化回Pointer之前,uintptr不能被存储在变量中。
u := reflect.ValueOf(new(int)).Pointer()
p := (*int)(unsafe.Pointer(u))
1.7 数据域转化为指针或从指针转化到数据
reflect.SliceHeader 或 reflect.StringHeader
与前面的状况相同,反射数据结构SliceHeader和StringHeader。
将字段 Data 声明为 uintptr,以避免调用者在没有导入的状况下将成果改变为一个恣意的类型,而不先导入 “unsafe”。
SliceHeader和StringHeader只需在解说实践切片或字符串的内容时才有用。
var s string
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) // case 1
hdr.Data = uintptr(unsafe.Pointer(p)) // case 6 (this case)
hdr.Len = n
在这种用法中,hdr.Data实践上是指字符串头中的底层指针的另一种方法。
字符串头中的指针,而不是一个 uintptr 变量自身。
一般来说,reflect.SliceHeader和reflect.StringHeader应该被用作仅作为reflect.SliceHeader和reflect.StringHeader运用,
指向实践的切片或字符串,而不是作为普通的结构。程序不应该声明或分配这些结构类型的变量。
-
非法操作:
一个直接声明的头不会把数据作为一个引证。var hdr reflect.StringHeader hdr.Data = uintptr(unsafe.Pointer(p)) hdr.Len = n
p或许现已丢掉了
s := *(*string)(unsafe.Pointer(&hdr))
小结
经过unsafe包,咱们能够完结功能更高,与底层体系交互更简单的低级代码。
它的存在也让咱们有了绕过Go类型安全屏障的路。
可是一旦运用不当,或许导致安全漏洞,简单引发程序panic过错。
正如官方介绍所讲,导入unsafe包或许是不被提倡的,而且不符合 GO 1 兼容性准则的。