How to Run a Target Program with bpftime Attached: CLI, Injection, and Execution Flow
You run a target program with bpftime attached by first loading the eBPF object into shared memory with bpftime load, then launching the target under the agent's control via bpftime start, which injects the syscall-server and agent libraries using LD_PRELOAD to intercept probe points and execute eBPF bytecode in user space.
bpftime is a high-performance userspace eBPF runtime that enables attaching eBPF programs to unmodified target binaries without kernel modifications. Understanding how to run a target program with bpftime attached requires knowledge of its shared-memory architecture, library injection mechanism, and the CLI workflow implemented in the eunomia-bpf/bpftime repository.
The bpftime Execution Architecture
bpftime attaches to targets through a coordinated three-layer injection system. When you run a target program with bpftime attached, you are actually spawning a managed process where eBPF programs execute natively in userspace.
The Three Core Components
The runtime consists of tightly-coupled libraries that handle different stages of the eBPF lifecycle:
- CLI Driver – Parses sub-commands in
tools/cli/main.cpp(lines 62–99) and prepares theLD_PRELOADenvironment strings necessary for injection. - Syscall-Server Library (
runtime/syscall-server/libbpftime-syscall-server.so) – The first injected library loaded viaLD_PRELOAD. It initializes the Boost-interprocess shared memory region viabpftime_initialize_global_shm(defined inruntime/include/bpftime_shm.hpp), registers eBPF handlers, and forwards intercepted syscalls to the bpftime VM. - Agent Library (
runtime/agent/libbpftime-agent.so) – The second injected library that rewrites the target binary's text segment, registers per-process helpers, and executes the loaded eBPF program within the target's address space.
The Two-Phase Workflow
Running a target with bpftime attached follows a strict load-then-execute pattern:
- Load Phase: The eBPF object is registered in the shared-memory manager using
bpftime load, which populates the handler registry inruntime/src/handler/handler_manager.hpp. - Run Phase: The CLI forks and execs the target with modified environment variables that inject the runtime libraries, enabling immediate eBPF execution at probe points.
How to Run a Target Program with bpftime Attached
Step 1: Load the eBPF Object into Shared Memory
Before running any target, you must load the compiled eBPF program into bpftime's global shared memory. This step registers the program IDs and map definitions that the agent will later reference.
bpftime load ./example/malloc/malloc
According to the source code in tools/cli/main.cpp, this command initializes the Boost-interprocess shared memory region that allows communication between the CLI, syscall-server, and agent components. The handler_manager stores the eBPF program metadata for later retrieval by the agent.
Step 2: Start the Target with Library Injection
Use the bpftime start command to launch your target program with the eBPF runtime attached. This command builds the injection environment and executes the target via run_command() (lines 78–99 of tools/cli/main.cpp).
bpftime start ./example/malloc/victim
The CLI constructs the environment strings exactly as implemented in the source:
std::string ld_preload_str("LD_PRELOAD=");
ld_preload_str += ld_preload; // e.g., ~/.bpftime/libbpftime-agent.so
std::string agent_so_str("AGENT_SO=");
agent_so_str += agent_so; // optional transformer for syscall tracing
The syscall-server becomes the first library in LD_PRELOAD, spawning the agent (or the transformer plus agent when using -s/--enable-syscall-trace). When the target hits a uprobe or syscall entry point, the agent executes the corresponding eBPF bytecode from the shared memory region.
Step 3: Attach to a Running Process (Alternative)
To run a target program with bpftime attached without restarting it, use Frida-based injection. This requires root privileges because it injects into an existing process.
sudo bpftime attach 6789
The inject_by_frida() function (lines 40–61 of main.cpp) calls frida_injector_inject_library_file_sync to load the agent library into the target PID. The output confirms successful attachment: "Injecting to 6789 … Successfully injected. ID: 1".
Understanding the Injection and Execution Mechanism
LD_PRELOAD and AGENT_SO Environment Variables
When you run a target program with bpftime attached via bpftime start, the CLI forks a child process that builds specific environment strings before calling execvpe. The syscall-server library must appear first in LD_PRELOAD to initialize the shared memory region before the agent loads.
For syscall tracing scenarios, the CLI additionally sets AGENT_SO to load libbpftime-agent-transformer.so alongside the base agent. This is triggered by the -s flag:
bpftime start -s ./example/opensnoop/victim
Binary Rewriting and Text Segment Transformation
Once injected, the agent library rewrites the target program's text segment using the technique described in attach/text_segment_transformer/README.md. This rewriting redirects execution flow at probe points (uprobes, syscall entries) to the eBPF VM running in userspace.
Shared Memory Communication
All components communicate through a Boost-interprocess shared memory region created by bpftime_initialize_global_shm (see runtime/include/bpftime_shm.hpp). This region stores:
- eBPF program IDs and bytecode
- Map definitions and shared data
- Link registrations between probe points and programs
Practical Code Examples
Example 1: Running a malloc Tracer from Scratch
# Build the example eBPF program
make -C example/malloc
# Load the eBPF object into bpftime's shared memory
bpftime load ./example/malloc/malloc
# Run the target program with the eBPF program attached
bpftime start ./example/malloc/victim
# Output:
# Hello malloc!
# malloc called from pid 12345
# continue malloc...
Example 2: Attaching to a Running Process
# Launch the target in the background and capture its PID
./example/malloc/victim &
PID=$!
# Inject the agent using Frida (requires root)
sudo bpftime attach $PID
# → "Injecting to 6789 … Successfully injected. ID: 1"
Example 3: Enabling Syscall Tracing
# The -s flag loads both the transformer and agent libraries
bpftime start -s ./example/opensnoop/victim
Key Source Files and Their Roles
| File | Purpose |
|---|---|
tools/cli/main.cpp |
Command-line front-end implementing run_command() (lines 78–99) and inject_by_frida() (lines 40–61) for environment preparation and process injection. |
runtime/include/bpftime_shm.hpp |
Defines the shared-memory region and bpftime_initialize_global_shm for inter-process map and program sharing. |
runtime/src/handler/handler_manager.hpp |
Central registry for eBPF objects (programs, maps, links) loaded during the bpftime load phase. |
runtime/src/syscall-server/ |
Implementation of the preload library that creates shared memory and forwards syscalls to the VM. |
runtime/src/agent/ |
Contains the in-process agent that rewrites binary text segments and executes eBPF programs. |
attach/text_segment_transformer/README.md |
Documents the binary-rewriting technique used for uprobe and syscall interception. |
Summary
- bpftime uses a two-phase workflow: First
bpftime loadregisters the eBPF object in shared memory, thenbpftime startorbpftime attachinjects the runtime. - Injection relies on LD_PRELOAD: The CLI constructs environment strings in
main.cpp(lines 78–99) to preload the syscall-server and agent libraries. - Three components coordinate execution: The CLI driver, syscall-server library, and agent library work together to rewrite the target binary and execute eBPF bytecode in userspace.
- Frida enables live attachment: The
bpftime attachcommand usesfrida_injector_inject_library_file_syncto inject the agent into running processes without restart. - Shared memory enables state sharing: Boost-interprocess shared memory (defined in
bpftime_shm.hpp) allows eBPF maps and programs to persist across the CLI and target process boundaries.
Frequently Asked Questions
What is the difference between bpftime start and bpftime attach?
bpftime start launches a new process with the agent preloaded via LD_PRELOAD before execve executes the target binary. bpftime attach uses Frida to inject the agent library into an already-running process identified by PID, which requires root privileges and calls frida_injector_inject_library_file_sync as implemented in tools/cli/main.cpp lines 40–61.
Why does bpftime use LD_PRELOAD for injection?
bpftime uses LD_PRELOAD to ensure the syscall-server library initializes before the target program's main function executes. This allows the runtime to establish the Boost-interprocess shared memory region and register syscall handlers before the target begins execution, ensuring immediate interception of probe points.
How does bpftime share eBPF maps between processes?
All bpftime components communicate through a Boost-interprocess shared memory region created by bpftime_initialize_global_shm and defined in runtime/include/bpftime_shm.hpp. When you run bpftime load, the CLI registers maps and programs in this region; the agent then accesses these structures when executing eBPF bytecode in the target's address space.
Can I attach bpftime to a process without restarting it?
Yes. Use bpftime attach <pid> to inject the agent into a running process. This method uses the Frida framework to call frida_injector_inject_library_file_sync, loading libbpftime-agent.so into the target's memory without requiring a process restart. Note that this operation typically requires root privileges or appropriate capabilities.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →