The Problem: Go's Runtime Is a "Control Freak"
To speed up Go, the natural idea is a JIT: compile hot code into machine code and execute it directly. This works in Java or C++, but breaks in Go.
The Go runtime tracks everything — stack pointers, stack growth, GC timing. If you inject your own generated machine code, the runtime sees an unrecognized PC address and throws fatal error: unknown pc, killing the process.
It's like smuggling a hot plate into a strictly managed dorm: the supervisor checks every hour, finds an unfamiliar device, and cuts the power.
The answer: instead of fighting the runtime head-on, become a parasite.
The Three-Step Approach
Step 1: Watch (Tracing)
Don't instrument Go code — that's slow and detectable. Use eBPF to profile from the kernel side. Observe from outside the window while the supervisor checks the room: record the most frequent behaviors at near-zero overhead, completely undetected.
Step 2: Write the Script (Compilation)
Once hot paths are identified, don't generate raw machine code. Instead, rewrite the hot behavior as a "script" in WebAssembly (Wasm).
Wasm is a sandbox. The runtime can't read the machine code inside — and doesn't need to. The Wasm engine (wazero) has already handled all safety concerns. The supervisor just sees "a legitimate student reading a book," unaware of what's written in it.
Step 3: Swap the Actor (Execution)
To execute the script, use a "legitimate student": a trampoline written in Go assembly, carrying an ABI0 declaration as its "student ID."
It raises its hand in class ("I'll answer"), the runtime approves, it secretly reads the script, then raises its hand again ("done"). The runtime only ever sees a legitimate Go assembly function.
The Core Insight
The goal is not "how to make Go support JIT" but "how to get JIT benefits while Go doesn't support JIT."
- Wasm is the firewall isolating the runtime from the JIT
- eBPF is the periscope, observing without being detected
- The trampoline is the springboard, legitimately entering the isolated zone
- JIT code must be pointer-free — the GC can't see JIT-internal pointers; if one references a Go object, the GC may collect it, causing use-after-free crashes.
- Spill to the C heap — when registers run out, spill data to malloc'd C memory, never to Go stacks (the GC scans stacks).
- Bail out immediately on uncovered instructions — don't push through an instruction that might trigger a runtime checkpoint.
- eBPF tracing: kernel-side non-intrusive profiling, near-zero overhead
- Wasm sandbox: wazero engine solves cross-platform/safety/performance; drafter is only ~2B parameter scale
- Trampoline: written in Go assembly with an ABI0 declaration, a legitimate anchor point
- Isolation policy: JIT code must be pointer-free; register spills go to the C heap (malloc)
- Safety anchors: all JIT calls are wrapped in standard Go assembly functions
Risks and Rules
Even with isolation, strict rules apply:
Technical Details
The Feynman Check
Explain it to a freshman: you want to secretly cook hotpot in a strictly supervised dorm.
1. Install a camera outside the window (eBPF) to see when roommates are hungriest; 2. Write the cooking steps as a script (Wasm) in a language the supervisor can't read; 3. Have a legitimate roommate (trampoline) with a student ID cook per the script while the supervisor isn't looking.
The supervisor only sees a normal roommate the whole time.
This is the parasitic architecture: not "turn Go into Java," but "find the optimal solution within Go's constraints." Avoid the strength, don't clash head-on.
---
References: Go Runtime ABIInternal documentation, eBPF kernel documentation, wazero project.