compile kernel with debug symbols
Kernel Development
William Patterson  

Compile Kernel with Debug Symbols

You want clear backtraces and fast troubleshooting, so I show how to compile kernel with debug symbols and turn cryptic traces into actionable clues.

I’ve guided many engineers through building a linux kernel that keeps rich DWARF info, frame pointers, and module matches—so GDB and SystemTap find meaningful names instead of hex addresses.

We’ll cover exact config flags, two build paths (manual and Buildroot), and a quick QEMU/KVM loop to boot and attach a debugger. I point out architecture nuances and size vs. speed trade-offs.

By the end you’ll have a repeatable workflow to produce vmlinux, load module data at runtime, and verify your running linux image matches its debug info—so troubleshooting feels predictable, not painful.

Table of Contents

Key Takeaways

  • Builds with DWARF and frame pointers give readable backtraces for real-world debugging.
  • Two practical paths: manual source builds for control, Buildroot for automation.
  • Use a fast QEMU/KVM loop to boot and attach GDB for quick iteration.
  • Verify module and vmlinux matches to avoid missing info during probes.
  • Balance symbol size against performance—strip only what you can afford.

Why kernel debug symbols matter for Linux kernel debugging today

When addresses become names and lines, your troubleshooting shifts from guessing to solving. Enabling full symbol data turns raw addresses into function names, file:line and readable variables. That change makes every backtrace meaningful.

I recommend building with CONFIG_GDB_SCRIPTS enabled and leaving CONFIG_DEBUG_INFO_REDUCED off. When supported, enable CONFIG_FRAME_POINTER — it produces far clearer backtraces across architectures.

GDB auto-loads the Python helpers in vmlinux-gdb.py and gives us commands like lx-symbols and lx-dmesg. These helpers load module data, read kernel logs, and expose per-CPU state. They are hard to use without good symbol info.

Keep a symbolized vmlinux for every debug build as a default practice. It costs disk space, but saves hours during live triage. Tools such as SystemTap, perf, and BPF workflows also rely on precise mappings between addresses and source to resolve page offsets and types.

How to compile kernel with debug symbols on Linux

I’ll walk you through the practical steps to prepare toolchains and configs so your linux kernel build produces usable DWARF and module data.

Prerequisites: Install a recent GCC or Clang, make, and the usual build-essential packages. Lay out the kernel source tree in a single workspace so vmlinux, bzImage, and .ko modules build cleanly.

Enable the right options

Open menuconfig and enable CONFIG_DEBUG_KERNEL and CONFIG_DEBUG_INFO so DWARF lands in both the main image and every module. Turn on CONFIG_GDB_SCRIPTS and keep CONFIG_DEBUG_INFO_REDUCED off for full type data.

Frame pointers and notes

Where the architecture permits, enable CONFIG_FRAME_POINTER. Frame pointers stabilize backtraces and make stepping in GDB far easier during live sessions.

  • Standard outputs: vmlinux (uncompressed DWARF), bzImage (bootable), .ko modules (carry DWARF).
  • Verify cross-toolchains when targeting other arches — mismatches cause opaque failures.
  • Keep a debug defconfig as the default for iterative hacking and a lean release config for production.
ArtifactContainsUse
vmlinuxDWARF, typesAttach GDB, source-level traces
bzImageCompressed boot imageBoot VM or hardware
.ko modulesModule DWARFLoad runtime symbols

Expect larger files and longer links—those trade-offs cut troubleshooting time drastically when you can see names and lines instead of addresses.

Set up a debuggable target with QEMU/KVM and GDB

I start by launching a virtual target that exposes a GDB stub so I can inspect a running system instantly. This lets me pause the CPU, set breakpoints, and step at source level.

Boot options: QEMU can boot a build directly using -kernel and -append for fast iteration. When I need a full userspace and modules, I install the image inside a guest disk instead.

Starting QEMU and the GDB stub

I enable the stub with -s (TCP port 1234) so the VM waits for a debugger. I usually add KVM for speed, set memory size, and forward SSH ports for convenience.

debugging target

Attach GDB and load symbols

On the host I run gdb vmlinux, and if auto-load is restricted I add an auto-load safe path in ~/.gdbinit. Then I attach with target remote :1234.

After attach, I run lx-symbols so GDB loads module symbols as modules appear. Use lx-dmesg, $lx_current(), and $lx_per_cpu() to inspect live state.

  • I verify CONFIG_GDB_SCRIPTS is enabled and CONFIG_DEBUG_INFO_REDUCED is off so types are complete.
  • Common commands: continue, step, finish, and pending breakpoints for drivers that load later.
  • For repeatable work I boot from snapshot-capable images and restore known states between runs.

Using Buildroot to automate a debug-friendly kernel and rootfs

A single Buildroot run can create a full debugging target — toolchain, minimal userspace, and kernel artifacts ready for QEMU. I use this flow when I want a reproducible sandbox fast.

Start by running make qemu_x86_64_defconfig and then make menuconfig. Toggle BR2_ENABLE_DEBUG so every package keeps package-level symbols. Add OpenSSH and choose an ext4 root filesystem (BR2_TARGET_ROOTFS_EXT2_4) for convenient editing and logs.

Pick versions and kernel options

In Toolchain and Kernel menus set the linux version and matching headers so build and runtime agree. Open the linux menu and enable CONFIG_DEBUG_KERNEL, DEBUG_INFO, and FRAME_POINTER to get a vmlinux with full DWARF, a bzImage, and module artifacts that GDB can use.

Key outputs and next steps

After make finishes, you’ll find the source under output/build/linux-…. The raw image is output/build/linux-…/vmlinux. Compressed boot images sit in output/images/bzImage and the rootfs at output/images/rootfs.ext4.

ArtifactPathUse
vmlinuxoutput/build/linux-…/vmlinuxAttach GDB for source-level traces
bzImageoutput/images/bzImageBoot under QEMU
rootfs.ext4output/images/rootfs.ext4SSH into the guest for testing

Boot QEMU using -s to expose gdbserver on port 1234 and add networking flags: -net nic,model=virtio -net user,hostfwd=tcp::5555-:22. SSH to host port 5555 to run commands and iterate. Use QEMU savevm/loadvm via the monitor socket to snapshot states during iterative hacking.

Troubleshooting missing module debuginfo and SystemTap warnings

I often see SystemTap print messages like cannot find module nfs debuginfo. Those warnings usually mean either the modules lack DWARF or the tool cannot locate installed symbols.

Start by confirming the build enabled CONFIG_DEBUG_INFO for both the core image and modules. If modules were built without DWARF, SystemTap and GDB cannot resolve names or types during debugging.

Ensuring modules are built with DWARF and installed symbols are findable

  • Check module files on disk with readelf -wi module.ko to see if DWARF sections exist.
  • Verify the install path used by your build matches where tools search for symbol files.
  • If the .ko lacks DWARF, rebuild using a default debug-friendly config that enables module info.

Matching running kernel, module versions, and symbol paths

Even a minor version mismatch will stop symbol resolution. I check the running version via uname -r and compare it to the built artifacts’ version string.

For GDB, I point vmlinux at the same build and run lx-symbols so module name matches load paths. After aligning versions and paths, SystemTap warnings usually vanish and my debugging becomes predictable again.

SymptomLikely causeQuick action
“cannot find module nfs debuginfo”Module built without DWARF or wrong install pathRun readelf; ensure CONFIG_DEBUG_INFO; reinstall module to expected path
SystemTap finds wrong typesVersion mismatch between running image and artifactsCompare uname -r to build output; rebuild or use matching vmlinux
GDB fails to load moduleTool can’t find symbol files by name/pathUse lx-symbols and point GDB to the module directory

Ship-ready checks and performance-minded optimizations

I keep release builds lean and retain a full offline vmlinux and symbol archive for postmortems. This splits a production kernel that’s stripped for speed from the artifacts we need to decode crashes later.

I review config options and module lists to avoid shipping costly debug-only features. Frame pointers and heavy tracing get toggled off if performance is tight.

For incident response I store the exact vmlinux, System.map, and a symbol archive per build target and linux kernel version. That makes replaying traces and matching addresses fast.

When storage matters, I use split DWARF or external debug packages so runtime stays slim and developers keep full data offline.

I automate this in CI: a default artifact set, a QEMU boot smoke-test, and a simple GDB attach. That keeps our security posture solid while making kernel debugging one command away during real incidents.

FAQ

Why do I need debug symbols for the Linux kernel?

Debug symbols provide human-readable names, file paths, and line numbers that GDB and other tools use to map binary code back to source. That makes tracking crashes, backtraces, and logic errors far easier — especially when diagnosing oopses, panics, or module faults. Without them you get raw addresses and guessed stack frames, which slows root cause analysis.

What prerequisites do I need before attempting a debug-friendly build?

Prepare a proper toolchain (GCC, binutils), build tools (make, bc, libncurses-dev if using menuconfig), and the full kernel source tree. Keep matching kernel headers and version control of your source. For cross-target work, install the right cross-compiler and sysroot. Also ensure sufficient disk space — DWARF data inflates artifacts.

Which kernel config options matter most for debugging?

Enable DEBUG_INFO or DEBUG_INFO_SUPPORT to include DWARF, turn off options that strip symbols, and enable CONFIG_DEBUG_KERNEL for extra checks. Keep CONFIG_DEBUG_INFO_REDUCED disabled if you want full info. Also consider CONFIG_GDB_SCRIPTS for easier symbol handling. Balance options against size and performance trade-offs.

Should I enable frame pointers and what about architecture specifics?

Enabling frame pointers preserves a reliable call chain for profilers and post-mortem tools — useful on architectures where unwind tables are incomplete. Some architectures have their own unwind mechanisms; follow architecture docs. Frame pointers add small overhead but improve stack traces significantly.

How do I produce vmlinux, bzImage, and modules that include DWARF symbols?

Build the kernel image and modules from the same source with DEBUG_INFO enabled. vmlinux typically holds full symbols; bzImage can be generated for boot but often is stripped — keep an unstripped vmlinux for debugging. Also build modules with config options that prevent stripping and install their .debug info where GDB can find it.

What are the size, performance, and build-time trade-offs?

Including full symbols increases binary size and disk use, slows link time, and may cost runtime checks if debugging kernels are enabled. For iterative work, use a debug build for development and a smaller, optimized build for production. Consider DEBUG_INFO_REDUCED to limit size when full symbols are unnecessary.

How do I set up a debuggable VM target with QEMU/KVM and GDB?

Boot the debug-friendly image in QEMU using -kernel/-append for fast iteration or install the build into a guest for integration tests. Start QEMU with -s or -gdb to expose a GDB stub. Then connect GDB with target remote :1234 and load your unstripped vmlinux and module debug files so symbols resolve locally.

What runtime QEMU flags are most helpful for kernel hacking?

Use -s (shortcut for -gdb tcp::1234) and -S to pause CPU at startup. Enable serial console redirection (-nographic or -serial) for kernel logs. Add device passthrough or virtio for realistic I/O. Snapshots and backing files accelerate iterative testing.

How do I load kernel and module symbols into GDB?

Point GDB at the unstripped vmlinux using file /path/to/vmlinux, then use add-symbol-file or sharedlibrary to load module symbols, or use scripts like lx-symbols to map modules. Ensure symbol paths match the exact build; mismatches produce incorrect frame names and line numbers.

Are there helper scripts that simplify kernel debugging in GDB?

Yes — the Linux source includes GDB scripts that add commands for printk decoding, symbol lookup, and stack walking. CONFIG_GDB_SCRIPTS enables installation of helper files. These scripts speed common tasks like translating addresses and dumping task state.

How can Buildroot help automate a debug-friendly kernel and rootfs?

Buildroot can build the kernel, toolchain, and root filesystem with debug symbols enabled. Configure it to keep debuginfo for packages and kernel, select the kernel version and headers that match your target, and produce images (vmlinux, bzImage, rootfs.ext4) ready for QEMU testing.

What Buildroot kernel options support debugging and iterative hacking?

In Buildroot’s kernel hacking menu, enable DEBUG_INFO/DEBUG_KERNEL and FRAME_POINTER as needed. Configure package rebuilds to include symbols and enable SSH in the rootfs for remote access. This setup makes iterative testing with QEMU quicker and more productive.

Why do I see missing module debuginfo or SystemTap warnings?

Those warnings usually mean modules were built or installed without DWARF info, or the running kernel version differs from your debug files. Ensure modules are built with debugging enabled, keep an unstripped copy of module .o or .debug files, and point tools to the correct symbol paths matching the runtime kernel.

How do I ensure module debuginfo is findable at runtime?

Install module debug files into a known directory and configure debuglink paths, or use GDB’s set debug-file-directory to point to .debug files. For SystemTap and perf, install separate debuginfo packages and keep versioning tight — build the modules and debug info from the same exact source tree and config.

What checks should I run before shipping a production kernel?

Strip unneeded symbols and test performance impact of debug options. Verify that all security and stability patches are applied, and confirm matching headers and modules across releases. For production, keep a separate debug archive — an unstripped vmlinux and module debuginfo stored securely for post-release analysis.