Before we build your first eBPF tool, let's understand how eBPF applications are structured. This knowledge will serve as the foundation for everything you'll build.
Every eBPF application in this project follows the same architectural pattern:
graph TB
subgraph "Userspace Application"
A[Go CLI Application] --> B[eBPF Loader]
B --> C[Ring Buffer Reader]
C --> D[Event Processor]
D --> E[Output Formatter]
end
subgraph "Kernel Space"
F[eBPF Program] --> G[eBPF Maps]
H[Kernel Events] --> F
end
G --> C
B --> F
style A fill:#e8f5e8
style F fill:#f3e5f5
style G fill:#fff3e0
- Purpose: Runs in kernel space, triggered by events
- Language: C with eBPF-specific extensions
- Limitations: Limited stack space, no loops, must be verifiable
SEC("tracepoint/sched/sched_process_exec")
int trace_exec(struct trace_event_raw_sched_process_exec *ctx) {
// This function runs in kernel space!
// It has access to kernel data structures
return 0;
}- Purpose: Data exchange between kernel and userspace
- Types: Ring buffers, hash maps, arrays, etc.
- Shared: Both kernel and userspace can access
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 1 << 24); // 16MB buffer
} events SEC(".maps");- Purpose: User interface and program orchestration
- Framework: Cobra CLI for command-line interface
- Responsibilities: Argument parsing, program lifecycle management
- Purpose: Load eBPF programs into the kernel
- Library: Cilium eBPF
- Tasks: Program verification, map creation, attachment
- Purpose: Receive events from kernel
- Mechanism: Zero-copy ring buffer for high performance
- Processing: Event deserialization and filtering
ebee/
βββ bpf/ # eBPF kernel programs
β βββ execsnoop.c # Process monitoring eBPF program
β βββ rmdetect.c # File deletion eBPF program
β βββ headers/ # Kernel headers
β βββ vmlinux.h # Kernel type definitions
βββ cmd/ # Go userspace applications
β βββ execsnoop.go # Process monitoring CLI
β βββ rmdetect.go # File deletion CLI
β βββ root.go # CLI root command
βββ main.go # Application entry point
βββ Makefile # Build automation
graph LR
A[bpf/execsnoop.c] --> B[execsnoopObjects]
B --> C[cmd/execsnoop.go]
C --> D[main.go]
E[vmlinux.h] --> A
F[Makefile] --> B
style A fill:#f3e5f5
style C fill:#e8f5e8
style E fill:#fff3e0
Understanding how data flows through an eBPF application is crucial:
Kernel Event (e.g., process execution) β eBPF Program Triggered
// In kernel space (execsnoop.c)
struct data_t *data = bpf_ringbuf_reserve(&events, sizeof(*data), 0);
data->pid = bpf_get_current_pid_tgid() & 0xFFFFFFFF;
bpf_get_current_comm(&data->comm, sizeof(data->comm));
bpf_ringbuf_submit(data, 0);Ring Buffer: Kernel Space β Userspace (zero-copy)
// In userspace (execsnoop.go)
record, err := rd.Read()
var data exec_data_t
binary.Read(bytes.NewReader(record.RawSample), binary.LittleEndian, &data)fmt.Printf("%d\t%s\n", data.Pid, string(data.Comm[:]))sequenceDiagram
participant M as Makefile
participant C as eBPF C Code
participant G as Go Code
participant O as Objects
M->>C: bpf2go generates Go bindings
C->>O: Compiles to eBPF bytecode
O->>G: Embeds bytecode in Go
G->>G: Compiles to executable
sequenceDiagram
participant U as User
participant G as Go App
participant K as Kernel
participant E as eBPF Program
U->>G: ./ebee execsnoop
G->>K: Load eBPF program
K->>K: Verify program safety
G->>K: Attach to tracepoint
K->>E: Event triggers eBPF
E->>G: Send data via ring buffer
G->>U: Display formatted output
- eBPF programs are reactive - they respond to kernel events
- No polling or active monitoring from userspace
- Efficient and low-overhead
- Producer: eBPF program generates events in kernel
- Consumer: Go application processes events in userspace
- Buffer: Ring buffer decouples producer from consumer
bpf2gotool generates Go bindings from C code- Ensures type safety between C structs and Go structs
- Embeds eBPF bytecode in Go binary
Understanding the build process helps with debugging and customization:
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target native execsnoop ../bpf/execsnoop.cThis generates:
execsnoop_bpfel.go- eBPF program loaderexecsnoop_bpfel.o- eBPF bytecode
make generate # Generate Go bindings from C
make build # Compile Go application// Auto-generated by bpf2go
type execsnoopObjects struct {
TraceExec *ebpf.Program `ebpf:"trace_exec"`
Events *ebpf.Map `ebpf:"events"`
}
func loadExecsnoopObjects(obj *execsnoopObjects, opts *ebpf.CollectionOptions) error {
// Load eBPF program and maps
}!!! tip "Architecture Principles" 1. Separation of Concerns: Kernel space handles data collection, userspace handles presentation 2. Type Safety: Shared data structures must match between C and Go 3. Resource Management: Always clean up eBPF resources (programs, maps, links) 4. Error Handling: eBPF operations can fail - handle errors gracefully
!!! warning "Limitations to Consider" 1. eBPF Program Size: Limited to ~1M instructions 2. Stack Space: Only 512 bytes of stack in eBPF programs 3. No Loops: eBPF verifier prevents unbounded loops 4. Helper Functions: Only approved kernel helpers can be called
Now that you understand the architecture, let's build your first tool step by step:
- Writing eBPF C Code - Create the kernel component
- Go Integration - Build the userspace application
- Build and Test - Put it all together
Understanding this architecture will make the following sections much easier to follow. Every tool in this project follows these same patterns - master them once, and you can build any eBPF tool! π