Linux Kernel Dynamic Debugging: pr_debug() & Dynamic Debug

pr_debug() and Dynamic Debug
Free Linux Kernel Programming Course · Part 6 · EmbeddedPathashala
Zero
Runtime Overhead
No Reboot
Toggle at Runtime
Linux 6.x
Verified
Series: PREV_LEC NEXT_LEC

The Problem with Debug Messages

When you are developing a kernel module, you want to print lots of detailed debug messages to understand what is happening inside. But when you ship the module for production, those same messages would flood the kernel log and slow things down.

The naive solution — using printk(KERN_DEBUG ...) — is bad because even when the message is not printed to the console, the kernel still has to format the string and go through locking overhead. In a hot path like an interrupt handler called 1000 times per second, this is significant wasted CPU time.

Linux solves this with two better tools: pr_debug() with Dynamic Debug, which gives you zero overhead when disabled and runtime on/off control without rebooting.

Three Debug Macros — Know the Difference

Beginners often confuse these three. Each behaves differently depending on how your kernel is built.

pr_debug() vs pr_devel() vs dev_dbg() — Quick Comparison
Macro Without DEBUG or DYNAMIC_DEBUG With -DDEBUG only With CONFIG_DYNAMIC_DEBUG Best used for
pr_debug() Compiled out (zero code) Always prints KERN_DEBUG Compiled in, off by default, toggle at runtime Debug in modules — preferred
pr_devel() Compiled out (zero code) Always prints KERN_DEBUG Still compiled out unless -DDEBUG Kernel-internal only, never in production
dev_dbg() Compiled out (zero code) Always prints KERN_DEBUG + device name Compiled in, off by default, toggle at runtime Device drivers — adds device identity

Key point about pr_devel(): Even if CONFIG_DYNAMIC_DEBUG=y is set in your kernel, pr_devel() does not get compiled in unless you explicitly compile with -DDEBUG. It is intentionally kept out of the dynamic debug system. Use it for very low-level debug lines you never want accidentally enabled on any production system — for example, per-packet tracing in a network driver.

How pr_debug() Actually Works Under the Hood

When CONFIG_DYNAMIC_DEBUG is enabled in your kernel (check with grep DYNAMIC_DEBUG /boot/config-$(uname -r)), every pr_debug() and dev_dbg() call is handled in a special way at compile time.

The compiler creates a small metadata record — called a _ddebug descriptor — for each callsite. This descriptor stores: the module name, the file name, the function name, the line number, and the format string. All these descriptors are packed into a special section of the kernel binary called __dyndbg.

How a Disabled pr_debug() Has Zero Cost
Step 1: Compile Time
Compiler sees pr_debug(). It creates a _ddebug record in __dyndbg section. At the call site in the code, it places a single NOP instruction (does nothing).
Step 2: Default Runtime (disabled)
The NOP is executed — 1 CPU cycle and done. No string formatting. No buffer writes. No lock overhead. Truly zero cost.
Step 3: After You Enable It
The kernel patches the NOP with a branch instruction (using jump labels). Now the callsite jumps to the debug print function. Disable it and it becomes a NOP again.

This mechanism uses the kernel’s static keys / jump labels feature. The actual binary code at the callsite is modified in memory — no branch that must be evaluated every time. When disabled, the instruction stream flows straight through without any branch taken.

Enabling pr_debug() at Runtime — No Reboot Needed

The control file is at /sys/kernel/debug/dynamic_debug/control. You need debugfs mounted (it usually is automatically) and root access.

# First, confirm dynamic debug is available
ls /sys/kernel/debug/dynamic_debug/control

# See all registered debug callsites (there are thousands in a real kernel)
cat /sys/kernel/debug/dynamic_debug/control | head -20

Each line in that file shows one debug callsite in this format:

# filename:lineno [module]function flags format
drivers/usb/core/hub.c:1234 [usbcore]hub_activate =_ "port %d: ..."

The flags field shows what is currently enabled: =_ means nothing enabled (all off). The flags you can set are:

Dynamic Debug Flags
Flag Meaning
pPrint the message (the main enable flag)
fInclude function name in the output
lInclude line number in the output
mInclude module name in the output
tInclude thread ID in the output
_No flags — disable all

Practical commands for your module

# Enable all pr_debug() in your module (simplest — just add 'p' flag)
echo 'module mymodule +p' > /sys/kernel/debug/dynamic_debug/control

# Enable with function name and line number shown
echo 'module mymodule +pfl' > /sys/kernel/debug/dynamic_debug/control

# Enable only in a specific file
echo 'file mymodule_core.c +p' > /sys/kernel/debug/dynamic_debug/control

# Enable only a specific function
echo 'func mymodule_probe +p' > /sys/kernel/debug/dynamic_debug/control

# Enable only a specific line
echo 'file mymodule_core.c line 87 +p' > /sys/kernel/debug/dynamic_debug/control

# Disable all debug messages in your module
echo 'module mymodule -p' > /sys/kernel/debug/dynamic_debug/control

# Disable all flags completely
echo 'module mymodule =_' > /sys/kernel/debug/dynamic_debug/control

Problem: What if you need debug messages from the module’s init function — which runs at insmod time, before you can write to the control file?

# Solution: pass dyndbg as a module parameter to modprobe
modprobe mymodule dyndbg=+p

# Or, to enable at boot for a built-in module
# Add this to your kernel command line in GRUB:
mymodule.dyndbg=+p
Enabling pr_debug() via the Makefile (the Old Way)

If your kernel does not have CONFIG_DYNAMIC_DEBUG, or if you want pr_devel() (which dynamic debug ignores), you compile with -DDEBUG. There are two modern ways to do this in a kernel module Makefile.

Method 1: ccflags-y (recommended, affects all .c files in this Makefile)

# In your module's Makefile
obj-m := mymodule.o
mymodule-objs := mymodule_core.o mymodule_spi.o

# Enable DEBUG for all source files
ccflags-y += -DDEBUG

Method 2: CFLAGS_filename.o (only one specific file)

# In your module's Makefile — only mymodule_core.c gets -DDEBUG
obj-m := mymodule.o
mymodule-objs := mymodule_core.o mymodule_spi.o

CFLAGS_mymodule_core.o := -DDEBUG

Method 3: Inside the source file itself (quickest for testing)

/*
 * Add this as the VERY FIRST LINE of your .c file,
 * before any #include. This enables pr_debug() and pr_devel()
 * for this file only.
 */
#define DEBUG

#include <linux/module.h>
/* ... rest of your file ... */
⚠ Important Note: The older EXTRA_CFLAGS syntax still works but is deprecated since kernel 2.6.24. Use ccflags-y instead. They do exactly the same thing but ccflags-y is the modern, correct form.
dev_*() Macros — For Device Drivers

When you write a device driver, use the dev_*() family instead of pr_*(). The difference is that every dev_*() message automatically includes the driver name and device identity (bus address, device name) so you can immediately tell which hardware instance produced the message.

#include <linux/device.h>

static int mydriver_probe(struct platform_device *pdev)
{
    /* dev_info adds "mydriver mydevice0: " prefix automatically */
    dev_info(&pdev->dev, "probe called, IRQ = %d\n", pdev->resource->start);

    if (some_error) {
        dev_err(&pdev->dev, "failed to map registers: %d\n", ret);
        return ret;
    }

    dev_warn(&pdev->dev, "operating in fallback mode\n");
    dev_dbg(&pdev->dev, "register dump: ctrl=0x%08x\n", ctrl);

    return 0;
}

The output in dmesg will look like this — notice the driver and device identity is there automatically:

[   12.345678] mydriver mydevice0: probe called, IRQ = 47
[   12.345700] mydriver mydevice0: register dump: ctrl=0x00010001
Complete dev_*() Family
Macro Equivalent Level Notes
dev_emerg(dev, fmt, …)KERN_EMERGSystem-wide emergency
dev_alert(dev, fmt, …)KERN_ALERT
dev_crit(dev, fmt, …)KERN_CRIT
dev_err(dev, fmt, …)KERN_ERRMost common in drivers
dev_warn(dev, fmt, …)KERN_WARNING
dev_notice(dev, fmt, …)KERN_NOTICE
dev_info(dev, fmt, …)KERN_INFOMost common in probe()
dev_dbg(dev, fmt, …)KERN_DEBUGDynamic debug — off by default

There are also dev_err_probe() and dev_warn_probe() specifically for the probe function — they handle the common -EPROBE_DEFER case cleanly and are the preferred style in modern drivers.

Reading Kernel Messages with dmesg

The dmesg command reads the kernel ring buffer. Here are the most useful options you will use every day as a kernel developer.

# Read all messages (may need sudo if kernel.dmesg_restrict=1)
dmesg

# Human-readable timestamps (shows real date/time, not seconds since boot)
dmesg -T

# Best for daily use: color + pager + readable timestamps
dmesg -H

# Follow — stay open and print new messages as they arrive (like tail -f)
dmesg -w

# Show only errors and warnings
dmesg -l err,warn

# Show only your module's messages
dmesg | grep mymodule

# Clear the ring buffer (useful before testing your module)
sudo dmesg -C

# Show and then clear
sudo dmesg -c

# Show last 20 lines only
dmesg | tail -20

# Change console log level to 7 (show everything including KERN_DEBUG)
sudo dmesg -n 7
Anatomy of a dmesg Line
12.345678
mymodule : probe called, IRQ = 47
Seconds since boot
Module/driver name (from pr_fmt)
Your message text

Show Everything with ignore_loglevel

Sometimes during debugging you want to see all messages on the console — including KERN_DEBUG — without having to change /proc/sys/kernel/printk. Use the ignore_loglevel toggle:

# Force all messages to the console regardless of level
echo Y | sudo tee /sys/module/printk/parameters/ignore_loglevel

# Restore normal loglevel filtering
echo N | sudo tee /sys/module/printk/parameters/ignore_loglevel

You can also enable this permanently by adding ignore_loglevel to your kernel boot parameters in GRUB.

Interview Questions & Answers

Q: What is the difference between pr_debug() and pr_devel()?

Both print at KERN_DEBUG and both are compiled out unless DEBUG is defined. The key difference is that pr_debug() is connected to the dynamic debug framework (CONFIG_DYNAMIC_DEBUG) — when dynamic debug is enabled, pr_debug() callsites are compiled in and can be toggled at runtime. pr_devel() is never connected to dynamic debug; it is always compiled out unless -DDEBUG is explicitly set, making it suitable for developer-only traces you never want to appear in any production system.

Q: How does dynamic debug achieve zero overhead when disabled?

Dynamic debug uses the kernel’s jump labels (static keys) feature. Each pr_debug() callsite is compiled as a NOP instruction by default. The kernel stores metadata about each callsite in the __dyndbg ELF section. When you enable a callsite via the control file, the kernel patches that NOP in memory with a branch instruction pointing to the debug print code. When disabled, it patches it back to a NOP. There is no branch that needs to be evaluated at runtime — the instruction itself changes.

Q: Why should device driver authors use dev_dbg() instead of pr_debug()?

dev_dbg() automatically prepends the driver name and device identity (bus address) to every message. When you have multiple instances of the same hardware (e.g. two USB devices using the same driver), dev_dbg() tells you exactly which one produced the message. pr_debug() has no device context — you would need to manually include the device name in every format string. Both use dynamic debug with the same runtime control mechanism.

Q: What is the difference between ccflags-y and CFLAGS_file.o in a kernel Makefile?

ccflags-y applies the compiler flags to all C files compiled in that Makefile. CFLAGS_filename.o applies the flags only to a single object file. For enabling debug in an entire module, use ccflags-y += -DDEBUG. To enable debug only in one source file of a multi-file module, use CFLAGS_thatfile.o := -DDEBUG. The old EXTRA_CFLAGS does the same as ccflags-y but is deprecated since kernel 2.6.24.

Q: How do you enable pr_debug() messages from a module’s init function, which runs at insmod time?

Writing to /sys/kernel/debug/dynamic_debug/control only works after the module is loaded — too late for init function callsites. The solution is to pass dyndbg as a module load parameter: modprobe mymodule dyndbg=+p. This processes the dynamic debug query before the module’s init function runs. For built-in (non-loadable) modules, add mymodule.dyndbg=+p to the kernel command line in GRUB.

Next: Rate Limiting & Avoiding Log Floods

Learn how to prevent your driver from flooding the kernel log when called at high frequency.

Next Chapter → Course Index

Leave a Reply

Your email address will not be published. Required fields are marked *