Writing eBPF C Code¶
Let's create your first eBPF program from scratch! We'll build a simple file operation monitor that tracks when files are opened.
๐ฏ What We're Building¶
A tool called fileopen that monitors file open operations and reports:
- Process ID that opened the file
- Process name
- Filename that was opened
๐ Step 1: Create the eBPF Program¶
Create bpf/fileopen.c:
#include "common.h"
// Data structure for events sent to userspace
struct file_event {
u32 pid; // Process ID
char comm[16]; // Process name (limited to 16 chars in kernel)
char filename[256]; // Filename (truncated if longer)
};
// Ring buffer for sending events to userspace
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 1 << 24); // 16MB buffer
} events SEC(".maps");
// eBPF program attached to file open operations
SEC("tracepoint/syscalls/sys_enter_openat")
int trace_file_open(struct trace_event_raw_sys_enter *ctx) {
// Reserve space in the ring buffer
struct file_event *event = bpf_ringbuf_reserve(&events, sizeof(*event), 0);
if (!event) {
return 0; // Skip if no space available
}
// Get process information
event->pid = bpf_get_current_pid_tgid() & 0xFFFFFFFF;
bpf_get_current_comm(&event->comm, sizeof(event->comm));
// Get filename from system call arguments
// ctx->args[1] contains the filename pointer
bpf_probe_read_user_str(&event->filename, sizeof(event->filename),
(void *)ctx->args[1]);
// Submit the event to userspace
bpf_ringbuf_submit(event, 0);
return 0;
}
char _license[] SEC("license") = "GPL";
๐ Code Breakdown¶
Let's understand each part of this eBPF program:
Data Structure¶
struct file_event {
u32 pid; // Process ID
char comm[16]; // Process name
char filename[256]; // Filename
};
Why these sizes?
commis limited to 16 characters in the Linux kernelfilenameis limited to prevent the eBPF program from being too largeu32for PID is sufficient (PIDs are 32-bit values)
Ring Buffer Map¶
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 1 << 24); // 16MB
} events SEC(".maps");
Ring Buffer Benefits
- Zero-copy: Efficient data transfer to userspace
- Lock-free: High performance under load
- Overflow handling: Old events are discarded if buffer is full
Program Attachment Point¶
Tracepoint Choice
sys_enter_openatcatches most file opens in modern Linux- Tracepoints are stable interfaces (unlike kprobes)
sys_enter_gives us access to system call arguments
Helper Functions Used¶
bpf_get_current_pid_tgid()¶
- Returns 64-bit value: upper 32 bits = thread group ID, lower 32 bits = process ID
- We mask to get just the PID
bpf_get_current_comm()¶
- Safely copies process name to our buffer
- Automatically null-terminates the string
bpf_probe_read_user_str()¶
- Safely reads string from userspace memory
- Handles page faults and invalid pointers
- Null-terminates the result
๐ก๏ธ Safety Considerations¶
eBPF programs must be verifiably safe. Here's how our program ensures safety:
1. Bounds Checking¶
// โ
Good: Size is specified and bounded
bpf_get_current_comm(&event->comm, sizeof(event->comm));
// โ Bad: Unbounded copy
// strcpy(event->comm, current->comm); // This won't compile
2. Null Pointer Checking¶
struct file_event *event = bpf_ringbuf_reserve(&events, sizeof(*event), 0);
if (!event) {
return 0; // Handle allocation failure
}
3. Safe Memory Access¶
// โ
Good: Safe helper function
bpf_probe_read_user_str(&event->filename, sizeof(event->filename), ptr);
// โ Bad: Direct memory access
// strcpy(event->filename, (char *)ptr); // Verifier will reject
๐งช Testing Your eBPF Program¶
Before moving to the Go code, let's verify our eBPF program compiles:
# Compile the eBPF program
clang -O2 -target bpf -c bpf/fileopen.c -o bpf/fileopen.o -I bpf/headers
# Check if it compiled successfully
file bpf/fileopen.o
# Should show: ELF 64-bit LSB relocatable, eBPF
Common Compilation Errors¶
Unknown helper function
๐ Alternative Attachment Points¶
Our program uses sys_enter_openat, but there are other options:
System Call Tracepoints¶
// Monitor different system calls
SEC("tracepoint/syscalls/sys_enter_open") // Older open() syscall
SEC("tracepoint/syscalls/sys_enter_openat") // Modern openat() syscall
SEC("tracepoint/syscalls/sys_exit_openat") // Exit point (has return value)
Kernel Function Tracepoints¶
Kprobes (Advanced)¶
// Attach to any kernel function (less stable)
SEC("kprobe/do_sys_openat2") // Internal kernel function
Choosing Attachment Points
- Tracepoints: Stable, well-documented, recommended for beginners
- Kprobes: Flexible but can break between kernel versions
- System call entry: Good for monitoring user actions
- System call exit: Good when you need return values
๐ Learning Exercises¶
Try modifying the program to learn more:
Exercise 1: Add Timestamps¶
struct file_event {
u32 pid;
char comm[16];
char filename[256];
u64 timestamp; // Add this field
};
// In the program
event->timestamp = bpf_ktime_get_ns();
Exercise 2: Filter by Process Name¶
// Only monitor files opened by specific processes
char target_comm[16] = "cat";
char current_comm[16];
bpf_get_current_comm(¤t_comm, sizeof(current_comm));
// Simple string comparison (eBPF style)
bool match = true;
for (int i = 0; i < 16; i++) {
if (current_comm[i] != target_comm[i]) {
match = false;
break;
}
if (current_comm[i] == '\0') break;
}
if (!match) return 0; // Skip event
Exercise 3: Count Operations¶
// Add a hash map to count operations per process
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, u32); // PID
__type(value, u64); // Count
__uint(max_entries, 1024);
} process_counts SEC(".maps");
// In the program
u32 pid = bpf_get_current_pid_tgid() & 0xFFFFFFFF;
u64 *count = bpf_map_lookup_elem(&process_counts, &pid);
if (count) {
(*count)++;
} else {
u64 initial_count = 1;
bpf_map_update_elem(&process_counts, &pid, &initial_count, BPF_ANY);
}
โ Checkpoint¶
Before moving to the next section, make sure you:
- Understand the basic eBPF program structure
- Know how to define data structures for events
- Understand ring buffers for data transfer
- Can use basic eBPF helper functions
- Your program compiles without errors
๐ What's Next?¶
Great! You've written your first eBPF program. Now let's create the Go userspace application to load this program and display the events:
๐ Additional Resources¶
- eBPF Helper Functions - Complete list of available helpers
- Linux Tracepoints - Available tracepoints
- eBPF Verifier - Understanding program verification