Tech News
← Back to articles

How to store Go pointers from assembly

read original related products more articles

2025-06-23 How to store Go pointers from assembly

The standard Go toolchain comes with an assembler out of the box. Said assembler is highly idiosyncratic, using syntax inherited from Plan 9 and choosing its own names for platform-specific instructions and registers. But it’s great to have it readily available.

More mundanely, Go comes with a garbage collector. This post explains how to make these two components play nice, if we want to manipulate Go pointers from our assembly.

Preamble: Go’s GC write barriers #

Go’s garbage collector strives to minimize long pauses, which calls for concurrent garbage collection: the garbage is picked up while your code is running. Even if you’re not a garbage collection expert you would have recognized this as a tricky problem. As the Go GC is marking reachable objects, new objects might become reachable due to code running alongside the GC.

A common technique deployed to address this problem consists of instrumenting all pointer stores to inform the GC that the destination is now being pointed to. This instrumentation will augment all assignments with a bit of code informing the GC of the new reference. Or more concretely code like x = y will become more like ( y ) add_to_gc_queue x = y

Where add_to_gc_queue(y) makes it so that y will be picked up by the GC even if x had already been examined. The widget above is often called a “write barrier” in the context of garbage collection. As you can imagine this instrumentation has a cost, a cost that is particularly dear when it comes to the very common task of storing pointers in stack variables. So Go chooses to not instrument stores where the receiver of the store is on the stack. Instead until Go 1.8, the GC stopped the world before collection, taking care to re-scan stacks for goroutines which had run after the time they had been first examined. This ensured that no new stack references would … go undetected.

This final stack rescanning procedure could often take uncomfortably long, and therefore Go switched to a broader write barrier, which roughly consists of adding both the old and the new reference to the GC queue:

(* x ) add_to_gc_queue ( y ) add_to_gc_queue * x = y

Above we have x to be a pointer to a pointer to highlight that the receiver of the pointer itself lives on the heap, rather than on the stack.

... continue reading