一文教你搞懂 Go 中栈操作 (6)

add3 函数是直接分配了一个 5000 bytes 的数组在栈上,所以开头还是一样的,将从 TLS 变量中加载一个值至 CX 寄存器,然后将 stackguard0 赋值给 SI 寄存器;

接下来会执行指令 CMPQ SI, $-1314,这里实际上比较 stackguard0 和 StackPreempt 的大小,至于为啥是 -1314 其实是直接在插入汇编代码的时候会调用 StackPreempt 变量,这个变量是在代码里面写死的:

代码位置:cmd/internal/objabi/stack.go

const ( StackPreempt = -1314 // 0xfff...fade )

如果没有被抢占,那么直接往下执行LEAQ 928(SP), AX,这句指令等于 AX = SP +_StackGuard,在 Linux 中 _StackGuard 等于 928;

接下来执行 SUBQ SI, AX,这一句指令等于 AX -= stackguard0 ;

最后执行 CMPQ AX, $5808,这个 5808 实际上是 framesize + _StackGuard - _StackSmall,如果 AX 小于 5808 那么跳转到 147 行执行 runtime.morestack_noctxt 函数。

到这里栈溢出检测就讲解完毕了,我看了其他的文章,应该都没有我讲解的全面,特别是栈帧大小大于 _StackBig 时的溢出检测。

栈的扩张

runtime.morestack_noctxt 是用汇编实现的,它会调用到 runtime·morestack,下面我们看看它的实现:

代码位置:src/runtime/asm_amd64.s

TEXT runtime·morestack(SB),NOSPLIT,$0-0 // Cannot grow scheduler stack (m->g0). // 无法增长调度器的栈(m->g0) get_tls(CX) MOVQ g(CX), BX MOVQ g_m(BX), BX MOVQ m_g0(BX), SI CMPQ g(CX), SI JNE 3(PC) CALL runtime·badmorestackg0(SB) CALL runtime·abort(SB) // 省略signal stack、morebuf和sched的处理 ... // Call newstack on m->g0's stack. // 在 m->g0 栈上调用 newstack. MOVQ m_g0(BX), BX MOVQ BX, g(CX) MOVQ (g_sched+gobuf_sp)(BX), SP CALL runtime·newstack(SB) CALL runtime·abort(SB) // 如果 newstack 返回则崩溃 crash if newstack returns RET

runtime·morestack 做完校验和赋值操作后会切换到 G0 调用 runtime·newstack来完成扩容的操作。

runtime·newstack

func newstack() { thisg := getg() gp := thisg.m.curg // 初始化寄存器相关变量 morebuf := thisg.m.morebuf thisg.m.morebuf.pc = 0 thisg.m.morebuf.lr = 0 thisg.m.morebuf.sp = 0 thisg.m.morebuf.g = 0 ... // 校验是否被抢占 preempt := atomic.Loaduintptr(&gp.stackguard0) == stackPreempt // 如果被抢占 if preempt { // 校验是否可以安全的被抢占 // 如果 M 上有锁 // 如果正在进行内存分配 // 如果明确禁止抢占 // 如果 P 的状态不是 running // 那么就不执行抢占了 if !canPreemptM(thisg.m) { // 到这里表示不能被抢占? // Let the goroutine keep running for now. // gp->preempt is set, so it will be preempted next time. gp.stackguard0 = gp.stack.lo + _StackGuard // 触发调度器的调度 gogo(&gp.sched) // never return } } if gp.stack.lo == 0 { throw("missing stack in newstack") } // 寄存器 sp sp := gp.sched.sp if sys.ArchFamily == sys.AMD64 || sys.ArchFamily == sys.I386 || sys.ArchFamily == sys.WASM { // The call to morestack cost a word. sp -= sys.PtrSize } ... if preempt { //需要收缩栈 if gp.preemptShrink { gp.preemptShrink = false shrinkstack(gp) } // 被 runtime.suspendG 函数挂起 if gp.preemptStop { // 被动让出当前处理器的控制权 preemptPark(gp) // never returns } //主动让出当前处理器的控制权 gopreempt_m(gp) // never return } // 计算新的栈空间是原来的两倍 oldsize := gp.stack.hi - gp.stack.lo newsize := oldsize * 2 ... //将 Goroutine 切换至 _Gcopystack 状态 casgstatus(gp, _Grunning, _Gcopystack) //开始栈拷贝 copystack(gp, newsize) casgstatus(gp, _Gcopystack, _Grunning) gogo(&gp.sched) }

newstack 函数的前半部分承担了对 Goroutine 进行抢占的任务,对于任务抢占还不清楚的可以看我这篇:《从源码剖析Go语言基于信号抢占式调度 https://www.luozhiyun.com/archives/485 》。

在开始执行栈拷贝之前会先计算新栈的大小是原来的两倍,然后将 Goroutine 状态切换至 _Gcopystack 状态。

栈拷贝 func copystack(gp *g, newsize uintptr) { old := gp.stack // 当前已使用的栈空间大小 used := old.hi - gp.sched.sp //分配新的栈空间 new := stackalloc(uint32(newsize)) ... // 计算调整的幅度 var adjinfo adjustinfo adjinfo.old = old // 新栈和旧栈的幅度来控制指针的移动 adjinfo.delta = new.hi - old.hi // 调整 sudogs, 必要时与 channel 操作同步 ncopy := used if !gp.activeStackChans { ... adjustsudogs(gp, &adjinfo) } else { // 到这里代表有被阻塞的 G 在当前 G 的channel 中,所以要防止并发操作,需要获取 channel 的锁 // 在所有 sudog 中找到地址最大的指针 adjinfo.sghi = findsghi(gp, old) // 对所有 sudog 关联的 channel 上锁,然后调整指针,并且复制 sudog 指向的部分旧栈的数据到新的栈上 ncopy -= syncadjustsudogs(gp, used, &adjinfo) } // 将源栈中的整片内存拷贝到新的栈中 memmove(unsafe.Pointer(new.hi-ncopy), unsafe.Pointer(old.hi-ncopy), ncopy) // 继续调整栈中 txt、defer、panic 位置的指针 adjustctxt(gp, &adjinfo) adjustdefers(gp, &adjinfo) adjustpanics(gp, &adjinfo) if adjinfo.sghi != 0 { adjinfo.sghi += adjinfo.delta } // 将 G 上的栈引用切换成新栈 gp.stack = new gp.stackguard0 = new.lo + _StackGuard // NOTE: might clobber a preempt request gp.sched.sp = new.hi - used gp.stktopsp += adjinfo.delta // 在新栈重调整指针 gentraceback(^uintptr(0), ^uintptr(0), 0, gp, 0, nil, 0x7fffffff, adjustframe, noescape(unsafe.Pointer(&adjinfo)), 0) if stackPoisonCopy != 0 { fillstack(old, 0xfc) } //释放原始栈的内存空间 stackfree(old) }

copystack 首先会计算一下使用栈空间大小,那么在进行栈复制的时候只需要复制已使用的空间就好了;

然后调用 stackalloc 函数从堆上分配一片内存块;

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpsjdd.html