Go语言中局部变量的逃逸分析(从汇编的角度)
正常情况下,局部变量是存储在栈中的,如果将局部变量的地址当作函数值返回,这势必会导致悬挂指针的错误,因为函数返回后,函数的栈帧就会被回收,返回的局部变量地址自然就访问不到了。但是Go语言会进行逃逸分析,编译器如果遇到这种情况,就会将该变量分配到堆中而不是栈中,这样函数返回后,返回的地址自然就可以访问到之前的局部变量。
源码
package main
func main() {
n := escape()
*n = 20
print(*n)
}
//go:noinline
func escape() *int {
n := 11
return &n
}
本次使用的Go版本
jagitch@34c4dd4d4a3e:relations$ go version
go version go1.22.2 linux/amd64
编译
go build main.go
反编译
jagitch@34c4dd4d4a3e:go-asm$ go tool objdump -S -s "main.escape" main
TEXT main.escape(SB) /home/coder/workspace/own/jagitch-code/gitee/go-study/go-asm/main.go
func escape() *int {
0x45d160 493b6610 CMPQ SP, 0x10(R14)
0x45d164 7621 JBE 0x45d187
0x45d166 55 PUSHQ BP
0x45d167 4889e5 MOVQ SP, BP
0x45d16a 4883ec10 SUBQ $0x10, SP
n := 11
0x45d16e 488d050b5d0000 LEAQ 0x5d0b(IP), AX
0x45d175 e8e6edfaff CALL runtime.newobject(SB)
0x45d17a 48c7000b000000 MOVQ $0xb, 0(AX)
return &n
0x45d181 4883c410 ADDQ $0x10, SP
0x45d185 5d POPQ BP
0x45d186 c3 RET
func escape() *int {
0x45d187 e874cdffff CALL runtime.morestack_noctxt.abi0(SB)
0x45d18c ebd2 JMP main.escape(SB)
Go反汇编代码解析
CMPQ SP, 0x10(R14)
判断是否需要增长栈
JBE 0x45d187
如果需要则跳转到0x45d187执行增长栈的逻辑
PUSHQ BP
将BP压站
MOVQ SP, BP
保存main.escape
函数的栈帧基址
SUBQ $0x10, SP
在栈上分配16个字节的空间
LEAQ 0x5d0b(IP), AX
将堆上的一个地址保存到AX
CALL runtime.newobject(SB)
根据AX中的地址,创建对象
MOVQ $0xb, 0(AX)
将11保存到0(AX)
中,即保存到刚刚创建的对象的地址处
ADDQ $0x10, SP
清理栈帧
POPQ BP
恢复调用者的函数栈帧基址
RET
返回
CALL runtime.morestack_noctxt.abi0(SB)
调用运行时中的函数去扩展栈空间
JMP main.escape(SB)
跳转到函数开始重新执行函数
结论
从反汇编的代码中可以看出,将局部变量的地址返回时,该局部变量会使用CALL runtime.newobject(SB)
在堆中分配而不是分配在栈中,即使函数返回了,该指针指向的内存地址还是可以正常使用。