Skip to content

How translation works

Champollion is user-mode only. A guest program is not a simulated machine; it is the host process itself. rvrun loads the ELF, maps its segments into a reserved guest address space, and starts executing at its entry point. When the program makes a system call, the runtime serves it — implemented directly where possible, translated to the host where it is not.

The loader parses the ELF, maps its segments into guest memory (a 256 GiB reservation; on macOS a guest address is a fixed offset into it, on arm64 Linux guest addresses are host addresses), builds the initial Linux stack, and relocates static-PIE images. When the program is dynamically linked, its PT_INTERP — musl’s ld-musl-riscv64.so.1 — runs first, resolved against the sysroot passed with -L.

The interpreter fetches and decodes every instruction it executes, and it is the permanent oracle: the other engines are diffed against it rather than trusted. The fast engine keeps a cache of decoded blocks, chained to their last two successors, plus a per-thread software TLB over the guest’s 4 KiB pages. The cache is dropped when memory mappings change or the guest runs fence.i.

The riscv-tests suite runs the fast engine and the JIT against the interpreter and requires identical state at the end of every case.

Blocks are translated to arm64 by a hand-written encoder whose output is checked against the assembler in tests. Generated code keeps guest addresses masked into the reservation and reaches translated successors through a jump cache. Translated blocks are saved per executable in a persistent cache (~/Library/Caches/champollion by default), so the second run of a program starts translated; RVRUN_CACHE=0 turns that off, and RVRUN_JIT=0 drops back to the interpreter.

Guest syscalls use the Linux ABI. Some are served by the runtime itself (mmap, brk, and similar calls); the rest cross a small HostOs interface. macOS backend translates the differences — open flag bits, errno values, signals, flock — and its Linux backend passes calls straight to the kernel, identity address space included. Errors come back as Linux errno values either way, and a host thread pumps signals into the guest.