typeadjustinfostruct{oldstackdeltauintptr// ptr distance from old to new stack (newbase - oldbase)// sghi is the highest sudog.elem on the stack.sghiuintptr}// adjustpointer checks whether *vpp is in the old stack described by adjinfo.// If so, it rewrites *vpp to point into the new stack.funcadjustpointer(adjinfo*adjustinfo,vppunsafe.Pointer){pp:=(*uintptr)(vpp)p:=*pp// ……ifadjinfo.old.lo<=p&&p<adjinfo.old.hi{*pp=p+adjinfo.delta}}
// Note: the argument/return area is adjusted by the callee.funcadjustframe(frame*stkframe,adjinfo*adjustinfo){// Adjust saved frame pointer if there is one.if(goarch.ArchFamily==goarch.AMD64||goarch.ArchFamily==goarch.ARM64)&&frame.argp-frame.varp==2*goarch.PtrSize{// ……adjustpointer(adjinfo,unsafe.Pointer(frame.varp))}// ……ifframe.continpc==0{// Frame is dead. The program might still see the frame pointer// saved in the frame, adjusted above, but we don't need to// adjust the rest of the frame.return}f:=frame.fnlocals,args,objs:=frame.getStackMap(true)// Adjust local variables if stack frame has been allocated.iflocals.n>0{size:=uintptr(locals.n)*goarch.PtrSizeadjustpointers(unsafe.Pointer(frame.varp-size),&locals,adjinfo,f)}// Adjust arguments.ifargs.n>0{adjustpointers(unsafe.Pointer(frame.argp),&args,adjinfo,funcInfo{})}// Adjust pointers in all stack objects (whether they are live or not).// See comments in mgcmark.go:scanframeworker.ifframe.varp!=0{fori:=rangeobjs{obj:=&objs[i]off:=obj.offbase:=frame.varp// locals base pointerifoff>=0{base=frame.argp// arguments and return values base pointer}p:=base+uintptr(off)ifp<frame.sp{// Object hasn't been allocated in the frame yet.// (Happens when the stack bounds check fails and// we call into morestack.)continue}// ……按对象的指针位图逐字调用 adjustpointer}}}
一帧中有三处需要调整。第一处是帧中保存的调用方帧指针,它总是指向栈内更高处的另一帧。第二处是局部变量区与参数区中由栈映射标出的指针。第三处是栈对象,即被取了地址的局部变量(13.7)。Randall 在 Go 1.12 中引入了这一机制,编译器把每个函数中被取地址的变量登记在函数的元数据中,回收器扫描栈时据此在栈对象之间做一次小规模的标记,只有确实被引用的栈对象才被扫描 [Randall, 2018]。栈拷贝不做这样的标记,所有栈对象中的指针一律调整,对于已经不再使用的对象,这只是多做了一些工作。若对象的地址低于当前帧的 SP,说明序言中的检查失败时这一帧尚未建立,对象还不存在,跳过即可。已经不会再恢复执行的死帧(continpc 为 0)只需调整保存的帧指针。
// bv describes the memory starting at address scanp.// Adjust any pointers contained therein.funcadjustpointers(scanpunsafe.Pointer,bv*bitvector,adjinfo*adjustinfo,ffuncInfo){minp:=adjinfo.old.lomaxp:=adjinfo.old.hidelta:=adjinfo.deltanum:=uintptr(bv.n)// If this frame might contain channel receive slots, use CAS// to adjust pointers. If the slot hasn't been received into// yet, it may contain stack pointers and a concurrent send// could race with adjusting those pointers. (The sent value// itself can never contain stack pointers.)useCAS:=uintptr(scanp)<adjinfo.sghifori:=uintptr(0);i<num;i+=8{// ……b:=*(addb(bv.bytedata,i/8))forb!=0{j:=uintptr(sys.TrailingZeros8(b))b&=b-1pp:=(*uintptr)(add(scanp,(i+j)*goarch.PtrSize))retry:p:=*ppiff.valid()&&0<p&&p<minLegalPointer&&debug.invalidptr!=0{// Looks like a junk value in a pointer slot.// Live analysis wrong?getg().m.traceback=2throw("invalid pointer found on stack")}ifminp<=p&&p<maxp{ifuseCAS{ppu:=(*unsafe.Pointer)(unsafe.Pointer(pp))if!atomic.Casp1(ppu,unsafe.Pointer(p),unsafe.Pointer(p+delta)){gotoretry}}else{*pp=p+delta}}}}}
位图每次取出一个字节,TrailingZeros8 直接跳到下一个置位的槽,未标为指针的字一概不碰,恰好像地址的整数因此不会被改写。函数顺带做一项检查:指针槽中若是一个小于 minLegalPointer 的非零值,说明活跃性分析出了错,或者程序用 unsafe 把整数存进了指针,运行时以「invalid pointer found on stack」终止程序。这项检查由 GODEBUG=invalidptr 控制,默认开启。
funcadjustctxt(gp*g,adjinfo*adjustinfo){adjustpointer(adjinfo,unsafe.Pointer(&gp.sched.ctxt))if!framepointer_enabled{return}// ……adjustpointer(adjinfo,unsafe.Pointer(&gp.sched.bp))// ……arm64 上还要处理保存在 SP 之下一个字处的帧指针}funcadjustdefers(gp*g,adjinfo*adjustinfo){// Adjust pointers in the Defer structs.// We need to do this first because we need to adjust the// defer.link fields so we always work on the new stack.adjustpointer(adjinfo,unsafe.Pointer(&gp._defer))ford:=gp._defer;d!=nil;d=d.link{adjustpointer(adjinfo,unsafe.Pointer(&d.fn))adjustpointer(adjinfo,unsafe.Pointer(&d.sp))adjustpointer(adjinfo,unsafe.Pointer(&d.link))}}funcadjustpanics(gp*g,adjinfo*adjustinfo){// Panics are on stack and already adjusted.// Update pointer to head of list in G.adjustpointer(adjinfo,unsafe.Pointer(&gp._panic))}funcadjustsudogs(gp*g,adjinfo*adjustinfo){// the data elements pointed to by a SudoG structure// might be in the stack.fors:=gp.waiting;s!=nil;s=s.waitlink{adjustpointer(adjinfo,unsafe.Pointer(&s.elem.vu))adjustpointer(adjinfo,unsafe.Pointer(&s.elem.vp))}}
// activeStackChans indicates that there are unlocked channels// pointing into this goroutine's stack. If true, stack// copying needs to acquire channel locks to protect these// areas of the stack.activeStackChansbool
// syncadjustsudogs adjusts gp's sudogs and copies the part of gp's// stack they refer to while synchronizing with concurrent channel// operations. It returns the number of bytes of stack copied.funcsyncadjustsudogs(gp*g,useduintptr,adjinfo*adjustinfo)uintptr{ifgp.waiting==nil{return0}// Lock channels to prevent concurrent send/receive.varlastc*hchanforsg:=gp.waiting;sg!=nil;sg=sg.waitlink{ifsg.c.get()!=lastc{// ……lockWithRank(&sg.c.get().lock,lockRankHchanLeaf)}lastc=sg.c.get()}// Adjust sudogs.adjustsudogs(gp,adjinfo)// Copy the part of the stack the sudogs point in to// while holding the lock to prevent races on// send/receive slots.varsgsizeuintptrifadjinfo.sghi!=0{oldBot:=adjinfo.old.hi-usednewBot:=oldBot+adjinfo.deltasgsize=adjinfo.sghi-oldBotmemmove(unsafe.Pointer(newBot),unsafe.Pointer(oldBot),sgsize)}// Unlock channels.// ……returnsgsize}
// Copies gp's stack to a new stack of a different size.// Caller must have changed gp status to Gcopystack.funccopystack(gp*g,newsizeuintptr){ifgp.syscallsp!=0{throw("stack growth not allowed in system call")}old:=gp.stack// ……used:=old.hi-gp.sched.sp// Add just the difference to gcController.addScannableStack.// ……gcController.addScannableStack(getg().m.p.ptr(),int64(newsize)-int64(old.hi-old.lo))// allocate new stacknew:=stackalloc(uint32(newsize))// ……// Compute adjustment.varadjinfoadjustinfoadjinfo.old=oldadjinfo.delta=new.hi-old.hi// Adjust sudogs, synchronizing with channel ops if necessary.ncopy:=usedif!gp.activeStackChans{ifnewsize<old.hi-old.lo&&gp.parkingOnChan.Load(){// ……throw("racy sudog adjustment due to parking on channel")}adjustsudogs(gp,&adjinfo)}else{// ……adjinfo.sghi=findsghi(gp,old)// Synchronize with channel ops and copy the part of// the stack they may interact with.ncopy-=syncadjustsudogs(gp,used,&adjinfo)}// Copy the stack (or the rest of it) to the new locationmemmove(unsafe.Pointer(new.hi-ncopy),unsafe.Pointer(old.hi-ncopy),ncopy)// Adjust remaining structures that have pointers into stacks.// We have to do most of these before we traceback the new// stack because gentraceback uses them.adjustctxt(gp,&adjinfo)adjustdefers(gp,&adjinfo)adjustpanics(gp,&adjinfo)ifadjinfo.sghi!=0{adjinfo.sghi+=adjinfo.delta}// Swap out old stack for new onegp.stack=newgp.stackguard0=new.lo+stackGuard// NOTE: might clobber a preempt requestgp.sched.sp=new.hi-usedgp.stktopsp+=adjinfo.delta// Adjust pointers in the new stack.varuunwinderforu.init(gp,0);u.valid();u.next(){adjustframe(&u.frame,&adjinfo)}// ……stackfree(old)}
flowchart TD
A["stackalloc(newsize)<br/>delta = new.hi − old.hi"] --> B{"activeStackChans?"}
B -->|"否"| C["adjustsudogs"]
B -->|"是"| D["findsghi 求出 sghi<br/>syncadjustsudogs:锁住 channel、<br/>调整 sudog、复制 sp 至 sghi"]
C --> E["memmove:复制其余已用部分"]
D --> E
E --> F["adjustctxt、adjustdefers、adjustpanics<br/>(须先于逐帧遍历)"]
F --> G["gp.stack = new<br/>更新 stackguard0、sched.sp、stktopsp"]
G --> H["unwinder 逐帧 adjustframe<br/>按栈映射与栈对象调整"]
H --> I["stackfree(old)"]
14.4.6 被调整的与不被调整的
下面的程序在栈上放一个整数 x,分别以指针 p 和整数 u 记下它的地址,然后递归约 1000 层,迫使栈增长:
[Clements, 2019] Austin Clements. runtime: make copystack/sudog
synchronization more explicit. Go commit 36a432f27b, 2019. https://go.dev/cl/172982
随 Go 1.14 发布。
[Clements, 2023] Austin Clements. runtime: replace all callback uses of
gentraceback with unwinder. Go commit 2d99109cfc, 2023. https://go.dev/cl/468297
随 Go 1.21 发布。
[Randall, 2013] Keith Randall. Contiguous stacks. Go design document,
2013. https://go.dev/s/contigstacks
连续栈取代分段栈的设计与拷贝时的指针调整动机。
[Randall, 2018] Keith Randall. cmd/compile,runtime: implement stack
objects. Go commit cbafcc55e8, 2018. https://go.dev/cl/134155
随 Go 1.12 发布。
[Saioc, 2025] Vlad Saioc. runtime,net/http/pprof: goroutine leak detection by
using the garbage collector. Go commit 8c68a1c1ab, 2025. https://go.dev/cl/688335
随 Go 1.26 发布。