writeonce/docs/plan/exploration/assembly/01-go-runtime-asm.md

7.2 KiB

01 — Go's runtime assembly, catalogued

The Go runtime ships ~72 TEXT functions in asm_amd64.s alone, ~43 in sys_linux_amd64.s, and per-architecture variants of both for 386, arm, arm64, loong64, mips(64)x, ppc64x, riscv64, s390x, wasm. This doc inventories them by purpose so a reader can map each Go asm concern to the writeonce equivalent (spoiler: usually "Rust stdlib does it"). Follow-on reading: 02-writeonce-stance.md.

All paths are inside reference/go/src/runtime/.

Scheduler & stack switching — asm_<arch>.s

One file per arch, everything that has to break Go's calling convention. The x86_64 version lives at asm_amd64.s.

Go symbol What
runtime·gogo(SB) Jump to a goroutine's saved program counter on its stack. The machine-level act of resuming a suspended goroutine.
runtime·mcall(SB) Call a function on the g0 stack (the OS-thread's real stack). Used by anything that may block indefinitely — the calling goroutine gets parked.
runtime·systemstack(SB) Transient jump to g0 for a single function (GC operations, scheduler code) and back.
runtime·morestack(SB) Stack-growth trampoline. Preamble-injected by the compiler when a function's stack usage exceeds the current segment; calls back into Go to allocate more.
runtime·asmcgocall(SB) Enter C code. Switches from Go stack to the OS thread's C stack, marshals arguments, handles re-entry if C calls back into Go.
runtime·cgocallback(SB) Re-enter Go from C. The inverse of the above.
runtime·memmove(SB) Optimised memcpy; falls into SSE/AVX paths for large regions. Competes with (and often beats) the C library's version on specific inputs.
runtime·memequal(SB) Fixed-length memory compare; used by map key comparisons and interface equality.
runtime·memclrNoHeapPointers(SB) Fast zero-fill for regions the GC doesn't need to scan. Autovectorised; avoids Go-level write-barrier overhead.
runtime·jmpdefer(SB) defer unwinding. Manually adjusts the frame pointer to jump into a deferred function as if it had been called at a different site.
runtime·asyncPreempt(SB) Preemption entry point (Go 1.14+). A signal handler rewrites the target goroutine's PC to point here; on resume it parks and runs the scheduler.

Why it's asm: every function above manipulates state (stack pointer, program counter, register contents) that Go's language semantics don't let you express.

Atomics & barriers — internal/runtime/atomic/atomic_<arch>.s

Lives at internal/runtime/atomic/atomic_amd64.s (and arch variants). Wrappers around arch-specific instructions:

Go symbol x86 instruction Purpose
·Load / ·Loadp / ·Load64 MOV Acquire load. On x86, plain MOV is already acquire-ordered; on ARM the same operation needs LDR + DMB ISH.
·Store / ·Store64 / ·StoreRel XCHG or MOV + MFENCE Release store.
·Xchg / ·Xchg64 XCHG Atomic swap.
·Xadd / ·Xadd64 LOCK XADD Atomic fetch-and-add.
·Cas / ·Cas64 / ·Casp LOCK CMPXCHG Compare-and-swap.
·And / ·Or / ·And8 / ·Or8 LOCK AND / LOCK OR Atomic bit manipulation.

Why it's asm: the instruction per operation differs per architecture. Go abstracts them behind a consistent sync/atomic surface.

Syscall trampolines — sys_<os>_<arch>.s

On Linux-x86_64 that's sys_linux_amd64.s — 43 TEXT functions. Each is a short wrapper: move args into the kernel's register layout, execute SYSCALL, convert rax into a Go return value + error.

Go symbol Linux syscall
runtime·write / runtime·write1 write(2)
runtime·read / runtime·pread read(2) / pread64(2)
runtime·closefd close(2)
runtime·open openat(2)
runtime·futex futex(2)
runtime·clone clone(2) — M (OS thread) creation
runtime·rt_sigaction / runtime·rt_sigprocmask Signal plumbing
runtime·sigreturn Return from signal handler
runtime·sched_yield Voluntary preemption
runtime·mmap / runtime·munmap / runtime·madvise Memory operations
runtime·epollcreate1 / runtime·epollctl / runtime·epollwait The epoll trio
runtime·exit / runtime·exitThread Process / thread termination

Why it's asm (in Go): libc's syscall wrappers check for cancellation, TLS, errno state — overhead Go's scheduler can't afford between entersyscall and exitsyscall. Go needs precise control over when the goroutine is "in a syscall" so the M can be detached from the P.

Note: writeonce does use libc's syscall wrappers — we don't have a scheduler to detach, so the overhead is irrelevant. See the stance doc for the full rationale.

Signal handling — sigtramp in sys_<os>_<arch>.s

runtime·sigtramp (same file as the syscalls) is the entry point the kernel jumps to when a signal fires. It saves the pre-signal register state, switches to the g0 stack, calls into runtime.sigtrampgo, then restores on return. Needs asm because the ABI between kernel and user signal handler is rigid and arch-specific.

Writeonce avoids this entirely by using signalfd — signals become fd reads on the epoll loop, no handler ever runs. See ../linux/04-signalfd.md.

Cgo bridge — cgo_<os>_<arch>.s

Files like cgo/asm_amd64.s. Machine-code marshalling between Go's register convention and C's SysV AMD64 ABI. Needed because Go's calling convention uses stack slots differently from C's register passing.

Writeonce doesn't cross language boundaries — Rust is the only language in the binary; libc is already in Rust's register convention via extern "C". No cgo bridge needed.

Timers & monotime — time_<os>_<arch>.s

Small files providing the absolute-minimum latency paths for nanotime() and friends. On modern Linux they use vDSO entries (__vdso_clock_gettime) that bypass the syscall boundary.

Writeonce uses std::time::Instant::now() which internally invokes the same vDSO through glibc — already optimised, no added value in rolling our own.

ASAN / MSAN / race detector — asan_<arch>.s, race_<arch>.s

Hookable entry points for sanitisers. Not relevant to writeonce.

What's NOT in asm

Everything else in Go's runtime is plain Go: the scheduler's policy (proc.go), the garbage collector (mgc.go etc.), netpoll dispatch (netpoll.go — Go code; the platform-specific backends like netpoll_epoll.go are also pure Go that call into the asm epollwait trampoline). The asm is strictly the three categories in 00-overview.md: calling-convention-breaking operations, arch-specific instructions, and syscall trampolines.

The three categories writeonce also needs a solution for — but writeonce gets all three from Rust stdlib + libc. The next doc catalogues those mappings.