14.4 Stack Copying and Pointer Adjustment
Once newstack (14.3) has decided the size of the new stack, it calls copystack to
perform the stack copy: copy the contents of the old stack to the new one, then free the old
stack. Copying the bytes takes a single memmove. The difficulty is that a stack may hold pointers
to itself, for example when one local variable stores the address of another. Copied byte for byte,
such a pointer keeps its value and still points into the old stack, which is about to be freed.
copystack must therefore find every pointer into the old stack and redirect it to the
corresponding location in the new one. The two stacks are aligned at their high-address ends, so a
pointer that falls inside the old range [old.lo, old.hi) needs only the offset δ = new.hi −
old.hi added. What remains is to know which words are pointers and where outside the stack
pointers into it may be stored. This section answers those two questions, then covers the
synchronization between stack copying and channel operations, the overall flow of copystack, and
the constraints this mechanism places on the language.
14.4.1 Where Pointers into the Stack Live
14.1 quoted the argument in the contiguous-stack design document: escape analysis
guarantees that pointers to data on the stack are passed only down the call tree, so such pointers
exist only in the stack itself [Randall, 2013]. The document also names
exceptions, a few pointers that live outside the stack yet point into it; it lists the function
objects and arguments of deferred calls, and the closure context passed in a register. In Go 1.26
the exceptions are the execution context g.sched saved by the goroutine, the linked list of
defer records, the head of the panic list, and the data address recorded in a sudog while the
goroutine is blocked on a channel. copystack handles pointers inside stack frames frame by frame
and treats these few places separately.
Wherever the pointer lives, adjusting it works the same way [Go Authors, 2026a]:
| |
sghi is used in 14.4.4.
14.4.2 Adjusting Frame by Frame
Which words in a frame are pointers is given by the stack maps the compiler generates: for every
position where execution may pause, the compiler records which words of the locals and arguments
areas hold live pointers (13.7) [Go Authors, 2026c].
This information was originally produced for precise garbage collection, and stack copying borrows
it (14.1). runtime/stack.go includes a diagram of where the regions of a stack
frame lie:
| |
adjustframe adjusts one frame according to this layout:
| |
Three things in a frame need adjusting. The first is the caller’s frame pointer saved in the frame,
which always points to another frame higher in the stack. The second is the pointers that the stack
map marks in the locals and arguments areas. The third is stack objects, local variables whose
address has been taken (13.7). Randall introduced this mechanism in Go 1.12:
the compiler registers the address-taken variables of each function in its metadata, and when
scanning a stack the collector uses it to do a small marking pass among the stack objects, scanning
only those that are actually referenced [Randall, 2018]. Stack copying does no
such marking and adjusts the pointers in every stack object; for an object no longer in use this is
merely some extra work. If the address of an object is below the SP of the current frame, the frame
had not yet been set up when the prologue check failed, so the object does not exist yet and can be
skipped. A dead frame (continpc is 0) will never resume, and only its saved frame pointer needs
adjusting.
The function that actually reads the bitmap is adjustpointers:
| |
The bitmap is consumed one byte at a time, and TrailingZeros8 jumps straight to the next set slot.
Words not marked as pointers are never touched, so an integer that merely looks like an address is
not rewritten. The function also performs a check: if a pointer slot holds a nonzero value smaller
than minLegalPointer, either the liveness analysis is wrong or the program has used unsafe to
store an integer in a pointer, and the runtime terminates the program with “invalid pointer found on
stack”. The check is controlled by GODEBUG=invalidptr and is on by default.
14.4.3 Pointers Outside Stack Frames
The four places outside the frames are adjusted by four functions:
| |
g.sched.ctxt holds the closure context, which may point to a closure on the stack; g.sched.bp is
the frame pointer of the innermost frame. A defer record may be allocated on the stack, so the
list head g._defer, the link of each record, its function value fn and the stack pointer sp
recorded at registration may all point into the stack (6.2).
panic records are on the stack themselves and have already been handled along with the frames,
which leaves only the pointer in g to the list head. sudog.elem records the address of the data
being sent or received in a channel operation, and this address is usually on the blocked
goroutine’s own stack (10.3). To detect goroutine
leaks, Go 1.26 changed elem to maybeTraceablePtr, in which vu stores the address as an integer
(the comment calls it the only trusted source) and vp serves only to let the collector judge
liveness; both must be adjusted [Saioc, 2025].
14.4.4 Synchronizing with Channel Operations
sudog.elem raises a problem beyond adjustment. A goroutine blocked on a channel has released the
channel’s lock, so another goroutine may at any moment perform the matching operation and write data
directly into its stack (the direct handoff in 10.3).
Stack shrinking can occur while a goroutine is blocked (14.5). If copying and writing
then happen at the same time, the write may land on the old stack and be lost, or it may overwrite
and be overwritten by the pointer adjustment.
In Go 1.14, Austin Clements added a field to g that records explicitly whether copying must
synchronize with channels [Clements, 2019;
Go Authors, 2026b]:
| |
The commit message explains why it was introduced. When a stack grows, the goroutine is running: it is either not blocked on a channel or holds the channel lock itself, and acquiring the lock again would deadlock it. When a stack shrinks, the goroutine is not running and the lock must be acquired. The old code told the two cases apart by whether the stack was growing or shrinking, and the timing of shrinking was about to change (14.5), so the field now states it directly.
When activeStackChans is true, copystack first calls findsghi to find the upper bound of the
highest send/receive data area on the stack, called sghi, and then calls syncadjustsudogs:
| |
It locks all channels the goroutine is waiting on, adjusts the sudogs, copies the segment from the
stack pointer up to sghi while holding the locks (oldBot in the code is the stack pointer of the
old stack), and then releases the locks. These channel locks should be acquired before the scan bit
in the goroutine’s status, and here the order is reversed; a comment elided above explains that
because the goroutines involved are necessarily suspended, no deadlock can occur here, so a
dedicated lock rank, lockRankHchanLeaf, is used. After the locks are released, copystack copies
the rest of the stack as usual. The frame-by-frame adjustment, however, happens after the release,
and slots below sghi may still be written by a concurrent send. The comment in adjustpointers
points out that a slot that has not yet received data may still hold an old pointer into the stack,
while the value being sent can never contain a pointer into the stack. That segment is therefore
adjusted with CAS: if the write wins the race, the CAS fails, and on rereading the new value does
not point into the old stack, so it is left alone. A comment in copystack notes that sghi is not
far from the stack pointer, so only a very small segment needs this care.
14.4.5 The Overall Flow of copystack
copystack ties the steps above together:
| |
Several details deserve attention. The stack cannot be moved during a system call, for the same
reason as the entersyscall comment in 14.3: the runtime does not know which uintptr
arguments actually point into the stack. Only the used part, [sched.sp, old.hi), is copied, and it
is placed at the high-address end of the new stack; the unused space at the low end need not be
copied. addScannableStack accounts for the change in stack size in the collector’s pacing
(13.3). g.sched and the defer and panic chains must be adjusted before
the frame walk, because the walk over the new stack uses them. The gentraceback in the comment is
the old stack-walking function; in Go 1.21 Clements replaced all its callback-style uses with the
unwinder iterator, which cut the time of four stack-copying benchmarks by about 10% on average
(geometric mean) [Clements, 2023]. Resetting stackguard0 clears a pending
preemption request that has been written into it, as the comment notes, while the gp.preempt flag
is unaffected (see 9.7 for how preemption requests
are issued and handled). The parkingOnChan check relates to stack shrinking and is covered in
14.5.
The following diagram summarizes the flow:
flowchart TD
A["stackalloc(newsize)<br/>delta = new.hi − old.hi"] --> B{"activeStackChans?"}
B -->|"no"| C["adjustsudogs"]
B -->|"yes"| D["findsghi computes sghi<br/>syncadjustsudogs: lock channels,<br/>adjust sudogs, copy sp to sghi"]
C --> E["memmove: copy the rest of the used part"]
D --> E
E --> F["adjustctxt, adjustdefers, adjustpanics<br/>(before the frame walk)"]
F --> G["gp.stack = new<br/>update stackguard0, sched.sp, stktopsp"]
G --> H["unwinder walks frames, adjustframe<br/>adjusts by stack map and stack objects"]
H --> I["stackfree(old)"]14.4.6 What Is Adjusted and What Is Not
The following program places an integer x on the stack, records its address in a pointer p and
in an integer u, and then recurses about 1000 levels to force the stack to grow:
| |
The output is:
address of x has changed: true
value read through p: 2
x does not escape and stays in the stack frame of main. When the stack grows, x is copied to
the new stack, and the slot holding p is marked as a pointer in the stack map and is adjusted, so
reading through p yields x on the new stack. u is only an integer and still holds an address
on the old stack. The documentation of the unsafe package states this explicitly: even if a
uintptr holds the address of an object, the collector does not update its value when the object
moves [Go Authors, 2026d]. Variables on the stack are no exception.
14.4.7 Constraints on the Language
Stack copying works because of two premises: pointers into the stack appear only where the runtime can find them, and every pointer can be identified exactly. These premises in turn constrain how the language is implemented and used.
The first is guaranteed by escape analysis. If the address of a variable would be stored in the heap
or in a global variable, or returned to the caller, the compiler allocates the variable on the heap
(15.5). Heap objects do not move with the stack, so
addresses held elsewhere stay valid. Conversely, whenever the runtime itself makes a structure
outside the stack point into it, the structure must be registered on g, as sudog is, so that
copystack can adjust it. Memory handed to the operating system cannot live on a movable stack,
which is why overlapped I/O on Windows pins its buffers before starting
(9.9).
The second falls on programs that use unsafe. Go 1.3 introduced precise stack scanning together
with stack growth by copying, and its release notes set this rule
[Go Authors, 2014]:
Starting with Go 1.3, the runtime assumes that values with pointer type contain pointers and other values do not. This assumption is fundamental to the precise behavior of both stack expansion and garbage collection. Programs that use package unsafe to store integers in pointer-typed values are illegal and will crash if the runtime detects the behavior. Programs that use package unsafe to store pointers in integer-typed values are also illegal but more difficult to diagnose during execution. Because the pointers are hidden from the runtime, a stack expansion or garbage collection may reclaim the memory they point at, creating dangling pointers.
The first kind of error, when a small integer is stored, is exactly what the invalidptr check in
14.4.2 catches; the second is the u in 14.4.6.
14.4.8 Summary
Stack copying reduces what looks like a problem of tracking arbitrary pointers to a few bounded
tasks. Pointers inside frames are identified by the compiler’s stack maps and stack objects.
Pointers outside frames are found only in g.sched, the defer chain, the panic chain and
sudog, and a goroutine blocked on a channel synchronizes with concurrent writes through locks and
CAS. The reduction rests on escape analysis and the rules for using unsafe, which together
guarantee that all pointers into the stack are knowable and identifiable.
Further Reading
- [Clements, 2019] Austin Clements. runtime: make copystack/sudog synchronization more explicit. Go commit 36a432f27b, 2019. https://go.dev/cl/172982 Shipped with 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 Shipped with Go 1.21.
- [Go Authors, 2014] The Go Authors. Go 1.3 Release Notes: Changes to the garbage collector. 2014. https://go.dev/doc/go1.3#garbage_collector
- [Go Authors, 2026a] The Go Authors. src/runtime/stack.go: copystack, adjustframe, adjustpointers, adjustsudogs, adjustdefers, adjustctxt. Go 1.26 source. https://github.com/golang/go/blob/release-branch.go1.26/src/runtime/stack.go The primary implementation this section is based on.
- [Go Authors, 2026b] The Go Authors. src/runtime/runtime2.go (the _Gcopystack state), src/runtime/mgcmark.go (stack scanning and shrinkstack). Go 1.26 source. https://github.com/golang/go/tree/release-branch.go1.26/src/runtime
- [Go Authors, 2026c] The Go Authors. cmd/compile: generation of stack maps / liveness. Go 1.26 source. https://github.com/golang/go/tree/release-branch.go1.26/src/cmd/compile/internal/liveness
- [Go Authors, 2026d] The Go Authors. src/unsafe/unsafe.go. Go 1.26 source. https://github.com/golang/go/blob/release-branch.go1.26/src/unsafe/unsafe.go
- [Randall, 2013] Keith Randall. Contiguous stacks. Go design document, 2013. https://go.dev/s/contigstacks The design of contiguous stacks replacing segmented stacks, and the motivation for pointer adjustment when copying.
- [Randall, 2018] Keith Randall. cmd/compile,runtime: implement stack objects. Go commit cbafcc55e8, 2018. https://go.dev/cl/134155 Shipped with 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 Shipped with Go 1.26.
- This book: 13.7 Safe-Point Analysis (stack maps and safe points, the shared
basis of pointer adjustment and GC scanning),
15.5 Escape Analysis (how stack movability
constrains allocation decisions),
10.3 Send, Receive, and Direct Handoff (sudog and
the channel send/receive slots, which explain why
adjustsudogsexists).