← Back to list

eBPF Tracing Implementation and Troubleshooting

Effective eBPF tracing implementation and troubleshooting require a deep understanding of kernel internals and the tooling available to…

Linux Guide · 2025-12-18 09:27 · 3 claps · 9.5 min read
#ebpf #tracing #implement #debug #bpf
Open on Medium ↗

eBPF Tracing Implementation and Troubleshooting

Effective eBPF tracing implementation and troubleshooting require a deep understanding of kernel internals and the tooling available to monitor and debug systems at a low level. This article delves into the intricacies of eBPF tracing, offering practical guidance for senior Linux engineers, DevOps architects, and cloud infrastructure specialists looking to leverage its power. Mastering eBPF tracing unlocks unprecedented observability into complex system behavior.

🏁 Introduction

eBPF, or extended Berkeley Packet Filter, has emerged as a powerful technology for observing and manipulating system behavior at the kernel level. It allows users to run sandboxed programs in the kernel without modifying kernel source code or loading kernel modules, enabling dynamic tracing, performance monitoring, and sophisticated network traffic control. This article provides a comprehensive guide to implementing and troubleshooting eBPF tracing solutions, focusing on practical examples and advanced techniques for real-world deployments. This deep dive is critical for diagnosing performance bottlenecks and improving application and system efficiency.

🧠 Core Concepts

Understanding the core concepts of eBPF is crucial for successful implementation and troubleshooting. These concepts include the eBPF virtual machine, maps, helpers, and the verifier.

The eBPF Virtual Machine executes the eBPF programs within the kernel. It’s a register-based machine with a limited instruction set, designed for safety and efficiency. Programs must adhere to strict rules enforced by the verifier to prevent crashes or security vulnerabilities.

Maps are data structures that facilitate communication between eBPF programs and user space. They can store various types of data, such as counters, histograms, and key-value pairs. Maps are essential for aggregating and sharing data collected by eBPF programs.

Helpers are kernel functions that eBPF programs can call to perform specific tasks, such as reading timestamps, accessing network packets, or manipulating maps. These helpers provide a safe and controlled interface to kernel functionality.

The Verifier is a critical component that ensures the safety and security of eBPF programs. It checks for various potential issues, such as out-of-bounds memory access, infinite loops, and invalid instruction sequences. Only programs that pass verification can be loaded into the kernel. The verifier is a powerful tool for preventing security vulnerabilities and system instability.

Effective eBPF programs typically involve a combination of these elements. An eBPF program is attached to a tracepoint, kprobe, or uprobe. When that event occurs, the eBPF program executes, potentially using helpers to gather data, storing it in a map. User-space tools can then read data from the map to monitor system behavior. This allows for dynamic instrumentation without requiring modifications to the running kernel or applications.

⚙️ Comprehensive Code Examples

This section presents a series of code examples demonstrating various eBPF tracing techniques and troubleshooting approaches. Each example includes a description of its purpose, use cases, risk assessment, operational value, the code itself, and a detailed explanation.

1️⃣ Tracing Kernel Function Execution Time with kprobe

This example demonstrates how to trace the execution time of a specific kernel function using kprobes, which allows us to instrument almost any kernel function without modifying the kernel. This provides critical insight into kernel performance.

💡 Use Case: Identifying performance bottlenecks within the kernel by measuring the execution time of specific functions.

⚠️ Risk Assessment: Incorrectly configured kprobes can potentially destabilize the system, especially if attached to frequently called functions. Thorough testing in a non-production environment is crucial.

🚀 Operational Value: Provides real-time insights into kernel function performance, enabling faster root cause analysis and optimization.

#include <uapi/linux/ptrace.h>

struct data_t {
    u64 pid;
    u64 ts;
    u64 duration;
    char func[64];
};

BPF_PERF_OUTPUT(events);

BPF_HASH(start, u64, u64);

int kprobe__sys_enter_openat(struct pt_regs *ctx) {
    u64 pid = bpf_get_current_pid_tgid();
    u64 ts = bpf_ktime_get_ns();
    start.update(&pid, &ts);
    return 0;
}

int kretprobe__sys_enter_openat(struct pt_regs *ctx) {
    u64 pid = bpf_get_current_pid_tgid();
    u64 *tsp = start.lookup(&pid);
    if (tsp == 0) {
        return 0;
    }

    u64 ts = bpf_ktime_get_ns();
    u64 delta = ts - *tsp;

    struct data_t data = {.pid = pid, .ts = ts, .duration = delta};
    bpf_probe_read_str(&data.func, sizeof(data.func), "sys_openat");

    events.perf_submit(ctx, &data, sizeof(data));
    start.delete(&pid);
    return 0;
}

This code uses kprobes to trace the sys_enter_openat function, recording the entry and exit timestamps. It calculates the duration of the function call and submits the data to a perf event. The BPF_HASH map stores the start timestamp for each process ID. This enables tracking function execution duration across multiple processes, offering comprehensive performance insights. The code is designed for systems using BCC or similar eBPF frameworks and requires appropriate kernel headers.

2️⃣ Monitoring Network Latency with tracepoint

This example utilizes tracepoints to monitor network latency, providing a more stable and less intrusive alternative to kprobes for certain events. Tracepoints are statically defined points in the kernel code, ensuring API stability.

💡 Use Case: Measuring network latency for specific events, such as TCP connection establishment or data transfer.

⚠️ Risk Assessment: Minimal risk, as tracepoints are designed to be stable and non-intrusive. However, incorrect filtering or aggregation can lead to inaccurate results.

🚀 Operational Value: Enables real-time monitoring of network performance, facilitating proactive identification of latency issues and optimization of network configurations.

#include <linux/sched.h>

struct data_t {
    u32 pid;
    u64 timestamp;
    u32 saddr;
    u32 daddr;
    u16 sport;
    u16 dport;
};

BPF_PERF_OUTPUT(tcp_connect_events);

int kprobe__tcp_v4_connect(struct pt_regs *ctx, struct sock *sk) {
    struct data_t data = {};
    data.pid = bpf_get_current_pid_tgid() >> 32;
    data.timestamp = bpf_ktime_get_ns();
    data.saddr = sk->__sk_common.skc_rcv_saddr;
    data.daddr = sk->__sk_common.skc_daddr;
    data.sport = sk->__sk_common.skc_num;
    data.dport = sk->__sk_common.skc_dport;

    tcp_connect_events.perf_submit(ctx, &data, sizeof(data));

    return 0;
}

This eBPF program attaches to the tcp_v4_connect kprobe, extracting data about the TCP connection, including source and destination addresses and ports. It then submits this data to a perf event. The user-space application can then read this data and calculate latency metrics. This approach provides detailed information about network connections and can be used to identify slow or problematic connections. The code relies on specific kernel structures and might require adjustments based on the kernel version.

3️⃣ Tracking Memory Allocation with uprobe

This example demonstrates using uprobes to trace memory allocation within a user-space application, allowing for detailed monitoring of application memory usage. This is useful for debugging memory leaks and optimizing application performance.

💡 Use Case: Tracking memory allocation patterns in user-space applications to identify memory leaks or inefficient allocation strategies.

⚠️ Risk Assessment: Attaching uprobes to frequently called functions can introduce overhead and potentially impact application performance.

🚀 Operational Value: Provides detailed insights into application memory usage, enabling developers to optimize memory allocation and improve application stability.

#include <uapi/linux/ptrace.h>

struct data_t {
    u32 pid;
    u64 timestamp;
    size_t size;
};

BPF_PERF_OUTPUT(malloc_events);

int uprobe__malloc(struct pt_regs *ctx) {
    struct data_t data = {};
    data.pid = bpf_get_current_pid_tgid() >> 32;
    data.timestamp = bpf_ktime_get_ns();
    data.size = PT_REGS_PARM1(ctx);

    malloc_events.perf_submit(ctx, &data, sizeof(data));
    return 0;
}

This code attaches an uprobe to the malloc function in a user-space application. It captures the size of the allocated memory and submits this data to a perf event. The user-space application can then analyze this data to track memory allocation patterns and identify potential issues. This requires specifying the path to the application binary and the offset of the malloc function.

4️⃣ Filtering Events Based on Process ID

This example demonstrates how to filter events based on the process ID, allowing you to focus on specific processes and reduce the amount of data collected. Filtering based on process ID is crucial for isolating specific application behavior.

💡 Use Case: Monitoring the behavior of a specific process, such as a critical service or a problematic application.

⚠️ Risk Assessment: Incorrectly configured filters can lead to missed events or inaccurate results.

🚀 Operational Value: Enables focused monitoring of specific processes, reducing noise and improving the efficiency of analysis.

#include <uapi/linux/ptrace.h>

#define TARGET_PID 12345 // Replace with the PID you want to monitor

struct data_t {
    u32 pid;
    u64 timestamp;
    char message[64];
};

BPF_PERF_OUTPUT(filtered_events);

int kprobe__sys_enter_write(struct pt_regs *ctx) {
    u32 pid = bpf_get_current_pid_tgid() >> 32;

    if (pid != TARGET_PID) {
        return 0; // Ignore events from other processes
    }

    struct data_t data = {};
    data.pid = pid;
    data.timestamp = bpf_ktime_get_ns();
    bpf_probe_read_str(&data.message, sizeof(data.message), "sys_write");

    filtered_events.perf_submit(ctx, &data, sizeof(data));
    return 0;
}

This code attaches to the sys_enter_write kprobe and filters events based on the process ID. Only events from the process with the specified ID will be submitted to the perf event. This allows for focused monitoring of specific processes and reduces the amount of data collected.

5️⃣ Aggregating Data with eBPF Maps

This example demonstrates how to use eBPF maps to aggregate data, such as counting the number of times a specific function is called. Aggregation is essential for creating metrics and identifying trends.

💡 Use Case: Counting the number of times a specific function is called or tracking the distribution of values.

⚠️ Risk Assessment: Incorrectly configured maps can lead to memory leaks or performance issues.

🚀 Operational Value: Enables efficient aggregation of data, allowing for the creation of meaningful metrics and the identification of trends.

#include <uapi/linux/ptrace.h>

BPF_HASH(counts, u64, u64);

int kprobe__sys_enter_read(struct pt_regs *ctx) {
    u64 pid = bpf_get_current_pid_tgid() >> 32;
    u64 *valp = counts.lookup(&pid);
    if (!valp) {
        u64 initval = 1;
        counts.update(&pid, &initval);
        return 0;
    }
    (*valp)++;
    return 0;
}

This code attaches to the sys_enter_read kprobe and increments a counter in the counts map for each process. The user-space application can then read the values from the map to track the number of times the sys_enter_read function has been called for each process.

6️⃣ Using Ring Buffer for High-Throughput Data Collection

This example demonstrates using the ring buffer for high-throughput data collection, which offers better performance than perf events for certain use cases. The ring buffer is a lockless data structure that allows for efficient data transfer between the kernel and user space.

💡 Use Case: Collecting high-throughput data, such as network packets or system events.

⚠️ Risk Assessment: Requires careful management of buffer size to avoid data loss or memory exhaustion.

🚀 Operational Value: Enables efficient collection of high-throughput data, facilitating real-time analysis and monitoring.

#include <linux/ring_buffer.h>
#include <linux/ptrace.h>

struct event {
    u32 pid;
    u64 timestamp;
    u64 value;
};

BPF_RINGBUF_OUTPUT(events, 4096);

int kprobe__sys_enter_nanosleep(struct pt_regs *ctx) {
    struct event *e;

    e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
    if (!e)
        return 0;

    e->pid = bpf_get_current_pid_tgid();
    e->timestamp = bpf_ktime_get_ns();
    e->value = PT_REGS_PARM1(ctx);

    bpf_ringbuf_submit(e, 0);
    return 0;
}

This code attaches to the sys_enter_nanosleep kprobe and writes data to the ring buffer. The user-space application can then read the data from the ring buffer. This approach offers better performance than perf events for high-throughput data collection. Ring buffer is critical for environments requiring minimal latency.

7️⃣ Implementing Rate Limiting to Prevent Overload

This example demonstrates how to implement rate limiting to prevent overload, ensuring that the eBPF program does not consume excessive resources. Rate limiting is crucial for maintaining system stability.

💡 Use Case: Preventing eBPF programs from consuming excessive resources and impacting system performance.

⚠️ Risk Assessment: Incorrectly configured rate limits can lead to missed events or inaccurate results.

🚀 Operational Value: Ensures system stability and prevents eBPF programs from negatively impacting performance.

#include <uapi/linux/ptrace.h>

BPF_MAP_DEF(LRU_HASH, ratelimit, u32, u64, 1024, BPF_F_NO_PREALLOC);
BPF_MAP_TYPE(ratelimit) ratelimit SEC(".maps");

#define MAX_CALLS_PER_SECOND 100

int kprobe__sys_enter_clone(struct pt_regs *ctx) {
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    u64 now = bpf_ktime_get_ns();
    u64 *last_time = bpf_map_lookup_elem(&ratelimit, &pid);

    if (last_time) {
        if (now - *last_time < 1000000000UL / MAX_CALLS_PER_SECOND) {
            return 0; // Rate limited
        }
    }

    bpf_map_update_elem(&ratelimit, &pid, &now, BPF_ANY);
    return 0;
}

This code attaches to the sys_enter_clone kprobe and implements rate limiting based on the number of calls per second. If the rate limit is exceeded, the event is ignored. This prevents the eBPF program from consuming excessive resources.

8️⃣ Handling Errors and Logging Information

This example demonstrates how to handle errors and log information, improving the debuggability and reliability of eBPF programs. Proper error handling is essential for production deployments.

💡 Use Case: Improving the debuggability and reliability of eBPF programs.

⚠️ Risk Assessment: Inadequate error handling can lead to missed events or incorrect results.

🚀 Operational Value: Enables faster root cause analysis and improves the stability of eBPF programs.

