Skip to content

Programs as SBF Bytecode

The part overview framed the runtime as the layer that has to run everyone’s code on every validator and still have all of them agree, byte for byte, on the resulting state. That raises a blunt question this page answers: when you deploy a program, what is the thing that gets deployed? Not “a smart contract” in the abstract — the actual bytes that land on-chain and that every validator executes.

The answer is a small, boring, well-chosen binary format called SBF — the Solana Bytecode Format. This page is about that format: what it is, why Solana runs bytecode at all instead of native code, why the bytecode is register-based (and how that differs from the EVM’s stack machine), and what the program’s one entrypoint actually receives. Everything later in this part — the loader and verifier, the syscall sandbox, the compute meter — exists to run this format safely and deterministically. So we start here.

You write a Solana program in a normal systems language — usually Rust, sometimes C. But you do not ship Rust, and you do not ship the x86-64 machine code that Rust would normally compile to for your laptop. You ship an intermediate thing: bytecode.

your_program.rs ──(LLVM, sbf target)──► program.so ──deploy──► on-chain account (executable)
Rust source compile SBF bytecode every validator runs THIS

The compiler is ordinary LLVM, aimed at a special backend target (sbf-solana-solana, historically bpfel-unknown-unknown). The output is an ELF shared object — a .so file — but it does not contain instructions your CPU can run. It contains SBF instructions: a compact, fixed-width bytecode that a validator’s SBF virtual machine reads and executes. That .so gets uploaded into an on-chain account marked executable, and from then on the bytes are the program.

Two properties of that binary matter enormously, and both come straight from the throughline — a single global state machine that runs at hardware speed without falling apart:

  • It is position-independent. The code makes no assumption about the absolute memory address it loads at. The runtime maps it into a fresh sandbox each invocation; the same bytes run identically no matter where in memory they land. Determinism starts here.
  • It is stateless. The binary is pure logic. It carries no heap that survives between calls, no global mutable state, no ambient access to disk or network. Every bit of mutable state it touches arrives as an argument (the accounts), a point the account model makes in full.

It is worth stopping on the “why,” because shipping bytecode is not free — it needs a VM to interpret or JIT it, which is slower than native code and more machinery to build. So why not let validators run native .so files compiled for their own CPUs?

Because the runtime’s whole job is replicated determinism. Hundreds of validators, on different hardware, different operating systems, different compiler versions, must execute the identical logic and converge on the identical state — or consensus fractures. Native machine code fails that test three ways:

  1. Portability. Native code is tied to one instruction set (x86-64 vs ARM). A program compiled for one validator’s CPU would not run on another’s.
  2. Determinism. Native CPUs expose behavior that legitimately differs across chips — floating-point edge cases, timing, undefined instructions. Consensus cannot tolerate “the answer depends on your CPU.”
  3. Safety. Native code can do anything the process can: read arbitrary memory, call arbitrary syscalls, loop forever. You cannot let untrusted, user-uploaded code do that inside a validator.

Bytecode solves all three at once. It is portable (the VM is the same everywhere), it is deterministic (the VM defines exactly what each instruction does), and it is containable — a bounded interpreter or JIT can check the code, meter it, and refuse anything it cannot prove safe. Shipping a portable, verifiable binary is what lets every validator run the identical logic and agree on the result. That is the entire reason the format exists.

native .so per validator SBF bytecode + one VM
─────────────────────── ─────────────────────
different ISAs → won't run one bytecode → runs everywhere
CPU-dependent behavior → divergence VM-defined semantics → determinism
full process access → unsafe bounded VM → sandboxed & metered

Solana did not invent this format from scratch. SBF is a dialect of eBPF — the same “extended Berkeley Packet Filter” bytecode the Linux kernel uses to run small, untrusted programs safely inside kernel space (for packet filtering, tracing, security policy). That lineage is not a coincidence; it is the entire reason for the choice.

eBPF was already engineered for the exact problem Solana has: run untrusted code fast, in a hostile host, without letting it misbehave. The kernel cannot let a BPF program crash the kernel or read arbitrary memory, so eBPF was designed to be statically verifiable and cheaply JIT-compiled to native code. Solana took that mature design and adapted it — the “S” in SBF — relaxing some kernel-specific limits (the Linux verifier forbids unbounded loops; Solana instead meters execution with compute units) and swapping the kernel’s helper functions for Solana’s own syscalls.

The inherited design gives SBF three defining traits:

  • Fixed 64-bit registers. There are a small, fixed number of general-purpose registers (r0r10), each exactly 64 bits wide. r10 is a read-only frame pointer; r0 carries the return value; the rest are scratch and arguments. That is the entire register file.
  • A small, bounded instruction set. Loads, stores, arithmetic, jumps, and calls — a few dozen fixed- width opcodes, each 64 bits. No sprawling, decades-accreted instruction set; a compact one a verifier can reason about exhaustively.
  • No unbounded native access. An SBF program cannot issue an arbitrary machine syscall. The only way it reaches the outside world is through the small, fixed set of syscalls the runtime chooses to expose (log a message, hash some bytes, invoke another program). Everything else is unreachable by construction.

Each of those is a deliberate ceiling. Fixed registers and fixed-width opcodes make the code trivial to decode and check. A bounded instruction set means the verifier has a finite thing to verify. No native syscalls means the sandbox has a small, auditable surface. The format was chosen precisely so that a bounded interpreter or JIT can run it safely — which is the property the next page, on the loader and verifier, cashes in.

Under the hood — register machine vs. stack machine

Section titled “Under the hood — register machine vs. stack machine”

The single most important structural fact about SBF is that it is a register machine, and the reason it matters is clearest when you contrast it with the EVM.

A stack machine (like the EVM) has no named registers. Every operation pops its operands off an operand stack and pushes its result back. To add two numbers you PUSH a, PUSH b, ADD — three instructions, and the values live on an implicit stack.

A register machine (like SBF) names its operands directly. To add two numbers you emit one instruction: add64 r1, r2 — “add register 2 into register 1.” No stack juggling.

EVM (256-bit stack machine) SBF (64-bit register machine)
─────────────────────────── ─────────────────────────────
PUSH a add64 r1, r2 ; one instruction
PUSH b (operands named directly)
ADD no operand stack
result on the stack result in a register
word size: 256 bits word size: 64 bits

Why does Solana prefer the register form? Because register bytecode maps almost one-to-one onto real CPU instructions, which makes it fast to JIT-compile. A modern CPU is a register machine. Turning add64 r1, r2 into a native add is nearly a direct translation; turning a sequence of stack pushes and pops into efficient native code requires a compiler to reconstruct the data flow the stack hid. Solana’s VM JITs SBF to native machine code so that on-chain programs run at close to native speed — the “hardware speed” half of the book’s question. A stack machine can be JIT’d too, but the register form starts far closer to the metal.

The 256-bit vs 64-bit word size is the other half of the story. The EVM’s 256-bit words exist to hold Ethereum’s hashes and balances as single native values, but no real CPU has 256-bit integer registers, so every EVM word operation is emulated in software. SBF’s 64-bit registers are what the CPU has, so its arithmetic runs directly.

None of this makes SBF “better” than the EVM in the abstract. Both are deterministic, replicated virtual machines — that is the shared, non-negotiable requirement, and both meet it. They just optimize different things. The EVM optimizes for a compact, self-contained semantics tuned to Ethereum’s 256-bit world and a simple gas model. SBF optimizes for raw execution speed and JIT-friendliness, accepting a more complex sandbox (a verifier, a compute meter, an explicit syscall boundary) as the price. It is the same trade the whole of Solana keeps making: more machinery, in exchange for hardware speed.

So the deployed artifact is a stateless, position-independent SBF binary. How does the runtime call it?

Every Solana program exposes exactly one entrypoint. By convention it is a function named process_instruction, and the runtime hands it three things and nothing else:

use solana_program::{
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey,
};
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey, // which program is this? (the executable account's address)
accounts: &[AccountInfo], // the ordered list of accounts this transaction handed us
instruction_data: &[u8], // an opaque blob: which action, with what arguments
) -> ProgramResult {
// ... read/validate accounts, mutate the writable ones, return Ok or Err ...
Ok(())
}

Read those three arguments as the whole interface between the runtime and your code:

  • program_id — the address of this program’s executable account. A program needs to know its own id to derive the program-derived addresses it owns.
  • accounts — the accounts the transaction named, in the exact order it named them. This is the program’s entire view of mutable state. It cannot reach out to any account not in this list; it can only read and write the buffers handed to it.
  • instruction_data — an opaque byte slice the runtime does not interpret. It is the program’s private argument encoding: usually a tag byte selecting a sub-instruction, followed by that instruction’s serialized arguments.

That is deliberately the same shape as the pure function in the book’s companion crate, solmini, where a program is a zero-sized struct with one method:

// From solmini's `Program` trait — the same interface, framework stripped away.
pub trait Program: Send + Sync {
fn process(&self, accounts: &mut [Account], data: &[u8]) -> Result<()>;
}

The real entrypoint! macro is that method plus the deserialization glue: it takes the raw bytes the runtime lays out in the program’s input region, parses them into program_id, &[AccountInfo], and &[u8], calls your function, and reports the Result back. Anchor sits one layer higher still, generating the account-validation and dispatch code on top of this same entrypoint. Strip every layer and you are back at stateless code plus one entrypoint over a handed-in account list — which is exactly the model the account-model part builds from first principles, and exactly what makes a transaction’s footprint knowable ahead of time so the Sealevel runtime can run non-conflicting transactions in parallel.

  • Why does it exist? Because a global, replicated state machine needs every validator to execute identical logic and reach identical state. A portable, verifiable bytecode — one VM everywhere — makes that possible; native per-CPU machine code cannot.
  • What problem does it solve? Running untrusted, user-uploaded code fast, on heterogeneous hardware, without letting it diverge, escape, or hang. Borrowing eBPF’s design gets a format built for exactly that.
  • What are the trade-offs? More machinery than a stack VM — a verifier, a JIT, a compute meter, an explicit syscall boundary — plus a smaller op set programs must fit within. The payoff is JIT-to-native speed and a tight, auditable sandbox.
  • When should I avoid it? You do not choose SBF à la carte; it is the runtime’s substrate. But its ceilings do shape what you write on-chain: heavy numeric work, floating point, or large in-program computation belong off-chain, with only the verifiable result submitted.
  • What breaks if I remove it? Determinism and safety both. Without a shared bytecode + VM, validators on different hardware would compute different results and consensus would fracture; and untrusted code would run with full native access to the validator process.
  1. You deploy a program written in Rust. What are the actual bytes that end up in the on-chain executable account — Rust source, x86-64 machine code, or something else? Why that choice?
  2. Give the three ways native machine code fails the runtime’s “replicated determinism” requirement, and how bytecode fixes each.
  3. SBF is a register machine and the EVM is a stack machine. Explain the practical consequence of that difference for execution speed, and name one thing the EVM optimizes for instead.
  4. List the three arguments a program’s process_instruction entrypoint receives, and say what each one is. Which of them is the program’s entire view of mutable state?
  5. SBF is derived from Linux’s eBPF. What property of eBPF made it the right starting point, and name one thing Solana changed when adapting it.
Show answers
  1. SBF bytecode. The Rust is compiled by LLVM to the SBF target, producing an ELF .so of SBF instructions (not native machine code), which is uploaded into an executable account. Bytecode is chosen because it is portable across validator hardware, has VM-defined (deterministic) semantics, and can be verified and sandboxed — none of which native machine code guarantees.
  2. Portability: native code is tied to one ISA (x86-64 vs ARM), so it would not run on other validators — one bytecode + one VM runs everywhere. Determinism: native CPUs differ in floating- point/timing/undefined behavior — the VM defines exact per-instruction semantics. Safety: native code has full process access — a bounded VM sandboxes and meters it.
  3. Register bytecode names its operands directly (add64 r1, r2) and maps almost one-to-one onto real CPU instructions, so it JIT-compiles to fast native code; a stack machine hides the data flow behind push/pop and needs more work to JIT well. The EVM instead optimizes for compact, self-contained semantics tuned to Ethereum’s 256-bit words and a simple gas model. (Both are still deterministic replicated VMs.)
  4. program_id (this program’s own address), accounts (the ordered list of accounts the transaction handed it), and instruction_data (an opaque byte blob the program interprets as its arguments). accounts is the program’s entire view of mutable state — it can only touch accounts in that list.
  5. eBPF was already engineered to run untrusted code fast inside a hostile host (the Linux kernel), being statically verifiable and cheaply JIT-compiled — exactly Solana’s problem. Solana changed, e.g., the handling of loops (Linux forbids unbounded loops; Solana meters execution with compute units instead) and replaced the kernel’s helper functions with Solana’s own syscalls.