How to Load an eBPF Program with bpftime

You load an eBPF program in bpftime by running bpftime load <program>, which injects a syscall-server library via LD_PRELOAD to intercept bpf() calls, parse the ELF bytecode, and optionally JIT-compile it for userspace execution.

The bpftime repository provides a userspace eBPF runtime that executes eBPF programs without requiring kernel support. Loading an eBPF program with bpftime involves parsing ELF bytecode into a virtual machine, loading the instruction array, and optionally compiling to native code for performance. This process can be executed through the command-line interface or directly via the C++ API.

Architecture of the bpftime Program Loader

Understanding the internal components helps diagnose loading issues and optimize performance. The architecture consists of three primary layers: the CLI parser, the syscall interception library, and the runtime VM.

CLI Parser and Syscall Interception

The entry point resides in tools/cli/main.cpp, which parses the load subcommand and prepares the execution environment. When you invoke bpftime load, the CLI builds the path to libbpftime-syscall-server.so and calls run_command(...) to launch your target binary.

The syscall-server library uses LD_PRELOAD to intercept bpf() syscalls from the target process. This interception layer creates a shared-memory manager and forwards program-load requests to the runtime, allowing unmodified eBPF programs to run in userspace.

Runtime Components and VM

The core loading logic lives in runtime/src/bpftime_prog.cpp. The bpftime_prog class encapsulates the VM state, instruction buffer, and JIT compilation status. When loading occurs:

  1. bpftime_prog::bpftime_prog_load(bool jit) validates and loads the bytecode
  2. The method calls ebpf_load from vm/vm-core/src/ebpf-vm.cpp to populate the VM
  3. If jit is true, it invokes ebpf_compile to generate native machine code

Configuration options defined in runtime/include/bpftime_config.hpp control this behavior through environment variables like BPFTIME_DISABLE_JIT and BPFTIME_RUN_WITH_KERNEL.

Loading eBPF Programs via the Command Line

The CLI workflow is the fastest way to load and test eBPF programs. The process involves building your eBPF object file, loading it into the bpftime runtime, and running the target application.

Step 1: Build the Example Program

Compile your eBPF program using the standard libbpf toolchain:

make -C example/malloc

This produces an executable containing the eBPF bytecode that bpftime will load.

Step 2: Load the Program into the Runtime

Execute the load command to inject the syscall server and prepare the runtime:

bpftime load ./example/malloc/malloc

This command sets LD_PRELOAD=~/.bpftime/libbpftime-syscall-server.so and starts your program. The runtime automatically parses the ELF sections, loads the bytecode via ebpf_load, and JIT-compiles it by default.

Step 3: Run the Target Process

Start the application you wish to instrument:

bpftime start ./example/malloc/victim

You should see output indicating the eBPF program is actively tracing events:


pid=247299  malloc calls: 10
pid=247322  malloc calls: 10

Controlling Load Behavior with Environment Variables

You can modify the loading behavior without changing code:

  • Disable JIT compilation (use interpreter mode):

    BPFTIME_DISABLE_JIT=true bpftime load ./example/malloc/malloc
  • Load into kernel for verification but run in userspace:

    BPFTIME_RUN_WITH_KERNEL=true BPFTIME_NOT_LOAD_PATTERN=start_.* bpftime load ./example/malloc/malloc

Programmatic Loading with the C++ API

For custom tooling, bypass the CLI and use the bpftime_prog class directly. This approach gives you explicit control over the load sequence and execution parameters.

#include <bpftime/runtime/bpftime_prog.hpp>
#include <fstream>
#include <vector>
#include <iostream>

int main() {
    // Read ELF object file
    std::ifstream f("example/malloc/malloc.o", std::ios::binary);
    std::vector<char> buf((std::istreambuf_iterator<char>(f)),
                         std::istreambuf_iterator<char>());

    // Initialize program object
    bpftime::bpftime_prog prog;
    prog.set_name("malloc_tracer");
    prog.load_from_elf(buf);  // Parses ELF and fills instruction buffer

    // Load into VM with JIT enabled (true) or interpreter (false)
    if (prog.bpftime_prog_load(true) != 0) {
        std::cerr << "Failed to load program\n";
        return 1;
    }

    // Execute the program with a memory context
    uint8_t mem[4096] = {};
    uint64_t ret = 0;
    prog.bpftime_prog_exec(mem, sizeof(mem), &ret);
    std::cout << "Program returned " << ret << "\n";
    
    return 0;
}

Key methods in runtime/src/bpftime_prog.cpp:

  • bpftime_prog_load(bool jit): Loads bytecode into the VM via ebpf_load and optionally calls ebpf_compile for JIT.
  • bpftime_prog_exec(void *memory, size_t size, uint64_t *ret): Executes the loaded program against the provided memory buffer.

Summary

  • bpftime provides a userspace eBPF runtime that loads programs via LD_PRELOAD interception, eliminating kernel dependencies.
  • The bpftime load command injects libbpftime-syscall-server.so to intercept bpf() syscalls and manages the ELF parsing and VM initialization.
  • Environment variables (BPFTIME_DISABLE_JIT, BPFTIME_RUN_WITH_KERNEL) control JIT compilation and kernel verification without code changes.
  • The bpftime_prog class in runtime/src/bpftime_prog.cpp exposes bpftime_prog_load() and bpftime_prog_exec() for programmatic control.
  • The VM core in vm/vm-core/src/ebpf-vm.cpp provides the low-level ebpf_load and ebpf_compile primitives used during the loading sequence.

Frequently Asked Questions

What is the difference between bpftime load and bpftime start?

bpftime load injects the syscall-server library into a target process and prepares the eBPF runtime, but the program executes immediately. bpftime start is typically used to launch a separate victim process that the loaded eBPF program will trace or instrument. According to the source in tools/cli/main.cpp, both commands set up the LD_PRELOAD environment, but load expects the eBPF program itself as the argument, while start expects the target workload.

How do I disable JIT compilation when loading a program?

Set the BPFTIME_DISABLE_JIT=true environment variable before running the load command. This forces the runtime to use the interpreter mode in ebpf_load rather than calling ebpf_compile, which is useful for debugging or when running on architectures without JIT support. You can verify this behavior in runtime/include/bpftime_config.hpp where the configuration flags are defined.

Can I load eBPF programs compiled for the kernel into bpftime?

Yes. bpftime is designed to run standard eBPF ELF objects compiled with libbpf or clang. The load_from_elf() method in the C++ API parses the standard ELF sections (.text, maps, etc.) and loads them into the userspace VM. When using BPFTIME_RUN_WITH_KERNEL=true, the runtime can also load the program into the kernel for verification purposes while still executing it in userspace.

What file formats does bpftime support for loading programs?

bpftime primarily supports ELF object files (.o) containing eBPF bytecode, which is the standard output from the eBPF LLVM backend or libbpf. The bpftime_prog::load_from_elf() method handles the parsing. Raw bytecode arrays can also be loaded directly via the ebpf_load function in vm/vm-core/src/ebpf-vm.cpp if you are implementing a custom loader without ELF parsing.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →