# 协程栈扩容

## 扩容

• 当前函数需要占用的栈空间
• 栈空间不足的判断条件
• 如何进行扩容

### 计算函数栈空间

• 本地变量
• 函数调用其他函数的参数
• 函数调用其他参数的返回值

``````package main

func f(a, b int) (int, int) {
sum := 0
elements := make([]int, 100)
for _, i := range elements {
sum += i
}
sum += a
sum += b
return sum, a + b
}

func main() {
f(1, 2)
}
``````
``````go tool compile -S test.go

"".f t=1 size=176 value=0 args=0x20 locals=0x320
0x0000 00000 (test.go:9)    TEXT    "".f(SB), \$800-32
0x0000 00000 (test.go:9)    MOVQ    (TLS), CX
0x0009 00009 (test.go:9)    LEAQ    -672(SP), AX
0x0011 00017 (test.go:9)    CMPQ    AX, 16(CX)

"".main t=1 size=64 value=0 args=0x0 locals=0x20
0x0000 00000 (test.go:24)   TEXT    "".main(SB), \$32-0
0x0000 00000 (test.go:24)   MOVQ    (TLS), CX
0x0009 00009 (test.go:24)   CMPQ    SP, 16(CX)
``````

### 判断栈空间不足

golang检测栈扩容的主要条件是SP是否达到了栈保护的边界，也就是我们前面途中的StackGuard。基本思想是如果SP小于StackGuard，那么需要进行栈扩容，否则无需栈扩容。

#### 函数栈极小，无需扩容

``````package main

func f(a, b int) (int, int) {
sum := 0
elements := make([]int, 10)
for _, i := range elements {
sum += i
}
sum += a
sum += b
return sum, a + b
}

func main() {
f(1, 2)
}
``````

``````"".f t=1 size=128 value=0 args=0x20 locals=0x50
0x0000 00000 (test.go:9)    TEXT    "".f(SB), \$80-32
0x0000 00000 (test.go:9)    SUBQ    \$80, SP
0x0004 00004 (test.go:9)    MOVQ    "".a+88(FP), R9
0x0009 00009 (test.go:9)    MOVQ    "".b+96(FP), R8
0x000e 00014 (test.go:9)    FUNCDATA    \$0, gclocals·a8eabfc4a4514ed6b3b0c61e9680e440(SB)
0x000e 00014 (test.go:9)    FUNCDATA    \$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x000e 00014 (test.go:12)   MOVQ    \$0, DX
0x0010 00016 (test.go:13)   LEAQ    "".autotmp_0004(SP), DI
......
``````

#### 函数栈适中，需要判断

\$\$sp-frameSize < stackLimit\$\$
=>

\$\$sp-frameSize < stackGuard-stackSmall\$\$
=>

\$\$sp-frameSize + stackSmall < stackGuard\$\$
=>

\$\$sp-frameSize + 128 < stackGuard\$\$

``````package main

func f(a, b int) (int, int) {
sum := 0
elements := make([]int, 100)
for _, i := range elements {
sum += i
}
sum += a
sum += b
return sum, a + b
}

func main() {
f(1, 2)
}
``````
``````"".f t=1 size=176 value=0 args=0x20 locals=0x320
0x0000 00000 (test.go:9)    TEXT    "".f(SB), \$800-32
0x0000 00000 (test.go:9)    MOVQ    (TLS), CX
// 就是sp-800+128与stackGuard对比
0x0009 00009 (test.go:9)    LEAQ    -672(SP), AX
0x0011 00017 (test.go:9)    CMPQ    AX, 16(CX)
0x0015 00021 (test.go:9)    JLS 158
``````

1. %fs:0xfffffffffffffff8,%rcx这里存储的到底是什么？猜测应该是线程相关变量,可能指的是正运行m的g；而0x10(%rcx)代表的是g的stackguard0，这样就能讲通这个比较的意义。关于%fs寄存器的相关说明可参考 http://www.airs.com/blog/archives/44，写的很不错；

## 栈空间扩容

``````TEXT runtime·morestack(SB),NOSPLIT,\$0-0
// Cannot grow scheduler stack (m->g0).
get_tls(CX)
MOVQ    g(CX), BX
MOVQ    g_m(BX), BX
MOVQ    m_g0(BX), SI
CMPQ    g(CX), SI
JNE     2(PC)
INT     \$3

// Cannot grow signal stack (m->gsignal).
MOVQ    m_gsignal(BX), SI
CMPQ    g(CX), SI
JNE     2(PC)
INT     \$3

// Called from f.
// Set m->morebuf to f's caller.
// ??? what does this do?
MOVQ    8(SP), AX       // f's caller's PC
MOVQ    AX, (m_morebuf+gobuf_pc)(BX)
LEAQ    16(SP), AX      // f's caller's SP
MOVQ    AX, (m_morebuf+gobuf_sp)(BX)
get_tls(CX)
// SI is current go-routine
// 将当前申请扩容stack的协程记录在m_morebuf中
// 这样后面切换到m.g0协程分配堆栈成功后知道返回到
// 哪个协程继续执行。
MOVQ    g(CX), SI
MOVQ    SI, (m_morebuf+gobuf_g)(BX)

// 将申请分配新堆栈的协程执行环境记录下来
// 这样下次返回该协程时知道从哪继续执行
// why 0(SP) is f's PC?
MOVQ    0(SP), AX // f's PC
MOVQ    AX, (g_sched+gobuf_pc)(SI)
MOVQ    SI, (g_sched+gobuf_g)(SI)
LEAQ    8(SP), AX // f's SP
MOVQ    AX, (g_sched+gobuf_sp)(SI)
// what does DX store?
MOVQ    DX, (g_sched+gobuf_ctxt)(SI)
MOVQ    BP, (g_sched+gobuf_bp)(SI)

// Call newstack on m->g0's stack.
// 切换到线程的调度协程和线程堆栈
MOVQ    m_g0(BX), BX
// switch to m.g0
MOVQ    BX, g(CX)
// what does this mean?
// 切换到线程堆栈
MOVQ    (g_sched+gobuf_sp)(BX), SP
// runtime.newstack() never return
CALL    runtime·newstack(SB)
MOVQ    \$0, 0x1003  // crash if newstack returns
RET
``````

``````func newstack() {
// gp是申请堆栈扩容的协程
gp := thisg.m.curg

......

// Allocate a bigger segment and move the stack.
oldsize := int(gp.stackAlloc)
// 新的栈大小是原来的两倍
newsize := oldsize * 2
if uintptr(newsize) > maxstacksize {
print("runtime: goroutine stack exceeds ", maxstacksize, "-byte limit\n")
throw("stack overflow")
}

casgstatus(gp, _Gwaiting, _Gcopystack)

// The concurrent GC will not scan the stack while we are doing the copy since
// the gp is in a Gcopystack status.
// 执行堆栈扩容并将原有堆栈数据拷贝至新栈
copystack(gp, uintptr(newsize))
if stackDebug >= 1 {
print("stack grow done\n")
}
casgstatus(gp, _Gcopystack, _Grunning)
gogo(&gp.sched)
}

// 申请新的栈空间并将原有栈数据拷贝至这里
func copystack(gp *g, newsize uintptr) {
if gp.syscallsp != 0 {
throw("stack growth not allowed in system call")
}
old := gp.stack
if old.lo == 0 {
throw("nil stackbase")
}
// 原有堆栈使用的空间
used := old.hi - gp.sched.sp

// allocate new stack
// newstkbar是什么？
// 0xfc是什么？
new, newstkbar := stackalloc(uint32(newsize))
if stackPoisonCopy != 0 {
fillstack(new, 0xfd)
}

......

// adjust pointers in the to-be-copied frames
// 这里主要调整g的一些调度相关参数
// 如果它们存储在老的栈上面，需要将它们拷贝到新栈上

// adjust other miscellaneous things that have pointers into stacks.

// copy the stack to the new location
// 0xfb又是什么？
if stackPoisonCopy != 0 {
fillstack(new, 0xfb)
}
// 数据拷贝，老的堆栈数据拷贝到新堆栈
memmove(unsafe.Pointer(new.hi-used), unsafe.Pointer(old.hi-used), used)

// copy old stack barriers to new stack barrier array
newstkbar = newstkbar[:len(gp.stkbar)]
copy(newstkbar, gp.stkbar)

// Swap out old stack for new one
// 切换到新堆栈上工作
gp.stack = new
gp.stackguard0 = new.lo + _StackGuard
gp.sched.sp = new.hi - used
oldsize := gp.stackAlloc
gp.stackAlloc = newsize
gp.stkbar = newstkbar

// 释放老的堆栈
if stackPoisonCopy != 0 {
fillstack(old, 0xfc)
}
stackfree(old, oldsize)
}

func fillstack(stk stack, b byte) {
for p := stk.lo; p < stk.hi; p++ {
*(*byte)(unsafe.Pointer(p)) = b
}
}
``````

## 参考资料

0 回复

• 请尽量让自己的回复能够对别人有帮助
• 支持 Markdown 格式, **粗体**、~~删除线~~、``单行代码``
• 支持 @ 本站用户；支持表情（输入 : 提示），见 Emoji cheat sheet
• 图片支持拖拽、截图粘贴等方式上传

## 扩容

• 当前函数需要占用的栈空间
• 栈空间不足的判断条件
• 如何进行扩容

### 计算函数栈空间

• 本地变量
• 函数调用其他函数的参数
• 函数调用其他参数的返回值

``````package main

func f(a, b int) (int, int) {
sum := 0
elements := make([]int, 100)
for _, i := range elements {
sum += i
}
sum += a
sum += b
return sum, a + b
}

func main() {
f(1, 2)
}
``````
``````go tool compile -S test.go

"".f t=1 size=176 value=0 args=0x20 locals=0x320
0x0000 00000 (test.go:9)    TEXT    "".f(SB), \$800-32
0x0000 00000 (test.go:9)    MOVQ    (TLS), CX
0x0009 00009 (test.go:9)    LEAQ    -672(SP), AX
0x0011 00017 (test.go:9)    CMPQ    AX, 16(CX)

"".main t=1 size=64 value=0 args=0x0 locals=0x20
0x0000 00000 (test.go:24)   TEXT    "".main(SB), \$32-0
0x0000 00000 (test.go:24)   MOVQ    (TLS), CX
0x0009 00009 (test.go:24)   CMPQ    SP, 16(CX)
``````

### 判断栈空间不足

golang检测栈扩容的主要条件是SP是否达到了栈保护的边界，也就是我们前面途中的StackGuard。基本思想是如果SP小于StackGuard，那么需要进行栈扩容，否则无需栈扩容。

#### 函数栈极小，无需扩容

``````package main

func f(a, b int) (int, int) {
sum := 0
elements := make([]int, 10)
for _, i := range elements {
sum += i
}
sum += a
sum += b
return sum, a + b
}

func main() {
f(1, 2)
}
``````

``````"".f t=1 size=128 value=0 args=0x20 locals=0x50
0x0000 00000 (test.go:9)    TEXT    "".f(SB), \$80-32
0x0000 00000 (test.go:9)    SUBQ    \$80, SP
0x0004 00004 (test.go:9)    MOVQ    "".a+88(FP), R9
0x0009 00009 (test.go:9)    MOVQ    "".b+96(FP), R8
0x000e 00014 (test.go:9)    FUNCDATA    \$0, gclocals·a8eabfc4a4514ed6b3b0c61e9680e440(SB)
0x000e 00014 (test.go:9)    FUNCDATA    \$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x000e 00014 (test.go:12)   MOVQ    \$0, DX
0x0010 00016 (test.go:13)   LEAQ    "".autotmp_0004(SP), DI
......
``````

#### 函数栈适中，需要判断

\$\$sp-frameSize < stackLimit\$\$
=>

\$\$sp-frameSize < stackGuard-stackSmall\$\$
=>

\$\$sp-frameSize + stackSmall < stackGuard\$\$
=>

\$\$sp-frameSize + 128 < stackGuard\$\$

``````package main

func f(a, b int) (int, int) {
sum := 0
elements := make([]int, 100)
for _, i := range elements {
sum += i
}
sum += a
sum += b
return sum, a + b
}

func main() {
f(1, 2)
}
``````
``````"".f t=1 size=176 value=0 args=0x20 locals=0x320
0x0000 00000 (test.go:9)    TEXT    "".f(SB), \$800-32
0x0000 00000 (test.go:9)    MOVQ    (TLS), CX
// 就是sp-800+128与stackGuard对比
0x0009 00009 (test.go:9)    LEAQ    -672(SP), AX
0x0011 00017 (test.go:9)    CMPQ    AX, 16(CX)
0x0015 00021 (test.go:9)    JLS 158
``````

1. %fs:0xfffffffffffffff8,%rcx这里存储的到底是什么？猜测应该是线程相关变量,可能指的是正运行m的g；而0x10(%rcx)代表的是g的stackguard0，这样就能讲通这个比较的意义。关于%fs寄存器的相关说明可参考 http://www.airs.com/blog/archives/44，写的很不错；

## 栈空间扩容

``````TEXT runtime·morestack(SB),NOSPLIT,\$0-0
// Cannot grow scheduler stack (m->g0).
get_tls(CX)
MOVQ    g(CX), BX
MOVQ    g_m(BX), BX
MOVQ    m_g0(BX), SI
CMPQ    g(CX), SI
JNE     2(PC)
INT     \$3

// Cannot grow signal stack (m->gsignal).
MOVQ    m_gsignal(BX), SI
CMPQ    g(CX), SI
JNE     2(PC)
INT     \$3

