过程(Procedures)是软件中一种重要的抽象,提供了一种封装代码的方式,用一组参数和可选的返回值实现某种功能。栈(stack)结构具有FILO特性,与过程调用的语义一致,因此适用于管理过程调用的内存空间。栈和寄存器组成的上下文为过程调用提供了传递控制、数据和内存分配等相关信息。

Stack

栈支持push和pop两个基本操作,一般使用寄存器%ebp和%esp指示当前栈帧的范围,指令如下:

pushl Srcpopl Dest
1. 从src位置(内存/寄存器)取值1. 从%esp位置取值
2. 移动栈指针%esp - 42. 写入dest位置
3. 写入%esp位置3. 移动栈指针%esp + 4

Procedure

过程调用需要解决以下基本问题:

  • 被调用者需要知道参数位置
  • 被调用者需要知道结束后返回到哪条指令继续执行,即返回地址
  • 调用者需要知道返回值在哪里
  • 调用者和被调用者共用CPU的寄存器,需要考虑保存和恢复

一种实现流程如下图示意:

  • caller准备好args后,跳转到callee执行
  • callee计算完成设置返回值后,清空当前栈帧,返回到caller的指定位置继续执行
  • caller在特定位置找到返回值
  • caller和callee共同完成寄存器的保存和恢复

调用和返回指令的一般形式如下,通过在指定指令地址和返回地址间跳转实现控制转移:

call labelret
1. push return address1. pop return address
2. jump to label2. 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的栈内存示例如下: