Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Overview

The pipeline

ELF ──▶ decode ──▶ analyse ──▶ CLIF ──▶ Cranelift ──▶ native code

Four crates, split so that decoding is settled before any code generation decision is made:

crateresponsibility
rvtime-coreregisters, instructions, ELF loading. No codegen.
rvtime-craneliftcontrol-flow analysis and CLIF emission.
rvtime-compilercodegen backends, guest memory, trap handling.
rvtimethe public API.

Backends depend on core; core depends on no backend. The seam is the Inst enum, so a second backend could consume the same vocabulary without the first knowing.

One CLIF function per RISC-V function

The central decision, and the one everything else follows from.

An ELF already carries function structure in its symbol table, so there is no whole-program control-flow graph to rebuild. Each RISC-V function becomes one Cranelift function: jal becomes a real call, ret becomes a native return, and the host’s own stack carries return addresses.

The alternative — compiling the whole program into a single function with every basic block in one map, and a jump table for every transfer — is what you are forced into when the input has no function boundaries. It works, but it makes every function’s registers live in one enormous SSA graph, rules out compiling anything independently, and scales badly with program size.

Because functions are separate here, they can be compiled independently, cached independently, and eventually compiled lazily.

What the ELF has to provide

Three things are recovered at load time and cannot be recovered later:

  • Function boundaries, from STT_FUNC symbols. Interior .L* labels share the address space of real functions and are filtered out; treating one as a function splits a body in half.
  • Indirect jump targets, from R_RISCV_64 relocations landing in .text. This is why --emit-relocs is required — see Calls.
  • Segment layout and permissions, from the program headers.

Guest code is never executed

The guest’s instructions are compiled, not interpreted and not run in place. Guest memory is mapped read-only where the image is executable, and never executable at all. The only reason to map .text is that programs read constants out of it.

That also means a guest which corrupts its own code image achieves nothing: the compiled code was generated once and lives in the JIT’s pages.