eBPF and Kernel Modules — What’s the Difference
The core of an operating system is very stable. It supports the entire OS, making changes complicated and time-consuming by design. How can we extend its functionality without risking instability?
The solution lies in Extended Berkeley Packet Filter (eBPF) programs and kernel modules—processes that can be dynamically loaded into and unloaded from the kernel on demand to extend or modify kernel functionality.
What is a Kernel Module?
A kernel module (or loadable kernel mode) is an object file containing code that extends kernel functionality at runtime. It can be loaded when needed and unloaded when no longer required. Device drivers typically operate as kernel modules, enabling the kernel to interact with connected hardware. Without modules, we would need to build monolithic kernels and add new functionality directly into the kernel image. This approach would not only result in larger kernels but also require rebuilding and rebooting the kernel for each new feature.
An example of a kernel module:
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
MODULE_DESCRIPTION("My kernel module");
MODULE_AUTHOR("Me");
MODULE_LICENSE("GPL");
static int dummy_init(void)
{
pr_debug("Hi\n");
return 0;
}
static void dummy_exit(void)
{
pr_debug("Bye\n");
}
module_init(dummy_init);
module_exit(dummy_exit);
The generated messages will not be displayed on the console but will be saved in a specially reserved memory area, from where they will be extracted by the logging daemon (syslog). To display kernel messages, you can use the dmesg command or inspect the logs:
# cat /var/log/syslog | tail -2
Feb 20 13:57:38 asgard kernel: Hi
Feb 20 13:57:43 asgard kernel: Bye
# sudo dmesg | tail -2
Hi
Bye
With great power comes great responsibility. A single wild pointer can wipe out your file system, and a core dump means a reboot.
Kernel Modules are very general purpose and can be used to write any type of program. There is no hand-holding or guardrails either.
However, eBPF is highly structured and follows strict rules. It offers several specific program types, including:
BPF_PROG_TYPE_KPROBE: determine whether a kprobe should fire or notBPF_PROG_TYPE_SCHED_CLS: a network traffic-control classifierBPF_PROG_TYPE_LWT_*: a network packet filter for lightweight tunnelsBPF_PROG_TYPE_XDP: a network packet filter run from the device-driver receive pathBPF_PROG_CGROUP_DEVICE: determine if a device operation should be permitted or not
Each program type has restricted access to specific data and functions, as it's designed for a particular purpose. Before compilation, an eBPF program must pass a verifier that checks its correctness and complexity.
What is eBPF?
eBPF is a technology that enables developers to write efficient, safe, and non-intrusive programs that run directly in the Linux kernel. These programs are written in a restricted C subset and compiled into eBPF bytecode using tools such as LLVM. ****The bytecode consists of a restricted set of instructions that follow the eBPF instruction set architecture and prevent runtime errors.
eBPFs operate as virtual machines (VMs) inside the Linux kernel, working on a low-level instruction set architecture and run eBPF bytecode.
Primary components in an eBPF program:
-
**eBPF interpreter/JIT compiler:**Just-in-time (JIT) compilers offer superior performance compiling bytecode when needed
-
**User space loader:**User space loaders are programs in the user space that load the eBPF bytecode into the kernel, attaching it to the appropriate hooks and managing any associated eBPF maps.
-
**eBPF maps:**eBPF maps are data structures with key-value pairs and read/write access that provide shared storage space and facilitate interaction between eBPF kernel programs and user space applications. Created and managed through system calls, eBPF maps can also be used to maintain state between different iterations of the eBPF programs.
-
eBPF verifier:
The verifier a critical component of eBPF systems checks the bytecode before it's loaded into the kernel to ensure the program contains no harmful operations like infinite loops, illegal instructions, or out-of-bounds memory access. It also verifies that all program data paths terminate successfully.
-
eBPF hooks:
Hooks are points in the kernel code where eBPF programs can be attached. When the kernel reaches a hook, it runs the attached eBPF program.
eBPF programs gain broad data access through various hook types, including tracepoints, kprobes, uprobes, and network packet receive queues. Tracepoints allow programs to inspect and collect data about the kernel or other processes. Traffic control hooks enable inspection and modification of network packets. Additionally, kprobes and uprobes enable dynamic tracing at both kernel and user levels.
-
Express data paths (XDPs):
XDPs are high-performance data paths that accelerate packet processing at the driver level and facilitate transfer across communication layers. They enable eBPF systems to make data routing decisions before data packets even reach the kernel.
XDP enabled developers to deploy eBPF-based load balancing functions capable of managing data traffic in even the busiest data centers.
-
Helper functions:
Because eBPFs cannot generate arbitrary functions and must maintain compatibility with every possible kernel version, sometimes basic eBPF instruction sets aren’t nuanced enough to run advanced operations. Helper functions bridge this gap.
An example of a kernel module:
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
SEC("xdp")
int xdp_drop_all(struct xdp_md *ctx)
{
return XDP_DROP;
}
char _license[] SEC("license") = "GPL";
These are the possible actions that XDP can perform:
/* User return codes for XDP prog type.
* A valid XDP program must return one of these defined values. All other
* return codes are reserved for future use. Unknown return codes will
* result in packet drops and a warning via bpf_warn_invalid_xdp_action().
*/
enum xdp_action {
XDP_ABORTED = 0,
XDP_DROP,
XDP_PASS,
XDP_TX,
XDP_REDIRECT,
};
Scope and Use-Cases of ePBF and kernel modules
eBPF's scope primarily focuses on security, observability, and networking. An eBPF program can perform various tasks such as overwriting parts of the Linux networking stack, making decisions about network packets before they reach the kernel, and monitoring kernel system calls from other programs.
Kernel modules have a broader scope than eBPF and can perform nearly everything eBPF programs can do and more. However, there are two key domains exclusive to kernel modules: device drivers and filesystems. This limitation exists because these use cases require direct hardware access.
There is, however, one notable exception where eBPF can do something kernel modules cannot. Modern network interface cards (NICs), particularly SmartNIC-based ones, can run XDP that processes packets immediately upon receipt, before they reach the kernel network stack. Even without direct NIC support, XDP programs can run on the CPU while still bypassing the Linux network stack for high-performance processing.
Conclusion
While eBPF is more restrictive than kernel modules, it offers simpler development through high-level libraries. Choose eBPF for networking, security, or observability tasks if your kernel supports it. For other use-cases, kernel modules remain the standard option.
In cloud environments, eBPF enhances application networking, firewall rules, and network performance. It enables faster development of complex applications with greater kernel independence.