过程(Procedures)是软件中一种重要的抽象,提供了一种封装代码的方式,用一组参数和可选的返回值实现某种功能。栈(stack)结构具有FILO特性,与过程调用的语义一致,因此适用于管理过程调用的内存空间。栈和寄存器组成的上下文为过程调用提供了传递控制、数据和内存分配等相关信息。
Stack
栈支持push和pop两个基本操作,一般使用寄存器%ebp和%esp指示当前栈帧的范围,指令如下:
pushl Src | popl Dest |
---|---|
1. 从src位置(内存/寄存器)取值 | 1. 从%esp位置取值 |
2. 移动栈指针%esp - 4 | 2. 写入dest位置 |
3. 写入%esp位置 | 3. 移动栈指针%esp + 4 |
Procedure
过程调用需要解决以下基本问题:
- 被调用者需要知道参数位置
- 被调用者需要知道结束后返回到哪条指令继续执行,即返回地址
- 调用者需要知道返回值在哪里
- 调用者和被调用者共用CPU的寄存器,需要考虑保存和恢复
一种实现流程如下图示意:
- caller准备好args后,跳转到callee执行
- callee计算完成设置返回值后,清空当前栈帧,返回到caller的指定位置继续执行
- caller在特定位置找到返回值
- caller和callee共同完成寄存器的保存和恢复
调用和返回指令的一般形式如下,通过在指定指令地址和返回地址间跳转实现控制转移:
call label | ret |
---|---|
1. push return address | 1. pop return address |
2. jump to label | 2. jump to address |
IA32通常使用寄存器传递返回值。首先caller或callee保存%eax,然后callee写入具体值或地址到%eax,caller从中取得返回值后,恢复%eax到原状态。
基于栈的过程调用如下图示意:
有两点值得注意:
- 当函数可以用寄存器保存局部变量,且不会调用其他函数时,该函数不需要使用栈帧
- 当函数调用处在当前过程的最后一步时(尾调用),可以直接复用当前栈帧,因为原有信息不会再使用
Go Closure
Go语言中函数为一等公民,定义为 runtime.funcval
结构体,包含一个指针指向代码段
1type funcval struct {
2 fn uintptr
3}
普通函数变量在编译期间会在数据段生产一个共用的 funcval
对象,匿名函数字面量则会在堆上分配 funcval
对象
函数内部引用了外部定义的自由变量后,成为闭包(closure)。闭包除了指向代码段的指针外,还包含自由变量的捕获列表。
- 被捕获的自由变量(局部变量、参数或返回值),将进行堆分配
- 闭包一定是func literal,因此其
funcval
也是堆分配
Goroutine的栈由runtime管理
- Go栈内存和堆内存一样,通过多级缓存机制进行分配
- 编译时插入栈增长检测代码,配合GC时检测收缩,实现栈空间的大小动态调整
- 编译时在函数结束时插入清理逻辑:写入返回值,释放栈帧,处理defer,恢复到caller
- 在栈上分配空间传递返回值,支持多返回值
一个closure的栈内存示例如下: