Architecture Overview¶
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.
🏗️ eBPF Application Architecture¶
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
🧩 Components Breakdown¶
1. Kernel Space Components¶
eBPF Program (bpf/*.c)¶
- 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;
}
eBPF Maps (SEC(".maps"))¶
- 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");
2. Userspace Components¶
Go CLI Application (cmd/*.go)¶
- Purpose: User interface and program orchestration
- Framework: Cobra CLI for command-line interface
- Responsibilities: Argument parsing, program lifecycle management
eBPF Loader¶
- Purpose: Load eBPF programs into the kernel
- Library: Cilium eBPF
- Tasks: Program verification, map creation, attachment
Ring Buffer Reader¶
- Purpose: Receive events from kernel
- Mechanism: Zero-copy ring buffer for high performance
- Processing: Event deserialization and filtering
📁 Project Structure Deep Dive¶
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
File Relationships¶
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
🔄 Data Flow¶
Understanding how data flows through an eBPF application is crucial:
1. Event Trigger¶
2. Data Capture¶
// 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);
3. Data Transfer¶
4. Data Processing¶
// In userspace (execsnoop.go)
record, err := rd.Read()
var data exec_data_t
binary.Read(bytes.NewReader(record.RawSample), binary.LittleEndian, &data)
5. Data Display¶
🔗 Component Interaction¶
Compilation Flow¶
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
Runtime Flow¶
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
🎯 Design Patterns¶
1. Event-Driven Architecture¶
- eBPF programs are reactive - they respond to kernel events
- No polling or active monitoring from userspace
- Efficient and low-overhead
2. Producer-Consumer Pattern¶
- Producer: eBPF program generates events in kernel
- Consumer: Go application processes events in userspace
- Buffer: Ring buffer decouples producer from consumer
3. Code Generation¶
bpf2gotool generates Go bindings from C code- Ensures type safety between C structs and Go structs
- Embeds eBPF bytecode in Go binary
🛠️ Build Process¶
Understanding the build process helps with debugging and customization:
1. Code Generation¶
This generates:
- execsnoop_bpfel.go - eBPF program loader
- execsnoop_bpfel.o - eBPF bytecode
2. Compilation Steps¶
3. Generated Code Example¶
// 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
}
🔍 Key Concepts to Remember¶
Architecture Principles
- Separation of Concerns: Kernel space handles data collection, userspace handles presentation
- Type Safety: Shared data structures must match between C and Go
- Resource Management: Always clean up eBPF resources (programs, maps, links)
- Error Handling: eBPF operations can fail - handle errors gracefully
Limitations to Consider
- eBPF Program Size: Limited to ~1M instructions
- Stack Space: Only 512 bytes of stack in eBPF programs
- No Loops: eBPF verifier prevents unbounded loops
- Helper Functions: Only approved kernel helpers can be called
📚 What's Next?¶
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! 🚀