Skip to main content

Go: Stack

📅 2026-02-05 ✏️ 2026-03-07 CS GO

1 · stack#

每个goroutine都有自己的栈; 栈初始很小(2KB),按需增长或者收缩; 使用#连续栈,不够时会自动扩容(分配更大新栈,把旧栈内容移动过去); 栈由一帧一帧的函数调用组成; 栈上放函数参数、返回值、局部参数等(如果放不下会逃逸到堆上);

按需增长/收缩,说明是运行时控制的,先看看栈是如何定义的, 其次是操作,怎么触发的扩容,怎么扩容,怎么把旧栈内容移动到新栈,说完扩容,就有缩容了。

g定义中可知,

函数调用栈:todo 分为调用者栈、被调用者栈 调用者栈:caller parent BP,(BP pseudo SP)local var0 …local varN,callee arg2 … callee arg0 ,(FP virtual register)return addr 被调用者栈:caller BP(caller frame pointer),Local var0 …local varN,(SP real register) 可见:调用子函数时,先在栈顶准备好参数,再执行CALL指令。CALL会将IP寄存器(PC)的值压栈,也就是执行完子函数后的下一条指令。然后进入被调用者栈,首先将caller BP压栈(栈基址,栈底) CALL类似push ip ,JMP somefuc结合,push 当前ip的地址到栈,然后把调用函数地址放到ip地址,实现调整。 RET 和CALL相反,pop 在CALL时push的ip指令到ip,实现函数返回。 总结:调用子函数前,准备参数、返回地址;CALL将返回地址入栈;进入被调用函数,汇编器插入BP寄存器相关指令;下面就是被调用函数栈的局部变量,与再次调用其他子函数的参数;被调用函数RET,从栈恢复BP、SP,接着取回返回地址调整??(RET这部分不明白) go汇编引入的伪寄存器:FP,PC,SB,SP FP,帧指针,参数和局部变量 symbol+offset(FP) PC,程序计数器,跳转和分支
SB,静态基指针全局符号 用来申明函数或者全局变量 SP,栈指针,栈顶 指向当前栈帧的局部变量的开始位置,symbol+offset(SP)方式引用函数的局部变量,offset 的合法取值是 [-framesize, 0) 手写汇编代码时,如果是 symbol+offset(SP) 形式,则表示伪寄存器 SP,表示栈底。如果是 offset(SP) 则表示硬件寄存器 SP,表示栈顶。务必注意。对于编译输出(go tool compile -S / go tool objdump)的代码来讲,目前所有的 SP 都是硬件寄存器 SP,无论是否带 symbol.

2 · stack#

go栈、用户栈 函数调用栈:todo 分为调用者栈、被调用者栈 调用者栈:caller parent BP,(BP pseudo SP)local var0 …local varN,callee arg2 … callee arg0 ,(FP virtual register)return addr 被调用者栈:caller BP(caller frame pointer),Local var0 …local varN,(SP real register) 可见:调用子函数时,先在栈顶准备好参数,再执行CALL指令。CALL会将IP寄存器(PC)的值压栈,也就是执行完子函数后的下一条指令。然后进入被调用者栈,首先将caller BP压栈(栈基址,栈底) CALL类似push ip ,JMP somefuc结合,push 当前ip的地址到栈,然后把调用函数地址放到ip地址,实现调整。 RET 和CALL相反,pop 在CALL时push的ip指令到ip,实现函数返回。 总结:调用子函数前,准备参数、返回地址;CALL将返回地址入栈;进入被调用函数,汇编器插入BP寄存器相关指令;下面就是被调用函数栈的局部变量,与再次调用其他子函数的参数;被调用函数RET,从栈恢复BP、SP,接着取回返回地址调整??(RET这部分不明白) go汇编引入的伪寄存器:FP,PC,SB,SP FP,帧指针,参数和局部变量 symbol+offset(FP) PC,程序计数器,跳转和分支
SB,静态基指针全局符号 用来申明函数或者全局变量 SP,栈指针,栈顶 指向当前栈帧的局部变量的开始位置,symbol+offset(SP)方式引用函数的局部变量,offset 的合法取值是 [-framesize, 0) 手写汇编代码时,如果是 symbol+offset(SP) 形式,则表示伪寄存器 SP。如果是 offset(SP) 则表示硬件寄存器 SP。务必注意。对于编译输出(go tool compile -S / go tool objdump)的代码来讲,目前所有的 SP 都是硬件寄存器 SP,无论是否带 symbol。

3 · think in stack#

Go 1.2 :协程的堆栈大小从 4Kb 增加到 8Kb。
Go 1.4 :协程的堆栈大小从 8Kb 减小到 2Kb。  

连续堆栈 VS 分段堆栈。

4 · 连续栈 contiguous stack#

https://docs.google.com/document/d/1wAaf1rYoM4S4gtnPh0zOlGzWtrZFQ5suE8qr2sD8uWQ/pub

给每一个 goroutine 的栈分配一段连续的空间,当空间填满时,使用 reallocation/copy 增长空间。

4.1 · Why#

非连续栈(split stack)的问题:

  1. 不是一整块连续栈,而是”栈不够了就再挂一块新 chunk”
  2. 热点路径上不断扩容/缩容
  3. 运行时的持续开销:chunk 链接/解绑 等

连续栈(contiguous stack)的优势:

  1. 是一整段连续内存
  2. 栈到合适位置,后续通常不需要折腾;不会在热点上不断扩容/缩容
  3. 运行时开销较简单

总结: 连续栈值扩一次,基本稳定;非连续栈会在阈值处来回跨,导致性能抖动。

4.2 · How#

如何复制栈:依赖逃逸分析保证,栈内指针指向的值都在栈上,可以复制整段栈后修复指针。

  1. 栈溢出检查:
  2. 复制栈:将栈看作一个指针数组,复制到新栈,根据 GC 的指针元数据修正所有真实指针
  3. reflect.call: 比较特殊 TODO
  4. GC, defer/panic/recover, stack tracing
  5. 收缩:在 GC 时按使用率缩回去

缺点:连续内存压力大,内存碎片化;严格限制进入栈的指针;

5 · 栈回溯 traceback#

栈回溯的实现在 src/runtime/traceback.go 里,核心是:

  • unwinder:按帧遍历栈
  • traceback / traceback1 / traceback2:从某个 (pc, sp, lr) 开始打印或收集调用栈
  • tracebackPCs:只收集 PC(用于 pprof、Callers 等)