深度探索Go语言-第2章 指针
第2章 指针
2.1指针构成
声明一个指针变量的代码如下
// 变量名为p,其中的*int为变量的类型
// * 表名p是一个指针变量,用来存储一个地址,而int是真真的元素类型
// 当p中存在一个有效的地址的时候,改地址处的内存会被解释成int类型
var p * int
指针变量本身是一个无符号类型整形,变量大小容纳当前平台的地址;386架构是个32位无符号整形,amd64架构上是一个64为无符号整形
有着不同类型元素的指针被视为不同类型
2.1.1 地址
地址在运行阶段用来在进程的内存地址空间中确定给一个位置
指针一般会用到基址+位移的寻址方式
在arm64架构下通过go build命令编译一个示例文件 code_2_1.go,代码如下
package main
func main() {
n := 7
println(read(&n))
}
// go:noinline
func read(p *int) (v int) {
v = *p
return
}
生成一个二级制文件,命令如下
go build -gcflags "-N -l" code_2_1.go
使用go自带的objdump工具反编译 main.read()函数,代码如下
➜ compile go tool objdump -S -s "main.read" code_2_1
TEXT main.read(SB) /Users/weihailong/work/code/go/compile/code_2_1.go
func read(p *int) (v int) {
// 将ZR寄存器的值 复制到 16(RSR) 地址处
0x1000597d0 f9000bff MOVD ZR, 16(RSP)
v = *p
// 将8(RSP) 地址处的值 复制到 R0寄存器中
0x1000597d4 f94007e0 MOVD 8(RSP), R0
// 将(R0)地址处的值 复制到 R27寄存器中
0x1000597d8 3980001b MOVB (R0), R27
// 将(R0)地址处的值 复制到R0寄存器中
0x1000597dc f9400000 MOVD (R0), R0
// 将R0寄存器的值 复制到 16(RSP)地址处
0x1000597e0 f9000be0 MOVD R0, 16(RSP)
return
// 执行RET命令
0x1000597e4 d65f03c0 RET
0x1000597e8 00000000 ?
0x1000597ec 00000000 ?
2.1.2 元素类型
指针本身是个无符号整形,不会因为不同的元素类型而有所不同,元素类型会影响编译器对指针中存储的地址进行解释
验证代码如下
// go:noinline
func read32(p *int32) (v int32) {
v = *p
return
}
重新编译和反编译,得到的汇编代码如下
➜ compile go tool objdump -S -s "main.read32" code_2_1
TEXT main.read32(SB) /Users/weihailong/work/code/go/compile/code_2_1.go
func read32(p *int32) (v int32) {
// 将ZR寄存器的值 复制到 16(RSP)地址出, 这里的MOVW是在复制一个int32的值 这个值是10
0x1000597d0 b90013ff MOVW ZR, 16(RSP)
v = *p
// 将8(RSP)处的值 复制到 R0处 这个值是地址
0x1000597d4 f94007e0 MOVD 8(RSP), R0
// 将(R0)处的值 复制到 R27 处 这个是7
0x1000597d8 3980001b MOVB (R0), R27
// 将(R0)处的值 复制到 R0 处 这个是7
0x1000597dc b9800000 MOVW (R0), R0
// 将R0的值 复制到 16(RSP)处 这个是 7
0x1000597e0 b90013e0 MOVW R0, 16(RSP)
return
0x1000597e4 d65f03c0 RET
0x1000597e8 00000000 ?
0x1000597ec 00000000 ?
可以看到关于地址的运算的指令都没有变化,只有在是变量值7的时候才发生了变化
2.2 相关操作
一些指针特性受限于安全问题不能直接使用
2.2.1 取地址
地址获取方式:通过去地址运算符或者在动态分配内存时由new之类的函数返回
代码如下
package main
var n int
func main() {
println(addr())
}
// go:noinline
func addr() (p *int) {
return &n
}
编译
go build -gcflags "-N -l" code_2_3.go
反编译
➜ 2_3 go tool objdump -S -s "main.addr" code_2_3
TEXT main.addr(SB) /Users/weihailong/work/code/go/compile/2_3/code_2_3.go
func addr() (p *int) {
// 栈上分配空间 将ZR寄存器的值复制到8(RSP)处
0x1000597c0 f90007ff MOVD ZR, 8(RSP)
return &n
// 获取 606208(PC) 的地址 并装载到R0寄存器中
0x1000597c4 900004a0 ADRP 606208(PC), R0
// 将R0的值+3040 装载到R0中
0x1000597c8 912f8000 ADD $3040, R0, R0
// 将R0的值 装载到8(RSP)处
0x1000597cc f90007e0 MOVD R0, 8(RSP)
0x1000597d0 d65f03c0 RET
0x1000597d4 00000000 ?
0x1000597d8 00000000 ?
0x1000597dc 00000000 ?
2.2.2 解引用
通过指针中的地址去访问原来的变量就是解引用
- 空指针异常
就是地址值为0的内存页面不会被分配和映射,Go语言中对空指针进行解引用会造成成语panic - 野指针问题
一般是有指针变量为初始化造成的,Go语言生命的变量默认都会初始化为对应类型的零值,指针类型的变量都会初始化为nil,代码中的空指针判断逻辑能够避免空指针异常 - 悬挂指针问题
Go语言实现了自动内存管理,由GC负责释放堆内存对象,不会存在该问题
2.2.3 强制类型转换
基于指针的强制类型转化非常搞笑。两种不同类型指针间的转化需要用unsafe.Pointer作为中间类型,unsafe.Pointer可以和任意一种指针类型互相转换
代码如下
// go:noinline
func convert(p *int) {
q := (*int32)(unsafe.Pointer(p))
*q = 0
}
编译指令
go build -gcflags "-N -l" code_2_3.go
反编译指令
➜ 2_3 go tool objdump -S -s "main.convert" code_2_3
TEXT main.convert(SB) /Users/weihailong/work/code/go/compile/2_3/code_2_3.go
func convert(p *int) {
0x1000597b0 f81e0ffe MOVD.W R30, -32(RSP)
0x1000597b4 f81f83fd MOVD R29, -8(RSP)
0x1000597b8 d10023fd SUB $8, RSP, R29
q := (*int32)(unsafe.Pointer(p))
0x1000597bc f94017e0 MOVD 40(RSP), R0
0x1000597c0 f9000be0 MOVD R0, 16(RSP)
*q = 0
0x1000597c4 f9400be0 MOVD 16(RSP), R0
0x1000597c8 3980001b MOVB (R0), R27
// 只有这行不一样,根据值的类型使用不同的指令
0x1000597cc b900001f MOVW ZR, (R0)
}
0x1000597d0 910083ff ADD $32, RSP, RSP
0x1000597d4 d10023fd SUB $8, RSP, R29
0x1000597d8 d65f03c0 RET
0x1000597dc 00000000 ?
2.3 unsafe包
unsafe.Pointer进行指针的强制类型转化和指针运算
string的结构体
data | len |
---|
slice 的结构体
data | len | cap |
---|
2.3.1 标准库与keyword
unsafe包提供的内容
// 一个任意类型的定义
type ArbitraryType int
// 指针类型定义
type Pointer *ArbitraryType
// 3个工具函数原型(只有原型,没有实现)
func Sizeof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Alignof(x ArbitraryType) uintptr
这些都是编译器直接支持的,出于安全考虑放在了unsafe包中
2.3.2 关于uinitptr
unsafe.Pointer可以和uintptr互相转换,所以Go语言将指针转换为uintptr进行数值运算,然后转换为原类型
需要注意的是:不要用uintptr来存储对象上的地址,具体原因与GC有关,GC在标记对象时会跟踪指针类型,而uintptr不属于指针,所以会被GC忽略造成堆上对象被认为不可达,进而被释放。用unsafe.Pointer不会存在这个问题
2.3.3 内存对齐
Go语言的内存对齐规则有两个因素
- 数据类型自身的大小,复合类型会参考最大成员的大小
- 硬件平台机器字长
对于struct而言,每个成员都会以结构体的起始地址作为基地址,按自身类型的对齐边界对齐,所以会在相邻成员之间和最后一个成员之后添加padding
来源
封幼林的《深度探索Go语言》