From 9b0ad889c6fe2428b3d5a0a70faee6f664081312 Mon Sep 17 00:00:00 2001 From: chai2010 Date: Fri, 8 Jun 2018 09:18:43 +0800 Subject: [PATCH] =?UTF-8?q?ch3-06:=20=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ch3-asm/ch3-06-func-again.md | 61 +++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/ch3-asm/ch3-06-func-again.md b/ch3-asm/ch3-06-func-again.md index 3496a9a..7df0462 100644 --- a/ch3-asm/ch3-06-func-again.md +++ b/ch3-asm/ch3-06-func-again.md @@ -78,7 +78,66 @@ L_END: ## 栈的扩容和收缩 -TODO +Go语言的编译器在生成函数的机器代码时,会在开头插入以小段代码。插入的代码可以做很多事情,包括触发runtime.Gosched进行协作式调度,还包括栈的动态增长等。其实栈等扩容工作主要在runtime包的runtime·morestack_noctxt函数实现,这是一个底层函数,只有汇编层面才可以调用。 + +在新版本的sum汇编函数中,我们在开头和末尾都引入了部分代码: + +``` +// func sum(n int) int +TEXT ·sum(SB), $16-16 + NO_LOCAL_POINTERS + +L_START: + MOVQ TLS, CX + MOVQ 0(CX)(TLS*1), AX + CMPQ SP, 16(AX) + JLS L_MORE_STK + + // 原来的代码 + +L_MORE_STK: + CALL runtime·morestack_noctxt(SB) + JMP L_START +``` + +其中NO_LOCAL_POINTERS表示没有局部指针。因为新引入的代码可能导致调用runtime·morestack_noctxt函数,而栈的扩容必然要涉及函数参数和局部编指针的调整,如果缺少局部指针信息将导致扩容工作无法进行。不仅仅是栈的扩容需要函数的参数和局部指针标记表格,在GC进行垃圾回收时也将需要。函数的参数和返回值的指针状态可以通过在Go语言中的函数声明中获取,函数的局部变量则需要手工指定。因为手工指定指针表格是一个非常繁琐的工作,因此一般要避免在手写汇编中出现局部指针。 + +喜欢深究的读者可能会有一个问题:如果进行垃圾回收或栈调整时,寄存器中的指针时如何维护的?前文说过,Go语言的函数调用时通过栈进行传递参数的,并没有使用寄存器传递参数。同时函数调用之后所有的寄存器视为失效。因此在调整和维护指针时,只需要扫描内存中的指针数据,寄存器中的数据在垃圾回收器函数返回后都需要重新加载,因此寄存器是不需要扫描的。 + +在Go语言的Goroutine实现中,每个TlS线程局部变量会保存当前Goroutine的信息结构体的指针。通过`MOVQ TLS, CX`和`MOVQ 0(CX)(TLS*1), AX`两条指令将表示当前Goroutine信息的g结构体加载到CX寄存器。g结构体在`$GOROOT/src/runtime/runtime2.go`文件定义,开头的结构成员如下: + +```go +type g struct { + // Stack parameters. + // stack describes the actual stack memory: [stack.lo, stack.hi). + // stackguard0 is the stack pointer compared in the Go stack growth prologue. + // It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption. + // stackguard1 is the stack pointer compared in the C stack growth prologue. + // It is stack.lo+StackGuard on g0 and gsignal stacks. + // It is ~0 on other goroutine stacks, to trigger a call to morestackc (and crash). + stack stack // offset known to runtime/cgo + stackguard0 uintptr // offset known to liblink + stackguard1 uintptr // offset known to liblink + + ... +} +``` + +第一个成员是stack类型,表示当前栈的开始和结束地址。stack的定义如下: + +```go +// Stack describes a Go execution stack. +// The bounds of the stack are exactly [lo, hi), +// with no implicit data structures on either side. +type stack struct { + lo uintptr + hi uintptr +} +``` + +在g结构体中的stackguard0成员是出现爆栈前的警戒线。stackguard0的偏移量是16个字节,因此上述代码中的`CMPQ SP, 16(AX)`表示将当前的真实SP和爆栈警戒线比较,如果超出警戒线则表示需要进行栈扩容,也就是跳转到L_MORE_STK。在L_MORE_STK标号处,线调用runtime·morestack_noctxt进行栈扩容,然后又跳回到函数到开始位置,此时此刻函数到栈已经调整了。然后再进行一次栈大小到检测,如果依然不足则继续扩容,直到栈足够大为止。 + +以上是栈的扩容,但是栈到收缩是在何时处理到呢?我们知道Go运行时会定期进行垃圾回收操作,这其中栈的回收工作。如果栈使用到比例小于一定到阈值,则分配一个较小到栈空间,然后将栈上面到数据移动到新的栈中,栈移动的过程和栈扩容的过程类似。 ## PCDATA和PCDATA