#include <uapi/linux/ptrace.h>

BPF_PERF_OUTPUT(error_events);

struct error_data_t {
    u32 pid;
    char message[64];
};

int kprobe__sys_enter_unlinkat(struct pt_regs *ctx) {
    struct error_data_t data = {};
    data.pid = bpf_get_current_pid_tgid() >> 32;

    int ret = bpf_probe_read_str(&data.message, sizeof(data.message), "sys_unlinkat");
    if (ret < 0) {
        // Handle the error
        bpf_printk("Error reading string: %d", ret);
        bpf_probe_read_str(&data.message, sizeof(data.message), "Error occurred");
    }

    error_events.perf_submit(ctx, &data, sizeof(data));
    return 0;
}

This code attaches to the sys_enter_unlinkat kprobe and attempts to read a string. If an error occurs, it logs the error message using bpf_printk and submits an error event. This allows for easier debugging and identification of issues.

9️⃣ Using cBPF Filters with eBPF

This example demonstrates how to use classic BPF cBPF filters with eBPF programs to further refine the events being traced.

💡 Use Case: Filtering specific network packets or system calls based on complex criteria.

⚠️ Risk Assessment: cBPF filters can increase the complexity of the eBPF program and may impact performance.

🚀 Operational Value: Enables highly specific filtering of events, improving the efficiency of analysis and reducing noise.

#include <linux/bpf.h>
#include <linux/ptrace.h>

struct data_t {
    u32 pid;
    u64 timestamp;
    u32 saddr;
    u32 daddr;
};

BPF_PERF_OUTPUT(filtered_events);

// This example requires a separate cBPF filter to be loaded
int kprobe__tcp_v4_connect(struct pt_regs *ctx, struct sock *sk) {
    struct data_t data = {};
    data.pid = bpf_get_current_pid_tgid() >> 32;
    data.timestamp = bpf_ktime_get_ns();
    data.saddr = sk->__sk_common.skc_rcv_saddr;
    data.daddr = sk->__sk_common.skc_daddr;

    filtered_events.perf_submit(ctx, &data, sizeof(data));

    return 0;
}

This eBPF code does not contain the cBPF filter directly, but assumes that a cBPF filter has been pre-loaded to filter network packets. This integration allows for powerful and flexible filtering capabilities. The cBPF filter needs to be loaded separately using tools like tcpdump or bpftool.

1️⃣0️⃣ Monitoring File System Operations with VFS Hooks

This example shows how to monitor file system operations using Virtual File System VFS hooks with eBPF.

💡 Use Case: Gaining insight into file access patterns, identifying performance bottlenecks, and tracking malicious activity.

⚠️ Risk Assessment: Instrumenting VFS hooks can introduce overhead and potentially impact file system performance.

🚀 Operational Value: Enables detailed monitoring of file system operations, allowing for improved performance tuning and security monitoring.

#include <uapi/linux/ptrace.h>
#include <linux/fs.h>

struct data_t {
    u32 pid;
    u64 timestamp;
    char filename[64];
};

BPF_PERF_OUTPUT(file_events);

int kprobe__vfs_read(struct pt_regs *ctx, struct file *file, char __user *buf, size_t count, loff_t *pos) {
    struct data_t data = {};
    data.pid = bpf_get_current_pid_tgid() >> 32;
    data.timestamp = bpf_ktime_get_ns();
    bpf_probe_read_str(&data.filename, sizeof(data.filename), file->f_path.dentry->d_name.name);

    file_events.perf_submit(ctx, &data, sizeof(data));
    return 0;
}

This code attaches to the vfs_read kprobe, extracts the filename, and submits the data to a perf event. This provides detailed information about file read operations and can be used to identify performance bottlenecks or track malicious activity.

🧩 Conclusion

Effective eBPF tracing implementation and troubleshooting require a comprehensive understanding of eBPF concepts, careful planning, and thorough testing. By leveraging the techniques and code examples presented in this article, senior Linux engineers, DevOps architects, and cloud infrastructure specialists can unlock the power of eBPF to gain unprecedented observability into complex system behavior, enabling faster root cause analysis, improved performance, and enhanced security. This detailed analysis empowers professionals to manage and optimize systems with precision.


메타데이터
post_id
d137027f45f3
slug
ebpf-tracing-implementation-and-troubleshooting-d137027f45f3
url
https://medium.com/@linuxgd/ebpf-tracing-implementation-and-troubleshooting-d137027f45f3
canonical_url
https://medium.com/@linuxgd/ebpf-tracing-implementation-and-troubleshooting-d137027f45f3
author_url
https://medium.com/@linuxgd
status
ok
fetched_at
2026-06-26 12:24:55