Understanding Rust's bpfel-unknown-none Target: A Deep Dive into eBPF Compilation
When compiling Rust code for eBPF (extended Berkeley Packet Filter), you encounter one of Rust's most unusual compilation targets: bpfel-unknown-none. This target produces bytecode that runs inside the Linux kernel's eBPF virtual machine, enabling powerful kernel-level instrumentation, networking, and security tools.
This guide explains what makes this target unique, why it exists, and how to use it effectively.
What is bpfel-unknown-none?
The target triple breaks down as:
- bpf - BPF bytecode format (runs in Linux kernel's eBPF VM)
- el - Little-endian byte order (
eb= big-endian) - unknown - No specific operating system (kernel bytecode, not userspace)
- none - No standard library - bare metal environment
There are two variants:
bpfel-unknown-none- Little-endian (most common, used on x86_64)bpfeb-unknown-none- Big-endian (used on some embedded systems)
The Target Specification
You can inspect the full target specification with:
rustc +nightly -Z unstable-options --print target-spec-json --target bpfel-unknown-none{
"arch": "bpf",
"atomic-cas": false,
"data-layout": "e-m:e-p:64:64-i64:64-i128:128-n32:64-S128",
"dynamic-linking": true,
"linker-flavor": "bpf",
"llvm-target": "bpfel",
"max-atomic-width": 64,
"min-atomic-width": 64,
"no-builtins": true,
"obj-is-bitcode": true,
"panic-strategy": "abort",
"singlethread": true,
"target-pointer-width": 64,
"metadata": {
"description": "BPF (little endian)",
"tier": 3,
"std": false
}
}Key Properties Explained
obj-is-bitcode: true - This is the most unusual property. Unlike normal targets that output object files, this target outputs LLVM bitcode (.bc files). The actual eBPF bytecode generation happens during linking via bpf-linker. This approach allows link-time optimizations to eliminate unsupported Rust constructs before code generation.
atomic-cas: false - eBPF doesn't support compare-and-swap atomics. Only simple 64-bit atomic loads and stores are available.
panic-strategy: abort - There's no unwinding in kernel space. Panics must abort immediately.
singlethread: true - eBPF programs run in a single-threaded context (per-CPU).
Why eBPF is Different
The eBPF virtual machine has severe constraints that make it unlike any other Rust target:
1. Formal Verification
Before your eBPF program runs, the Linux kernel's verifier statically proves that your program:
- Will terminate (no infinite loops)
- Won't access invalid memory
- Won't crash the kernel
- Follows the eBPF calling convention
This is actual formal verification, not just type checking.
2. Resource Limits
- Stack size: 512 bytes (256 with tail calls)
- Heap: None (use BPF maps instead)
- Instructions: ~1 million (kernel version dependent)
- Nested calls: 8 levels
- Registers: 11 (r0-r10)
3. No Standard Library
eBPF programs use #![no_std] and #![no_main]. You only have access to core - no heap allocation, no I/O, no threads.
4. Register-Based Calling Convention
- Arguments:
r1throughr5(max 5 arguments) - Return value:
r0 - Frame pointer:
r10(read-only) - Kernel helpers invoked via
call [helper_id]
The Compilation Pipeline
βββββββββββββββββββ
β Rust Source β
β #![no_std] β
ββββββββββ¬βββββββββ
β rustc --target bpfel-unknown-none
βΌ
βββββββββββββββββββ
β LLVM Bitcode β β Note: NOT object code!
β (.bc) β
ββββββββββ¬βββββββββ
β bpf-linker (LTO + codegen)
βΌ
βββββββββββββββββββ
β eBPF Object β
β (.o) β
ββββββββββ¬βββββββββ
β loader (aya, libbpf, etc.)
βΌ
βββββββββββββββββββ
β Kernel eBPF VM β
β (after verify) β
βββββββββββββββββββWhy Bitcode First?
The bpf-linker defers code generation until after link-time optimization. This is crucial because:
- Dead code elimination - Unsupported Rust constructs can be optimized away before code generation fails
- Cross-crate inlining - Better optimization across crate boundaries
- Constant propagation - More code can be evaluated at compile time
Build Configuration
.cargo/config.toml
[build]
target = "bpfel-unknown-none"
[unstable]
build-std = ["core"]
[target.bpfel-unknown-none]
linker = "bpf-linker"
rustflags = [
"-C", "debuginfo=2",
"-C", "link-arg=--btf",
"-C", "panic=abort",
]Cargo.toml
[package]
name = "my-ebpf-program"
version = "0.1.0"
edition = "2021"
[dependencies]
aya-ebpf = "0.1"
aya-log-ebpf = "0.1"
[profile.release]
lto = true
panic = "abort"
opt-level = 3
[profile.dev]
opt-level = 3 # eBPF needs optimization even in dev
debug = false
panic = "abort"Build Command
cargo +nightly build \
--target=bpfel-unknown-none \
-Z build-std=core \
--releaseA Minimal eBPF Program
#![no_std]
#![no_main]
use aya_ebpf::{macros::tracepoint, programs::TracePointContext};
use aya_log_ebpf::info;
#[tracepoint]
pub fn sys_enter_execve(ctx: TracePointContext) -> u32 {
match try_execve(ctx) {
Ok(()) => 0,
Err(_) => 1,
}
}
fn try_execve(ctx: TracePointContext) -> Result<(), i64> {
info!(&ctx, "execve called!");
Ok(())
}
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
}Common Pitfalls
1. Stack Overflow
With only 512 bytes of stack, large local variables cause immediate failure:
// BAD: 1024 bytes on stack
let buffer: [u8; 1024] = [0; 1024];
// GOOD: Use a BPF map
#[map]
static BUFFER: PerCpuArray<[u8; 1024]> = PerCpuArray::with_max_entries(1, 0);2. Unbounded Loops
The verifier rejects loops it can't prove terminate:
// BAD: Verifier can't prove termination
for item in collection.iter() { ... }
// GOOD: Bounded loop
for i in 0..MAX_ITEMS {
if i >= collection.len() { break; }
...
}3. Function Pointers
eBPF doesn't support indirect calls:
// BAD: Function pointer
let f: fn() = some_function;
f();
// GOOD: Direct call
some_function();Nix Integration
Building eBPF with Nix requires network access for dependencies (cargo2nix doesn't support the BPF target):
{ stdenv, fenix, bpf-linker }:
let
toolchain = fenix.complete.withComponents [
"cargo" "rustc" "rust-src" "llvm-tools"
];
in
stdenv.mkDerivation {
pname = "my-ebpf";
nativeBuildInputs = [ toolchain bpf-linker ];
buildPhase = ''
export HOME=$(mktemp -d)
cargo build --release
'';
}Note: You may need --option sandbox false for the cargo build to fetch dependencies.
BTF (BPF Type Format)
BTF embeds type information in the eBPF object file, enabling:
- CO-RE (Compile Once, Run Everywhere) portability
- Better debugging with
bpftool - Kernel structure relocation
Enable with: -C link-arg=--btf
Further Reading
- Aya Book - Official Aya documentation
- bpf-linker - The BPF static linker
- BPF Target PR #79608 - Original Rust implementation
- eBPF.io - General eBPF resources
Conclusion
The bpfel-unknown-none target represents one of Rust's most constrained compilation environments. Understanding its limitations - 512 bytes of stack, no heap, formal verification, bitcode-first compilation - is essential for writing successful eBPF programs.
The key insight is that this isn't just "Rust for a weird CPU" - it's Rust for a formally verified, kernel-resident virtual machine with unique constraints that exist nowhere else in computing.