BPF Instruction Set

The BPF instruction set is the encoding layer of the BPF virtual machine: the precise bit-level format of the instructions that the verifier checks and the JIT compiles. Almost every BPF instruction is exactly 64 bits (8 bytes) wide and is described in the kernel by struct bpf_insn — an 8-bit opcode, a 4-bit destination register, a 4-bit source register, a signed 16-bit offset, and a signed 32-bit immediate (include/uapi/linux/bpf.h, v6.12). The opcode’s low three bits select one of eight instruction classes — arithmetic (ALU/ALU64), jumps (JMP/JMP32), and loads/stores (LD/LDX/ST/STX) — and the remaining bits qualify the operation. A single exception to the 8-byte rule is the wide (16-byte) instruction, used to carry a full 64-bit immediate (BPF_LD | BPF_IMM | BPF_DW). This instruction set is small and regular by design, because the verifier must reason about every opcode; and as of October 2024 it is also a formal internet standard, published as RFC 9669, “BPF Instruction Set Architecture (ISA)” by the IETF BPF Working Group (RFC 9669; LWN). All encodings here are verified against the kernel’s canonical ISA document at the 6.12 LTS tag, which is the working copy that became RFC 9669.

Mental Model

Think of the instruction set as a tiny, rigid grammar. Every basic instruction is the same 8-byte shape, and you decode it by first reading the bottom three bits of the opcode to learn the class, then interpreting the rest of the opcode according to that class’s rules. There are only two layouts for the opcode byte: one for arithmetic/jump instructions and one for load/store instructions. Once you know the class, the rest of the fields (dst_reg, src_reg, off, imm) have fixed meanings. This regularity is the whole point — it is what lets the verifier walk the program one fixed-width instruction at a time, and it is what lets the JIT translate most instructions one-to-one into native machine code.

flowchart TB
  INSN["struct bpf_insn (64 bits)<br/>code:8 · dst_reg:4 · src_reg:4 · off:16 · imm:32"]
  INSN --> OP["opcode byte (code)"]
  OP --> LOW["bottom 3 bits = class"]
  LOW --> C0["LD 0x0 · LDX 0x1<br/>ST 0x2 · STX 0x3"]
  LOW --> C1["ALU 0x4 (32-bit)<br/>ALU64 0x7 (64-bit)"]
  LOW --> C2["JMP 0x5 (64-bit)<br/>JMP32 0x6 (32-bit)"]
  C1 --> ALUFMT["code:4 · source:1 · class:3<br/>source K=imm, X=src_reg"]
  C2 --> ALUFMT
  C0 --> LSFMT["mode:3 · size:2 · class:3<br/>size W/H/B/DW · mode MEM/MEMSX/IMM/ATOMIC"]

Decoding a BPF instruction. What it shows: the fixed 64-bit bpf_insn layout, and how the opcode byte’s bottom three bits pick the class, which in turn picks one of two opcode sub-layouts — code·source·class for ALU/JMP, mode·size·class for load/store. The insight to take: there is exactly one way to decode any instruction, and it is shallow (two table lookups). That shallowness is deliberate: a deep or ambiguous encoding would make the verifier’s per-instruction reasoning harder and the JIT’s translation slower.

The Basic 64-bit Encoding

The canonical structure, from the kernel’s user-facing header (include/uapi/linux/bpf.h, v6.12):

struct bpf_insn {
        __u8    code;           /* opcode */
        __u8    dst_reg:4;      /* dest register */
        __u8    src_reg:4;      /* source register */
        __s16   off;            /* signed offset */
        __s32   imm;            /* signed immediate constant */
};

Walking it field by field:

  • code (8 bits) — the opcode. Its three least-significant bits are the instruction class; the upper bits are class-specific qualifiers (the operation and the source-operand selector for ALU/JMP, or the mode and size for LD/ST).
  • dst_reg (4 bits) — the destination register number, 010. Four bits is exactly enough to name the eleven registers R0R10 of the VM.
  • src_reg (4 bits) — the source register number, 010. For some instructions this field is repurposed: in the 64-bit-immediate (wide) instructions it carries an opcode subtype rather than a register number.
  • off (signed 16 bits) — a signed offset, used as a pointer displacement in loads/stores (*(u64 *)(dst + off)) and as a branch displacement in jumps (measured in 64-bit instructions). A few arithmetic instructions repurpose off to select a variant — SDIV, SMOD, and MOVSX set it non-zero to distinguish signed division/modulo and sign-extending move from their plain counterparts.
  • imm (signed 32 bits) — a signed 32-bit immediate constant, the source operand when an instruction takes a literal instead of a register.

The RFC/ISA spec is precise about byte order: multi-byte fields (off, imm) use the host’s native byte ordering — little-endian fields on a little-endian host, big-endian on a big-endian host — and on a little-endian host the regs byte is laid out as src_reg in the high nibble and dst_reg in the low nibble (swapped on big-endian hosts) (RFC 9669 §Instruction encoding). Unused fields must be cleared to zero. Most instructions do not use every field — a register-to-register add uses code, dst_reg, src_reg and zeroes off and imm.

The Eight Instruction Classes

The class is the bottom three bits of code. There are eight, defined identically in the ISA spec and the kernel header (RFC 9669; classic_vs_extended.rst):

classvaluemeaning
LD0x0non-standard / immediate loads (64-bit imm, legacy packet access)
LDX0x1load into a register from memory: dst = *(src + off)
ST0x2store an immediate to memory: *(dst + off) = imm
STX0x3store a register to memory: *(dst + off) = src
ALU0x432-bit arithmetic/logic
JMP0x564-bit jumps and the call/exit control-flow ops
JMP320x632-bit jumps (comparison on 32-bit operands)
ALU640x764-bit arithmetic/logic

Two design points fall out of this table. First, arithmetic comes in a 32-bit (ALU) and a 64-bit (ALU64) flavor performing otherwise identical operations — ALU operates on the lower 32-bit subregister and zero-extends the result into the full 64-bit register; ALU64 operates on the full width. The same split exists for jumps: JMP compares 64-bit operands, JMP32 compares the 32-bit subregisters. This pairing is why a BPF compiler can emit narrow operations that the JIT maps cleanly onto native 32-bit instructions (e.g. x86 eax-class ops) — the rationale lives in BPF Virtual Machine and Registers. Second, the load/store family has four classes because BPF distinguishes loading into a register (LDX) from the special immediate loads (LD), and storing an immediate (ST) from storing a register (STX).

Class-value trivia

JMP32 (0x6) and ALU64 (0x7) reuse the numeric slots that classic BPF spent on BPF_RET and BPF_MISC. eBPF dropped cBPF’s dedicated return class (it models exit as a JMP op instead) and its A↔X move class, freeing 0x6/0x7 for the 32-bit-jump and 64-bit-ALU classes (classic_vs_extended.rst). The historical comparison is the subject of Classic BPF vs Extended BPF.

Arithmetic and Jumps: the code · source · class Opcode

For ALU, ALU64, JMP, and JMP32, the 8-bit opcode splits into 4 bits of operation code, 1 source-selector bit, and 3 class bits (RFC 9669 §Arithmetic and jump instructions):

+-+-+-+-+-+-+-+-+
|  code |s|class|
+-+-+-+-+-+-+-+-+

The single source bit s decides where the second operand comes from: s = 0 (BPF_K) means use the 32-bit imm field; s = 1 (BPF_X) means use the src_reg register. So one operation code yields two encodings — {ADD, K, ALU64} is dst += imm, {ADD, X, ALU64} is dst += src.

The arithmetic operations (the 4-bit code) are ADD, SUB, MUL, DIV, OR, AND, LSH (left shift), RSH (logical right shift), NEG, MOD, XOR, MOV, ARSH (arithmetic/sign-extending right shift), and END (byte swap) (RFC 9669). A few semantic edges the verifier and JIT must honor exactly:

  • Division and modulo by zero do not trap. DIV by zero sets the destination to 0; MOD by zero leaves the destination unchanged (for ALU64) or zeroes its upper 32 bits (for ALU). There is no fault — a BPF program cannot divide-by-zero its way to a kernel crash.
  • Shifts mask the count. A 64-bit shift uses the low 6 bits of the count (& 0x3F); a 32-bit shift uses the low 5 bits (& 0x1F). This matches x86/arm64 shift semantics, so the JIT need not insert a range check.
  • Signed division/modulo (SDIV/SMOD) reuse the DIV/MOD operation codes but set off = 1 to mark the signed variant, and the spec mandates truncated division (so -13 % 3 == -1, C-style, not Python-style) (RFC 9669).
  • MOVSX (move with sign extension) reuses the MOV code with off set to 8, 16, or 32 to choose the source width, and is only defined for a register source.

For jumps, the code field encodes the comparison: JA (unconditional), JEQ/JNE, the unsigned JGT/JGE/JLT/JLE, the signed JSGT/JSGE/JSLT/JSLE, the bit-test JSET, and the control-flow ops CALL and EXIT. A conditional jump increments the program counter by off 64-bit instructions if the comparison holds, falling through otherwise — note eBPF replaced cBPF’s two-target if (cond) jt; else jf; form with single-target jump-or-fall-through (classic_vs_extended.rst). The CALL op is overloaded by src_reg: src_reg = 0 calls a helper by its static numeric ID in imm; src_reg = 1 is a program-local BPF-to-BPF call (a relative jump-and-link with the offset in imm); src_reg = 2 calls a helper by BTF ID — the mechanism behind kfuncs (RFC 9669 §Helper functions). EXIT returns, with the program’s result already placed in R0.

Byte-Swap Operations

The END operation (a code value within the ALU/ALU64 classes) byte-swaps the destination register; the imm field selects the width (16, 32, or 64 bits). It exists because BPF programs constantly parse network headers, which are big-endian (“network byte order”), on hosts that are usually little-endian. There are two forms (RFC 9669 §Byte swap instructions):

  • In the ALU class, the source bit selects a direction-aware conversion: LE converts between host order and little-endian, BE between host order and big-endian. On a little-endian host, {END, LE, ALU} is therefore a no-op and {END, BE, ALU} is a real swap — this is exactly the bpf_htons/bpf_ntohs-style helper macros in BPF C.
  • In the ALU64 class, the source bit is reserved (must be 0) and the op is an unconditional byte swap (bswap16/bswap32/bswap64) regardless of host endianness.

The unconditional ALU64 bswap form is newer than the original LE/BE ops — it is part of the “v4” instruction-set additions of kernel 6.6 (see Versioning below) (pchaigno, eBPF Instruction Set Extensions).

Loads, Stores, and the mode · size · class Opcode

For LD, LDX, ST, and STX, the opcode splits into 3 mode bits, 2 size bits, and 3 class bits (RFC 9669 §Load and store instructions):

+-+-+-+-+-+-+-+-+
|mode |sz |class|
+-+-+-+-+-+-+-+-+

The size is W (word, 4 bytes), H (half-word, 2 bytes), B (byte), or DW (double word, 8 bytes — eBPF-only). The mode selects the access flavor: MEM is a regular load/store; MEMSX is a sign-extending load (introduced in v4 / 6.6); IMM is the 64-bit-immediate load (the wide instruction, below); ATOMIC is an atomic memory operation; and ABS/IND are the deprecated legacy packet-access modes carried over from classic BPF. A regular store of a register is {MEM, <size>, STX}*(size *)(dst + off) = src; a sign-extending load is {MEMSX, <size>, LDX}dst = *(signed size *)(src + off).

The Wide (16-byte) Instruction for 64-bit Immediates

The imm field is only 32 bits, so a single basic instruction cannot carry a full 64-bit constant — and BPF programs need 64-bit constants constantly (pointers, large flags, and especially map file descriptors that get rewritten into 64-bit map addresses). The solution is the wide instruction: a basic {IMM, DW, LD} instruction immediately followed by a second 64-bit pseudo-instruction whose opcode, dst_reg, src_reg, and off are all zero, and whose own imm field holds the upper 32 bits of the constant (RFC 9669 §64-bit immediate instructions). The two 32-bit halves are combined as dst = (next_imm << 32) | imm. The kernel’s macro makes the encoding concrete (include/linux/filter.h, v6.12):

#define BPF_LD_IMM64_RAW(DST, SRC, IMM)                 \
        ((struct bpf_insn) {                            \
                .code  = BPF_LD | BPF_DW | BPF_IMM,     \
                .dst_reg = DST,                         \
                .src_reg = SRC,                         \
                .off   = 0,                             \
                .imm   = (__u32) (IMM) }),              \
        ((struct bpf_insn) {                            \
                .code  = 0, /* zero is reserved opcode */ \
                .dst_reg = 0,                           \
                .src_reg = 0,                           \
                .off   = 0,                             \
                .imm   = ((__u64) (IMM)) >> 32 })

The first bpf_insn carries the low 32 bits in imm with the real opcode BPF_LD | BPF_DW | BPF_IMM; the second carries the high 32 bits in its imm with code == 0 (the reserved/null opcode). This is the only 16-byte instruction in BPF, and it is why a disassembler that naively reads 8 bytes at a time will choke on the trailing pseudo-instruction — it must recognize LD_IMM64 and consume two slots (a fact that matters for BPF Bytecode and Disassembly).

The src_reg field of the wide instruction is repurposed as an opcode subtype selecting what kind of 64-bit value to materialize (RFC 9669 §64-bit immediate instructions): 0 is a plain integer; BPF_PSEUDO_MAP_FD (1) means “the imm is a map file descriptor — resolve it to the map’s address”; other subtypes resolve a map value address, a variable address, or a code address. This is the mechanism by which a verified program ends up holding a real kernel map pointer without the program ever computing one: the loader writes the fd, and the kernel rewrites the wide instruction to the map’s in-kernel address at load time. The macro BPF_LD_MAP_FD(DST, MAP_FD) is exactly BPF_LD_IMM64_RAW(DST, BPF_PSEUDO_MAP_FD, MAP_FD) (filter.h).

Atomic Operations

Atomic memory operations are encoded as stores with the ATOMIC mode, in either 32-bit ({ATOMIC, W, STX}) or 64-bit ({ATOMIC, DW, STX}) width — 8- and 16-bit atomics are not supported. The kind of atomic op lives in the imm field (RFC 9669 §Atomic operations):

  • Simple ops reuse a subset of the arithmetic codes: ADD (0x00), OR (0x40), AND (0x50), XOR (0xa0). {ATOMIC, W, STX} with imm = ADD means *(u32 *)(dst + off) += src, atomically.
  • A FETCH modifier (0x01) OR-ed into the imm makes the op return the old value (it overwrites src with the pre-modification memory value), turning e.g. atomic_add into atomic_fetch_add.
  • Two complex ops: XCHG (0xe0 | FETCH) atomically exchanges src with memory; CMPXCHG (0xf0 | FETCH) atomically compares memory against R0 and, if equal, stores src — in either case the pre-operation memory value is zero-extended into R0.

This generalized atomic family — fetch variants, and/or/xor, xchg, cmpxchg — replaced the original eBPF, which had only a single atomic-add op (the legacy XADD). The generalized atomics landed in kernel 5.12 along with the x86-64 JIT support; the original motivation was generating globally-unique cookies in BPF programs, but they are broadly useful for in-kernel counters and lock-free structures (LWN, Atomics for eBPF; ebpf.io updates #3).

Versioning the Instruction Set

The ISA spec presents the instruction set as a flat, timeless whole — but the kernel grew it incrementally, and BPF feature-version discipline matters because a program built for a newer ISA will be rejected (or mis-JITed) on a kernel too old to understand it. The major milestones, all present in the 6.12 LTS kernel but introduced earlier:

  • 32-bit ALU subregisters / ALU32 (-mattr=+alu32) — Linux 5.0 (pchaigno).
  • JMP32 class (32-bit-operand jumps) — Linux 5.1 (pchaigno).
  • Generalized atomics (fetch / and / or / xor / xchg / cmpxchg) — Linux 5.12 (LWN).
  • The “v4” set — sign-extending loads (LDX MEMSX), MOVSX, signed SDIV/SMOD, unconditional BSWAP, and the 32-bit unconditional jump gotol ({JA, K, JMP32} using imm for a 32-bit branch offset) — Linux 6.6 (pchaigno).

Tooling reflects this: Clang/LLVM accept -mcpu=v1/v2/v3/v4 to bound which instructions the compiler may emit, so you can target an older kernel deliberately. To resolve a “v4 only” mismatch at runtime, you compile for the lowest CPU your fleet runs.

Uncertain

Verify: the precise kernel versions for ALU32 (5.0), JMP32 (5.1), generalized atomics (5.12), and the v4 set (6.6). Reason: the version-to-feature mapping comes from a secondary source (pchaigno’s blog) corroborated by LWN for the atomics; the presence of every one of these instructions is confirmed against the v6.12 ISA doc and headers, but the introduction versions were not each cross-checked against the merge commit. To resolve: confirm each against the bpf-next merge commits / git log for BPF_MOVSX, BPF_JMP32, and BPF_ATOMIC introduction. #uncertain

The IETF Standardization Effort (RFC 9669)

For most of its life the BPF ISA was defined only by the kernel implementation and a Documentation/bpf/ text file. That changed: the kernel’s Documentation/bpf/standardization/instruction-set.rst became the working draft of an IETF specification, and on 2024-10, after roughly two years of work in the IETF BPF Working Group, it was published as RFC 9669, “BPF Instruction Set Architecture (ISA)” — an Internet Standards Track document (RFC 9669; datatracker BPF WG). The kernel doc at v6.12 explicitly points at the working group (“documents that are being iterated on as part of the BPF standardization effort with the IETF”).

Why standardize at all? eBPF is no longer a Linux-only technology — there are independent runtimes (Windows’ eBPF-for-Windows, userspace VMs, hardware offload targets). A vendor-neutral spec lets a compiler emit BPF for any conforming runtime and lets a runtime advertise exactly what it supports. The mechanism for that is conformance groups: instead of “support everything,” a runtime declares which named groups it implements — base32 (the mandatory core), base64 (adds 64-bit ops), atomic32/atomic64, divmul32/divmul64, and the deprecated packet group (RFC 9669 §Conformance groups). A concrete driver named in the LWN write-up: certain NVMe vendors wanted to build BPF offload (e.g. eXpress Resubmission Path) but could not fund it without a stable, standardized instruction set to target (LWN, RFC 9669). The Linux kernel remains the reference implementation, but the encoding in this note is now portable, standardized, and frozen at the RFC level.

Common Misunderstandings

“Every instruction is 8 bytes.” Almost — but the 64-bit-immediate LD_IMM64 is 16 bytes (two slots). A disassembler must special-case it; reading the second slot as a standalone instruction yields the reserved opcode 0 and garbage.

imm can hold a 64-bit constant.” No — imm is 32 bits. Sixty-four-bit constants require the wide instruction, which is precisely why it exists.

“BPF has a tail_call opcode.” No. bpf_tail_call is a helper, invoked via the ordinary CALL instruction with a helper ID; the kernel marks it internally with the non-ISA value BPF_TAIL_CALL (0xf0) in filter.h, but that is an implementation detail, not an instruction-set opcode. See BPF Tail Calls.

“The legacy ABS/IND packet-load instructions are still recommended.” They are deprecated — the ISA spec says they “SHOULD no longer be used” and places them in the optional packet conformance group (RFC 9669). Modern programs use direct packet access via the context pointer instead.

See Also