recompiler

package
v0.0.0-...-c7fb743 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 24, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

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

View Source
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

View Source
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).

View Source
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)
)
View Source
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):

View Source
const DefaultExecutableSize = 16 * 1024 * 1024 // 16MB
View Source
const DjumpCallID = 0xFE

DjumpCallID is the sentinel host-call ID for indirect jumps (jump_ind, load_imm_jump_ind).

View Source
const PVMRegCount = 13

PVMRegCount is the number of PVM general-purpose registers (RA, SP, T0–T2, S0–S1, A0–A5).

View Source
const SbrkCallID = 0xFF

SbrkCallID is the sentinel host-call ID used by emitSbrk to exit to Go.

Variables

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

func EmitEntryTrampoline(a *asm.Assembler)

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:

  1. Saves host callee-saved registers
  2. Sets R15 = guestBase (from RAX)
  3. Saves return address & RSP into control region (for signal handler)
  4. Loads 13 PVM registers from control region
  5. Jumps to target block (address in RBX, saved before overwrite)

On return (via exit trampoline or signal handler jumping to "return_label"):

  1. Restores host callee-saved registers
  2. Returns to Go caller

func EmitExitTrampoline

func EmitExitTrampoline(a *asm.Assembler)

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:

  1. Saves all 13 PVM registers back to the control region
  2. Restores RSP from control region (ReturnStack)
  3. Jumps to return_label (ReturnAddr in control region)

func EmitHostCallExit

func EmitHostCallExit(a *asm.Assembler, exitReason int32, nextPC int32)

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:

  1. Locks the current goroutine to its OS thread (required for TLS)
  2. Sets the per-thread guest base pointer for the signal handler
  3. Builds and caches the entry trampoline
  4. Calls into native code via callNative
  5. 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 PVMReg

func PVMReg(index uint8) asm.Register

PVMReg returns the x86-64 register assigned to a PVM register index.

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 NewCodeCache

func NewCodeCache() *CodeCache

NewCodeCache creates an empty CodeCache.

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

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

func (cc *CodeCache) Invalidate() error

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

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

type Recompiler struct {
	Trace *PVMtrace.Trace
	// contains filtered or unexported fields
}

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

Directories

Path Synopsis
Package asm provides a minimal x86-64 assembler for the PVM JIT recompiler.
Package asm provides a minimal x86-64 assembler for the PVM JIT recompiler.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL