Documentation
¶
Overview ¶
Package recompiler implements the PVM JIT recompiler's memory management layer, including executable memory (W^X), guest memory (unified mmap with slice aliasing), code cache, and control region accessors for the Generic Sandbox memory layout.
Index ¶
- Constants
- Variables
- func EmitEntryTrampoline(a *asm.Assembler)
- func EmitExitTrampoline(a *asm.Assembler)
- func EmitHostCallExit(a *asm.Assembler, exitReason int32, nextPC int32)
- func ExecuteBlock(ctx *JITContext, block *CompiledBlock) PVM.ExitReason
- func FlushProfile()
- func HandleSbrk(ctx *JITContext, rD, rA uint8) PVM.ExitReason
- func IsDjumpExit(reason PVM.ExitReason) bool
- func IsSbrkExit(reason PVM.ExitReason) bool
- func PVMReg(index uint8) asm.Register
- func Psi_M_recompiler(code PVM.StandardCodeFormat, counter PVM.ProgramCounter, gas types.Gas, ...) PVM.Psi_M_ReturnType
- type CodeCache
- type CompiledBlock
- type CompiledProgram
- type Compiler
- type ExecutableMemory
- type JITContext
- func (ctx *JITContext) ClearMemAccess()
- func (ctx *JITContext) Close() error
- func (ctx *JITContext) GuestBase() uintptr
- func (ctx *JITContext) GuestMem() []byte
- func (ctx *JITContext) GuestMemory() PVM.GuestMemory
- func (ctx *JITContext) HasMemAccess() bool
- func (ctx *JITContext) InitFromProgram(p PVM.StandardCodeFormat, a PVM.Argument) (PVM.Instructions, PVM.Registers, error)
- func (ctx *JITContext) ReadExitPC() PVM.ProgramCounter
- func (ctx *JITContext) ReadExitReason() PVM.ExitReason
- func (ctx *JITContext) ReadGas() PVM.Gas
- func (ctx *JITContext) ReadGasInto(dst *PVM.Gas)
- func (ctx *JITContext) ReadHeapPointer() uint64
- func (ctx *JITContext) ReadMemAccess() (addr uint32, val uint64)
- func (ctx *JITContext) ReadRegister(idx uint8) uint64
- func (ctx *JITContext) ReadRegisters() PVM.Registers
- func (ctx *JITContext) ReadRegistersInto(dst *PVM.Registers)
- func (ctx *JITContext) SetExecutableMemory(em *ExecutableMemory)
- func (ctx *JITContext) SetPageAccess(pageNum uint32, prot int) error
- func (ctx *JITContext) WriteExitPC(pc PVM.ProgramCounter)
- func (ctx *JITContext) WriteExitReason(reason PVM.ExitReason)
- func (ctx *JITContext) WriteGas(gas PVM.Gas)
- func (ctx *JITContext) WriteHeapPointer(hp uint64)
- func (ctx *JITContext) WriteRegisters(regs PVM.Registers)
- type Recompiler
Constants ¶
const ( ControlRegionSize = 4096 // 4KB control region before guest memory GuestMemorySize = 4 * 1024 * 1024 * 1024 // 4GB PVM address space GuardPageSize = 4096 // 4KB guard page after guest memory TotalMmapSize = ControlRegionSize + GuestMemorySize + GuardPageSize )
Generic Sandbox mmap layout sizes
const ( OffsetReturnStack = 8 // R15 - 8: uintptr (signal handler RSP restore) OffsetReturnAddr = 16 // R15 - 16: uintptr (signal handler return address) OffsetHeapPointer = 24 // R15 - 24: uint64 OffsetExitPC = 32 // R15 - 32: uint32 (+ 4B padding) OffsetExitReason = 40 // R15 - 40: PVM.ExitReason (uint64) OffsetGas = 48 // R15 - 48: int64 — within disp8 range (-128..+127) OffsetRegisters = 152 // R15 - 152: [13]uint64 = 104 bytes (R15-152 .. R15-49) )
Control region field offsets (negative from R15 = guestBase). Fields are laid out from the end of the control region backward, with the most frequently accessed fields closest to R15 (smallest offset).
const ( OffsetMemAccessAddr = 160 // R15 - 160: uint32 (+ 4B padding) OffsetMemAccessVal = 168 // R15 - 168: uint64 // JIT djump metadata pointers (Go-heap / rodata addresses; read-only during invoke). OffsetDjumpTable = 176 // R15 - 176: uintptr — jump table rodata in ExecutableMemory OffsetDjumpBitmask = 184 // R15 - 184: uintptr — bitmask rodata in ExecutableMemory OffsetDjumpDispatch = 192 // R15 - 192: uintptr — PC→native dispatch table ([]uintptr) )
const ( RegGuestBase = asm.R15 // R15 = guest memory base; control region at [R15 - offset] RegScratch = asm.RCX // scratch for DIV (needs CL), shifts, address calculation )
Reserved registers (not allocated to any PVM register):
const DefaultExecutableSize = 16 * 1024 * 1024 // 16MB
const DjumpCallID = 0xFE
DjumpCallID is the sentinel host-call ID for indirect jumps (jump_ind, load_imm_jump_ind).
const PVMRegCount = 13
PVMRegCount is the number of PVM general-purpose registers (RA, SP, T0–T2, S0–S1, A0–A5).
const SbrkCallID = 0xFF
SbrkCallID is the sentinel host-call ID used by emitSbrk to exit to Go.
Variables ¶
var PVMToX86 = [PVMRegCount]asm.Register{ asm.RAX, asm.RDX, asm.RBX, asm.RSI, asm.RDI, asm.R8, asm.R9, asm.R10, asm.R11, asm.R12, asm.R13, asm.R14, asm.RBP, }
PVMToX86 maps each PVM register index to its dedicated x86-64 native register. All 13 PVM registers are statically assigned — zero spill.
Allocation rationale:
- High-frequency registers (RA, SP, T0–T2, A5) get non-REX registers (saves 1 byte/instruction)
- Lower-frequency registers (S0, S1, A0–A4) get R8–R14
- RA/SP control-region slots use disp8 offsets (see pvmRegSlot) for division spill paths
Functions ¶
func EmitEntryTrampoline ¶
EmitEntryTrampoline generates the Go → JIT entry trampoline.
Calling convention (Go internal ABI, Go 1.17+):
RAX = guestBase (R15's value) RBX = target native code address to jump to
The trampoline:
- Saves host callee-saved registers
- Sets R15 = guestBase (from RAX)
- Saves return address & RSP into control region (for signal handler)
- Loads 13 PVM registers from control region
- Jumps to target block (address in RBX, saved before overwrite)
On return (via exit trampoline or signal handler jumping to "return_label"):
- Restores host callee-saved registers
- Returns to Go caller
func EmitExitTrampoline ¶
EmitExitTrampoline generates the JIT → Go exit stub. This is emitted once and shared by all exit paths (halt, panic, OOG, host call). The caller must set ExitReason and ExitPC *before* jumping here.
The trampoline:
- Saves all 13 PVM registers back to the control region
- Restores RSP from control region (ReturnStack)
- Jumps to return_label (ReturnAddr in control region)
func EmitHostCallExit ¶
EmitHostCallExit emits an inline exit sequence for a specific ecalli instruction. It stores ExitReason (encoded as host_call + callID), ExitPC (next PVM PC), then jumps to exit_trampoline.
exitReason should be pre-encoded: (uint64(callID) << 8) | ExitHostCall nextPC is the PVM PC of the instruction after ecalli.
func ExecuteBlock ¶
func ExecuteBlock(ctx *JITContext, block *CompiledBlock) PVM.ExitReason
ExecuteBlock runs a single compiled basic block through the JIT entry/exit trampoline and returns the ExitReason. The caller is responsible for interpreting the exit reason (halt, panic, page fault, host call, OOG) and driving the execution loop accordingly.
This function:
- Locks the current goroutine to its OS thread (required for TLS)
- Sets the per-thread guest base pointer for the signal handler
- Builds and caches the entry trampoline
- Calls into native code via callNative
- Reads the exit reason from the control region
func FlushProfile ¶
func FlushProfile()
FlushProfile prints a final cumulative [JIT-PROFILE TOTAL] line. Callers should defer it from main (or call on shutdown) so short-lived runs — which exit before the ticker fires — still emit output. No-op when profiling is disabled.
func HandleSbrk ¶
func HandleSbrk(ctx *JITContext, rD, rA uint8) PVM.ExitReason
HandleSbrk performs the sbrk heap expansion in Go, updating the control region heap pointer and mprotecting newly required pages. rD and rA are the PVM register indices from the sbrk instruction encoding. Returns the ExitReason to propagate (ExitContinue on success).
func IsDjumpExit ¶
func IsDjumpExit(reason PVM.ExitReason) bool
IsDjumpExit returns true if the exit reason is an indirect jump request.
func IsSbrkExit ¶
func IsSbrkExit(reason PVM.ExitReason) bool
IsSbrkExit returns true if the exit reason is an sbrk request.
func Psi_M_recompiler ¶
func Psi_M_recompiler( code PVM.StandardCodeFormat, counter PVM.ProgramCounter, gas types.Gas, argument PVM.Argument, omegas PVM.Omegas, addition PVM.HostCallArgs, ) PVM.Psi_M_ReturnType
Types ¶
type CodeCache ¶
type CodeCache struct {
// contains filtered or unexported fields
}
CodeCache maps PVM Program Counters to compiled basic blocks.
Thread-safe: with the cross-invocation cache a CodeCache is shared by a CompiledProgram across concurrent invocations (parallel accumulate). Reads (Get/Has) take a read lock; writes (Put) take a write lock. Compilation is additionally serialized by CompiledProgram.mu, so Put never races with another Put, but Get can run concurrently with a compile on another goroutine.
func (*CodeCache) BindExecutableMemory ¶
func (cc *CodeCache) BindExecutableMemory(em *ExecutableMemory)
BindExecutableMemory associates the JIT code arena with this cache so Invalidate can reclaim emitted native code.
func (*CodeCache) Get ¶
func (cc *CodeCache) Get(pc PVM.ProgramCounter) *CompiledBlock
Get looks up a compiled block by its PVM start PC. Returns nil if the block has not been compiled.
func (*CodeCache) Has ¶
func (cc *CodeCache) Has(pc PVM.ProgramCounter) bool
Has reports whether a block has been compiled for the given PC.
func (*CodeCache) Invalidate ¶
Invalidate discards all compiled blocks and resets bound ExecutableMemory when it is writable (INT3-fill and used=0).
func (*CodeCache) Put ¶
func (cc *CodeCache) Put(block *CompiledBlock)
Put stores a compiled block in the cache, keyed by its PVMStartPC.
type CompiledBlock ¶
type CompiledBlock struct {
PVMStartPC PVM.ProgramCounter // first PVM instruction PC in this block
PVMEndPC PVM.ProgramCounter // one-past-last PVM instruction PC
NativeAddr uintptr // callable native code address (precomputed)
NativeOffset int // byte offset into ExecutableMemory (for debug)
NativeSize int // size of emitted native code in bytes
GasCost int64 // total gas cost for this block (= number of PVM instructions)
InstrCount int // number of PVM instructions in this block
}
CompiledBlock holds metadata for a single compiled PVM basic block.
type CompiledProgram ¶
type CompiledProgram struct {
// contains filtered or unexported fields
}
CompiledProgram is the per-CodeHash compiled artifact shared across invocations: the executable code arena, the PC→block cache, djump support, and the pre-emitted entry trampoline. Per-invocation guest state lives in a fresh JITContext bound via bindContext.
type Compiler ¶
type Compiler struct {
// contains filtered or unexported fields
}
func NewCompiler ¶
func NewCompiler(program *PVM.Program, ctx *JITContext, cache *CodeCache) *Compiler
func (*Compiler) CompileBasicBlock ¶
func (c *Compiler) CompileBasicBlock(startPC PVM.ProgramCounter) (*CompiledBlock, error)
type ExecutableMemory ¶
type ExecutableMemory struct {
// contains filtered or unexported fields
}
ExecutableMemory manages a JIT code region as a **dual mapping** of one memfd-backed allocation:
- rwMem (PROT_READ|PROT_WRITE) — code is *written* here
- rxMem (PROT_READ|PROT_EXEC) — code is *executed* here (base = rxBase)
Both views alias the same physical pages (MAP_SHARED on the same fd), so code written through rwMem is immediately runnable through rxBase with **no per-write mprotect**. This removes the per-block W^X toggle that the profile showed to be ~49% of total time (PERFORMANCE.md §1.2 / §4.0): QEMU re-translated the whole 16MB arena on every PROT_EXEC flip.
Trade-off: the pages are simultaneously writable (rwMem) and executable (rxMem) at different virtual addresses — a relaxation of strict W^X, acceptable for this JIT sandbox because guest code cannot obtain the rwMem address.
INVARIANT: every callable/stored address — CompiledBlock.NativeAddr, the djump dispatch entries, the entry trampoline, and the signal-handler fault window — MUST come from GetPtr, i.e. the rxBase (executable) view, because that is where execution actually happens. Writes go only through Write → rwMem.
amd64 note: x86-64 keeps the instruction cache coherent with data writes, so no explicit icache flush / barrier is needed between Write (rwMem) and execution (rxBase) on the same core; cross-core coherency is guaranteed by hardware.
func NewExecutableMemory ¶
func NewExecutableMemory(size int) (*ExecutableMemory, error)
NewExecutableMemory allocates a memfd-backed dual mapping of the given size. Both an RW and an RX mapping of the same backing pages are created up front, so no mprotect is ever needed during compilation or execution.
func (*ExecutableMemory) Close ¶
func (em *ExecutableMemory) Close() error
Close unmaps both views and closes the backing fd.
func (*ExecutableMemory) GetPtr ¶
func (em *ExecutableMemory) GetPtr(offset int) uintptr
GetPtr returns the callable (executable-view) address at the given byte offset.
func (*ExecutableMemory) Reset ¶
func (em *ExecutableMemory) Reset() error
Reset discards all written code (INT3-fill through the writable view) and resets the cursor. The executable view sees the INT3 immediately (same pages).
func (*ExecutableMemory) Size ¶
func (em *ExecutableMemory) Size() int
Size returns the total capacity in bytes.
func (*ExecutableMemory) Used ¶
func (em *ExecutableMemory) Used() int
Used returns the number of bytes currently written.
func (*ExecutableMemory) Write ¶
func (em *ExecutableMemory) Write(code []byte) (offset int, err error)
Write appends native code through the writable view and returns the byte offset where it was placed. No mprotect: the executable view (GetPtr) sees the bytes immediately because both views map the same physical pages.
type JITContext ¶
type JITContext struct {
// contains filtered or unexported fields
}
JITContext holds the unified mmap region (control region + guest memory + guard page) and provides Go-side accessors for the control region fields.
func NewJITContext ¶
func NewJITContext() (*JITContext, error)
NewJITContext allocates the unified mmap region for the Generic Sandbox layout:
[control region 4KB] [guest memory 4GB] [guard page 4KB]
func (*JITContext) ClearMemAccess ¶
func (ctx *JITContext) ClearMemAccess()
ClearMemAccess resets the memory access fields to zero.
func (*JITContext) Close ¶
func (ctx *JITContext) Close() error
Close releases the unified mmap region.
func (*JITContext) GuestBase ¶
func (ctx *JITContext) GuestBase() uintptr
GuestBase returns the guest memory base address as a uintptr (R15's value in JIT code).
func (*JITContext) GuestMem ¶
func (ctx *JITContext) GuestMem() []byte
GuestMem returns the guest memory byte slice (4GB).
func (*JITContext) GuestMemory ¶
func (ctx *JITContext) GuestMemory() PVM.GuestMemory
GuestMemory returns the PVM.GuestMemory view of this context for omega / R(). The returned value is a single pointer (pointer-shaped), so assigning it to a GuestMemory interface is allocation-free.
func (*JITContext) HasMemAccess ¶
func (ctx *JITContext) HasMemAccess() bool
HasMemAccess returns true if the last instruction recorded a memory access.
func (*JITContext) InitFromProgram ¶
func (ctx *JITContext) InitFromProgram(p PVM.StandardCodeFormat, a PVM.Argument) (PVM.Instructions, PVM.Registers, error)
InitFromProgram decodes a PVM program blob and maps its segments into the mmap'd guest memory region with correct mprotect permissions. Guest memory is a contiguous 4GB []byte — all subsequent access (from both JIT native code and Go host calls) is simply guestMem[addr : addr+size].
func (*JITContext) ReadExitPC ¶
func (ctx *JITContext) ReadExitPC() PVM.ProgramCounter
func (*JITContext) ReadExitReason ¶
func (ctx *JITContext) ReadExitReason() PVM.ExitReason
func (*JITContext) ReadGas ¶
func (ctx *JITContext) ReadGas() PVM.Gas
func (*JITContext) ReadGasInto ¶
func (ctx *JITContext) ReadGasInto(dst *PVM.Gas)
ReadGasInto is designed for host-call hot path to keep the snapshot heap-free.
func (*JITContext) ReadHeapPointer ¶
func (ctx *JITContext) ReadHeapPointer() uint64
func (*JITContext) ReadMemAccess ¶
func (ctx *JITContext) ReadMemAccess() (addr uint32, val uint64)
ReadMemAccess reads the memory access addr and value from the control region.
func (*JITContext) ReadRegister ¶
func (ctx *JITContext) ReadRegister(idx uint8) uint64
ReadRegister reads a single PVM register by index from the control region.
func (*JITContext) ReadRegisters ¶
func (ctx *JITContext) ReadRegisters() PVM.Registers
ReadRegisters reads all 13 PVM registers from the control region. Registers are stored as [13]uint64 starting at guestBase - OffsetRegisters.
func (*JITContext) ReadRegistersInto ¶
func (ctx *JITContext) ReadRegistersInto(dst *PVM.Registers)
ReadRegistersInto reads all 13 PVM registers from the control region into the caller's buffer, keeping the snapshot heap-free.
func (*JITContext) SetExecutableMemory ¶
func (ctx *JITContext) SetExecutableMemory(em *ExecutableMemory)
SetExecutableMemory attaches an ExecutableMemory to this context.
func (*JITContext) SetPageAccess ¶
func (ctx *JITContext) SetPageAccess(pageNum uint32, prot int) error
SetPageAccess changes the hardware protection of a guest memory page.
func (*JITContext) WriteExitPC ¶
func (ctx *JITContext) WriteExitPC(pc PVM.ProgramCounter)
func (*JITContext) WriteExitReason ¶
func (ctx *JITContext) WriteExitReason(reason PVM.ExitReason)
func (*JITContext) WriteGas ¶
func (ctx *JITContext) WriteGas(gas PVM.Gas)
func (*JITContext) WriteHeapPointer ¶
func (ctx *JITContext) WriteHeapPointer(hp uint64)
func (*JITContext) WriteRegisters ¶
func (ctx *JITContext) WriteRegisters(regs PVM.Registers)
WriteRegisters writes all 13 PVM registers to the control region.
type Recompiler ¶
Recompiler is the machine layer of the JIT backend, symmetrical to PVM.Interpreter on the interpreter backend. It owns compilation and native execution over a JITContext, but not host-call dispatch state. Host-call orchestration (OOG, HALT, PANIC handling, sbrk, Omega dispatch) lives in host, which drives Recompiler.BlockBasedInvoke in a loop.
func NewRecompiler ¶
func NewRecompiler(program *PVM.Program, ctx *JITContext) *Recompiler
NewRecompiler builds a Recompiler over a fresh uncached artifact bound to ctx's executable memory. Used by tests and any non-Psi_M caller; the Psi_M backend uses the cached path (acquireCompiledProgram + newRecompiler). The caller owns the ctx lifetime (Close/Release).
func (*Recompiler) BlockBasedInvoke ¶
func (r *Recompiler) BlockBasedInvoke(pc PVM.ProgramCounter) (PVM.ExitReason, PVM.ProgramCounter)
BlockBasedInvoke runs one or more compiled basic blocks until the native side signals a non-CONTINUE exit.
func (*Recompiler) Ctx ¶
func (r *Recompiler) Ctx() *JITContext
func (*Recompiler) MachineInvoke ¶
func (r *Recompiler) MachineInvoke(pc PVM.ProgramCounter) (PVM.ExitReason, PVM.ProgramCounter)
MachineInvoke runs native PVM execution until a non-CONTINUE exit.
func (*Recompiler) Program ¶
func (r *Recompiler) Program() *PVM.Program
Source Files
¶
- block_link.go
- call_native.go
- code_cache.go
- compiled_program.go
- compiler.go
- context.go
- djump_native.go
- emit_arith_imm.go
- emit_arith_three.go
- emit_basic.go
- emit_branch.go
- emit_memory.go
- emit_meta.go
- emit_record_mem.go
- emit_two_reg.go
- executable.go
- execute.go
- gas.go
- guest_memory.go
- host.go
- host_call_notrace.go
- invoke_mode.go
- jit_hotpath.go
- jit_perfmap.go
- psi_m_recompiler.go
- recompiler.go
- register_map.go
- runtime_exit.go
- sbrk.go
- trampoline.go