Table of Contents
- Introduction to Kernel Debugging
- Understanding the Kernel Debugging Environment
- Essential Static Debugging Techniques
- Dynamic Tracing and Profiling
- Interactive Debugging with kgdb
- Post-Mortem Debugging with kdump and crash
- Advanced Techniques: eBPF for Kernel Tracing
- Common Kernel Bugs and How to Diagnose Them
- Best Practices for Effective Kernel Debugging
- Conclusion
- References
Understanding the Kernel Debugging Environment
Kernel debugging differs fundamentally from user-space debugging due to the kernel’s role as the core of the operating system. Key differences include:
- Privilege Level: The kernel runs in ring 0 (x86) or EL1 (ARM), with unrestricted access to hardware and memory. A bug here can corrupt the entire system, leading to panics or data loss.
- No User-Space Safety Nets: There’s no signal handling for errors like segmentation faults. Instead, the kernel triggers an “oops” (non-fatal error) or “panic” (fatal error) and may reboot.
- Limited Tooling: Traditional user-space tools (e.g.,
valgrind) don’t work in kernel-space. Specialized tools likekgdb,ftrace, orkdumpare required. - Kernel Symbols and Artifacts: Debugging relies on kernel symbols (stored in
vmlinux, the uncompressed kernel image), module symbols, andkallsyms(a runtime symbol table).
Key Concepts:
- vmlinux: The kernel’s uncompressed, executable image with debug symbols (if compiled with
-g). - Modules: Loadable kernel modules (
.kofiles) with their own symbols. - kallsyms:
/proc/kallsymsprovides runtime access to kernel symbol addresses, critical for debugging oops logs. - Oops/Panic Logs: Kernel error reports containing registers, stack traces, and faulty instruction pointers (RIP).
Essential Static Debugging Techniques
Static debugging involves embedding debug logic directly into kernel code or using compile-time tools to inspect behavior. These techniques are simple but powerful for initial diagnostics.
printk and Kernel Logging
printk is the kernel’s equivalent of printf and the most basic debugging tool. It logs messages to the kernel ring buffer, accessible via dmesg or /var/log/kern.log.
Log Levels:
printk supports log levels to prioritize messages. Levels range from KERN_EMERG (highest priority, system unusable) to KERN_DEBUG (lowest, verbose debugging):
printk(KERN_EMERG "System halted!\n"); // Level 0
printk(KERN_ALERT "Immediate action needed!\n"); // Level 1
printk(KERN_CRIT "Critical condition!\n"); // Level 2
printk(KERN_ERR "Error occurred!\n"); // Level 3
printk(KERN_WARNING "Warning!\n"); // Level 4
printk(KERN_NOTICE "Notice (normal but significant)\n"); // Level 5
printk(KERN_INFO "Informational message\n"); // Level 6
printk(KERN_DEBUG "Debugging message\n"); // Level 7
Controlling Log Output:
- Use
dmesgto view the ring buffer:dmesg | grep "my debug message". - Adjust verbosity with
/proc/sys/kernel/printk, which sets the default log level (e.g.,echo 8 > /proc/sys/kernel/printkenables all levels).
Pitfalls:
- Buffer Size: The ring buffer is limited (default ~16KB). Avoid large messages or spamming
printkin hot paths (e.g., interrupt handlers). - Performance Impact: Frequent
printkcalls slow down the kernel. Use them sparingly.
debugfs and procfs for Debug Data
debugfs and procfs allow kernel code to expose debug data via virtual files in /sys/kernel/debug (debugfs) or /proc (procfs). debugfs is preferred for debugging due to its flexibility and isolation from production interfaces.
Example: Creating a debugfs File
To expose a variable for read/write via debugfs:
#include <linux/debugfs.h>
#include <linux/module.h>
static struct dentry *debug_dir; // Handle for debugfs directory
static u32 debug_counter = 0; // Variable to expose
static int __init my_debug_init(void) {
// Create a directory under /sys/kernel/debug
debug_dir = debugfs_create_dir("my_driver", NULL);
if (!debug_dir)
return -ENOMEM;
// Expose debug_counter as a read/write file
debugfs_create_u32("counter", 0644, debug_dir, &debug_counter);
return 0;
}
static void __exit my_debug_exit(void) {
debugfs_remove_recursive(debug_dir); // Cleanup
}
module_init(my_debug_init);
module_exit(my_debug_exit);
MODULE_LICENSE("GPL");
After loading the module, read/write the counter via:
cat /sys/kernel/debug/my_driver/counter
echo 42 > /sys/kernel/debug/my_driver/counter
Compiler Debug Flags
Compiling the kernel with debug flags enables richer debugging information:
-g: Includes debug symbols invmlinuxand modules, required for tools likegdborcrash.-O0: Disables optimizations, making variables and control flow easier to inspect (use cautiously—slows the kernel).-Og: Balances debuggability and performance (recommended for most debugging).
To enable these, configure the kernel with make menuconfig:
- Set
Kernel hacking → Compile-time checks and compiler options → Debug informationtoReliableorFull. - Disable
Kernel hacking → Compile the kernel with frame pointers(optional but helps with stack traces).
Dynamic Tracing and Profiling
Dynamic tracing tools capture runtime behavior without modifying kernel code, ideal for diagnosing performance issues or intermittent bugs.
Ftrace: Function Tracing and Beyond
ftrace is a built-in kernel tracer that supports function calls, events, and custom tracepoints. It’s accessible via /sys/kernel/debug/tracing.
Basic Function Tracing:
- Enable the function tracer:
echo function > /sys/kernel/debug/tracing/current_tracer - View traces:
cat /sys/kernel/debug/tracing/trace - Filter by function or module:
echo "sys_open" > /sys/kernel/debug/tracing/set_ftrace_filter
Advanced Tracers:
- Function Graph Tracer: Shows call graphs with latency:
echo function_graph > /sys/kernel/debug/tracing/current_tracer - Event Tracer: Traces kernel events (e.g., scheduler, memory allocations):
ls /sys/kernel/debug/tracing/events # List events echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable # Enable event
perf: Performance Analysis and Event Tracing
perf is a versatile tool for profiling and tracing kernel/user-space events. It uses hardware performance counters and kernel tracepoints.
Common Use Cases:
- Trace syscalls:
perf traceshows syscalls in real time. - Sample function calls:
perf record -g -p <pid>profiles a process, andperf reportvisualizes the call graph. - Trace kernel events:
perf listshows available events (e.g.,sched:sched_switch), andperf record -e sched:sched_switchcaptures them.
Example: Diagnosing Latency
To find functions causing high latency:
perf record -g -a sleep 10 # Profile all CPUs for 10 seconds
perf report --sort=dso,function # Show latency by function
Interactive Debugging with kgdb
kgdb extends gdb to debug the kernel interactively, supporting breakpoints, watchpoints, and stack inspection. It requires two machines (or a VM) connected via serial, Ethernet, or USB.
Setting Up kgdb
1. Configure the Kernel:
Enable kgdb in make menuconfig:
Kernel hacking → KGDB: kernel debuggerKernel hacking → KGDB: use kgdb over serial console(or Ethernet withkgdboc).
2. Boot the Target Kernel with kgdb Options:
Add to the bootloader (e.g., GRUB) command line:
kgdboc=ttyS0,115200 kgdbwait
kgdboc=ttyS0,115200: Use serial portttyS0at 115200 baud.kgdbwait: Pause at boot to wait for a debugger connection.
3. Connect from the Host:
On the host machine, run:
gdb vmlinux
(gdb) target remote /dev/ttyS0 # Replace with your serial port
Breakpoints, Watchpoints, and Inspecting State
Once connected, use standard gdb commands:
break sys_open: Break whensys_openis called.watch my_variable: Pause whenmy_variableis modified.bt: Print a stack trace.info registers: Inspect CPU registers.c: Continue execution.
Post-Mortem Debugging with kdump and crash
When the kernel panics, kdump captures a memory dump (vmcore) for offline analysis. The crash utility then inspects the vmcore using vmlinux symbols.
Configuring kdump
1. Reserve Memory for the Crash Kernel:
Add crashkernel=128M to the bootloader command line (adjust size based on RAM).
2. Install kdump Tools:
On Debian/Ubuntu:
sudo apt install kdump-tools
On RHEL/CentOS:
sudo yum install kexec-tools
3. Trigger a Panic to Test:
Force a panic (for testing only!):
echo c > /proc/sysrq-trigger
The system will reboot, and kdump will save vmcore to /var/crash/.
Analyzing vmcores with crash Utility
The crash tool parses vmcore to inspect the kernel’s state at panic time:
crash vmlinux /var/crash/202401011234/vmcore
Key crash Commands:
bt: Show the panic stack trace.ps: List running processes.lsmod: List loaded modules.kmem: Inspect kernel memory usage.struct task_struct 0xdeadbeef: Dump atask_struct(process descriptor).
Advanced Techniques: eBPF for Kernel Tracing
eBPF (extended Berkeley Packet Filter) is a revolutionary technology for writing sandboxed kernel programs without recompiling the kernel. It’s ideal for tracing, monitoring, and debugging.
Introduction to eBPF
eBPF programs run in a restricted VM in the kernel, triggered by events (e.g., syscalls, function calls, network packets). They can collect data and send it to user-space via maps.
Using bcc and bpftrace for Debugging
BCC (BPF Compiler Collection):
BCC provides Python/JavaScript frontends to write eBPF programs. Example: Trace sys_open calls with filenames:
from bcc import BPF
# eBPF program to trace sys_open
bpf_code = """
#include <vmlinux.h>
#include <bcc/proto.h>
int trace_sys_open(struct pt_regs *ctx, const char __user *filename) {
bpf_trace_printk("Opening file: %s\\n", filename);
return 0;
}
"""
# Load and attach to sys_open
b = BPF(text=bpf_code)
b.attach_kprobe(event="sys_open", fn_name="trace_sys_open")
# Print output
b.trace_print()
bpftrace:
bpftrace is a high-level scripting language for eBPF. Example: Count sys_open calls per process:
bpftrace -e 'kprobe:sys_open { @count[pid, comm] = count(); }'
Common Kernel Bugs and How to Diagnose Them
Null Pointer Dereferences
Symptom: Oops log with NULL pointer dereference and RIP (instruction pointer).
Diagnosis:
- Use
addr2lineto map RIP to a function/line:addr2line -e vmlinux 0xffffffff81234567 - Check the function for uninitialized pointers (e.g.,
struct my_struct *ptr; ptr->field = 42;).
Race Conditions and Deadlocks
Symptom: System hang, high CPU usage, or lockdep warnings.
Diagnosis:
- Use
ftraceto trace lock acquisition order:echo lockdep > /sys/kernel/debug/tracing/current_tracer - Use
perf lockto detect contended locks:perf lock record -p <pid> # Record lock events perf lock report # Analyze contention
Memory Leaks
Symptom: Increasing kernel memory usage (free -m shows shrinking MemFree).
Diagnosis:
- Use
kmemleak: Enable withkmemleak=onin the boot command line, then:echo scan > /sys/kernel/debug/kmemleak cat /sys/kernel/debug/kmemleak # List unreferenced objects
Best Practices for Effective Kernel Debugging
- Reproduce the Bug Consistently: Debugging intermittent bugs is nearly impossible. Isolate triggers (e.g., specific workloads, hardware).
- Use a Test Environment: Never debug on production systems—use VMs (QEMU/KVM) or dedicated test machines.
- Leverage Multiple Tools: Combine
printk,ftrace, andkgdbfor layered insights. - Enable Debug Symbols: Always compile the kernel with
-gto get meaningful stack traces. - Document Everything: Log steps, oops logs, and tool outputs for later analysis.
- Start Simple: Use
dmesgandprintkfirst before moving to complex tools like eBPF.
Conclusion
Kernel debugging is a challenging but rewarding skill. By mastering techniques like printk, ftrace, kgdb, and eBPF, developers can diagnose even the most elusive kernel bugs. Remember to combine static and dynamic tools, leverage post-mortem analysis, and follow best practices to minimize downtime and system risk. With persistence and the right tools, you’ll be able to build more robust and reliable kernel software.
References
- Linux Kernel Documentation: Debugging
- Ftrace User Guide
- kgdb Documentation
- kdump and crash Utility
- eBPF.io
- BPF Compiler Collection (BCC)
- bpftrace Reference Guide
- Linux Kernel Development by Robert Love (3rd Edition)
- Kernel Memory Leak Debugging