深度探索go语言

第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 解引用

通过指针中的地址去访问原来的变量就是解引用

  1. 空指针异常
    就是地址值为0的内存页面不会被分配和映射,Go语言中对空指针进行解引用会造成成语panic
  2. 野指针问题
    一般是有指针变量为初始化造成的,Go语言生命的变量默认都会初始化为对应类型的零值,指针类型的变量都会初始化为nil,代码中的空指针判断逻辑能够避免空指针异常
  3. 悬挂指针问题
    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的结构体

datalen

slice 的结构体

datalencap

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语言的内存对齐规则有两个因素

  1. 数据类型自身的大小,复合类型会参考最大成员的大小
  2. 硬件平台机器字长
    对于struct而言,每个成员都会以结构体的起始地址作为基地址,按自身类型的对齐边界对齐,所以会在相邻成员之间和最后一个成员之后添加padding

来源

封幼林的《深度探索Go语言》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值