// Called from f.
// Set m->morebuf to f's caller.
// ??? what does this do?
MOVQ    8(SP), AX       // f's caller's PC
MOVQ    AX, (m_morebuf+gobuf_pc)(BX)
LEAQ    16(SP), AX      // f's caller's SP
MOVQ    AX, (m_morebuf+gobuf_sp)(BX)
get_tls(CX)
// SI is current go-routine
// 将当前申请扩容stack的协程记录在m_morebuf中
// 这样后面切换到m.g0协程分配堆栈成功后知道返回到
// 哪个协程继续执行。
MOVQ    g(CX), SI
MOVQ    SI, (m_morebuf+gobuf_g)(BX)

// 将申请分配新堆栈的协程执行环境记录下来
// 这样下次返回该协程时知道从哪继续执行
// why 0(SP) is f's PC?
MOVQ    0(SP), AX // f's PC
MOVQ    AX, (g_sched+gobuf_pc)(SI)
MOVQ    SI, (g_sched+gobuf_g)(SI)
LEAQ    8(SP), AX // f's SP
MOVQ    AX, (g_sched+gobuf_sp)(SI)
// what does DX store?
MOVQ    DX, (g_sched+gobuf_ctxt)(SI)
MOVQ    BP, (g_sched+gobuf_bp)(SI)

// Call newstack on m->g0's stack.
// 切换到线程的调度协程和线程堆栈
MOVQ    m_g0(BX), BX
// switch to m.g0
MOVQ    BX, g(CX)
// what does this mean?
// 切换到线程堆栈
MOVQ    (g_sched+gobuf_sp)(BX), SP
// runtime.newstack() never return
CALL    runtime·newstack(SB)
MOVQ    \$0, 0x1003  // crash if newstack returns
RET
``````

``````func newstack() {
// gp是申请堆栈扩容的协程
gp := thisg.m.curg

......

// Allocate a bigger segment and move the stack.
oldsize := int(gp.stackAlloc)
// 新的栈大小是原来的两倍
newsize := oldsize * 2
if uintptr(newsize) > maxstacksize {
print("runtime: goroutine stack exceeds ", maxstacksize, "-byte limit\n")
throw("stack overflow")
}

casgstatus(gp, _Gwaiting, _Gcopystack)

// The concurrent GC will not scan the stack while we are doing the copy since
// the gp is in a Gcopystack status.
// 执行堆栈扩容并将原有堆栈数据拷贝至新栈
copystack(gp, uintptr(newsize))
if stackDebug >= 1 {
print("stack grow done\n")
}
casgstatus(gp, _Gcopystack, _Grunning)
gogo(&gp.sched)
}

// 申请新的栈空间并将原有栈数据拷贝至这里
func copystack(gp *g, newsize uintptr) {
if gp.syscallsp != 0 {
throw("stack growth not allowed in system call")
}
old := gp.stack
if old.lo == 0 {
throw("nil stackbase")
}
// 原有堆栈使用的空间
used := old.hi - gp.sched.sp

// allocate new stack
// newstkbar是什么？
// 0xfc是什么？
new, newstkbar := stackalloc(uint32(newsize))
if stackPoisonCopy != 0 {
fillstack(new, 0xfd)
}

......

// adjust pointers in the to-be-copied frames
// 这里主要调整g的一些调度相关参数
// 如果它们存储在老的栈上面，需要将它们拷贝到新栈上

// adjust other miscellaneous things that have pointers into stacks.

// copy the stack to the new location
// 0xfb又是什么？
if stackPoisonCopy != 0 {
fillstack(new, 0xfb)
}
// 数据拷贝，老的堆栈数据拷贝到新堆栈
memmove(unsafe.Pointer(new.hi-used), unsafe.Pointer(old.hi-used), used)

// copy old stack barriers to new stack barrier array
newstkbar = newstkbar[:len(gp.stkbar)]
copy(newstkbar, gp.stkbar)

// Swap out old stack for new one
// 切换到新堆栈上工作
gp.stack = new
gp.stackguard0 = new.lo + _StackGuard
gp.sched.sp = new.hi - used
oldsize := gp.stackAlloc
gp.stackAlloc = newsize
gp.stkbar = newstkbar

// 释放老的堆栈
if stackPoisonCopy != 0 {
fillstack(old, 0xfc)
}
stackfree(old, oldsize)
}

func fillstack(stk stack, b byte) {
for p := stk.lo; p < stk.hi; p++ {
*(*byte)(unsafe.Pointer(p)) = b
}
}
``````