Debugging kernel code

Debugging application code helps you gain insight into the way code works and what is happening when it misbehaves and you can do the same with the kernel, with some limitations.

You can use kgdb for source level debugging, in a manner similar to remote debugging with gdbserver. There is also a self-hosted kernel debugger, kdb, that is handy for lighter weight tasks such as seeing if an instruction is executed and getting the backtrace to find out how it got there. Finally, there are kernel oops messages and panics, which tell you a lot about the cause of a kernel exception.

When looking at kernel code using a source debugger, you must remember that the kernel is a complex system, with real-time behaviors. Don't expect debugging to be as easy as it is for applications. Stepping through code that changes the memory mapping or switches context is likely to produce odd results.

kgdb is the name given to the kernel GDB stubs that have been part of mainline Linux for many years now. There is a user manual in the kernel DocBook and you can find an online version at https://www.kernel.org/doc/htmldocs/kgdb/index.html.

The widely supported way to connect to kgdb is over the serial interface, which is usually shared with the serial console, and so this implementation is called kgdboc, meaning kgdb over console. To work, it requires a platform tty driver that supports I/O polling instead of interrupts, since kgdb has to disable interrupts when communicating with GDB. A few platforms support kgdb over USB and there have been versions that work over Ethernet but, unfortunately, none of those have found their way into mainline Linux.

The same caveats about optimization and stack frames apply to the kernel, with the limitation that the kernel is written to assume an optimization level of at least -O1. You can override the kernel compile flags by setting KCGLAGS before running make.

These, then, are the kernel configuration options you will need for kernel debugging:

In addition to the uImage or zImage compressed kernel image, you will need the kernel image in ELF object format so that GDB can load the symbols into memory. That is the file called vmlinux that is generated in the directory where Linux is built. In the Yocto Project, you can request that a copy be included in the target image, which is convenient for this and other debug tasks. It is built into a package named kernel-vmlinux, which you can install like any other, for example by adding it to the IMAGE_INSTALL_append list. The file is put into the boot directory, with a name like this:

In Buildroot, you will find vmlinux in the directory where the kernel was built, which is in output/build/linux-<version string>/vmlinux.

The best way to show you how it works is with a simple example.

You need to tell kgdb which serial port to use, either through the kernel command line or at runtime via sysfs. For the first option, add kgdboc=<tty>,<baud rate> to the command line, as shown:

For the second option, boot the device up and write the terminal name to the /sys/module/kgdboc/parameters/kgdboc file, as shown:

Note that you cannot set the baud rate in this way. If it is the same tty as the console then it is set already, if not use stty or a similar program.

Now you can start GDB on the host, selecting the vmlinux file that matches the running kernel:

GDB loads the symbol table from vmlinux and waits for further input.

Next, close any terminal emulator that is attached to the console: you are about to use it for GDB and, if both are active at the same time, some of the debug strings might get corrupted.

Now, you can return to GDB and attempt to connect to kgdb. However, you will find that the response you get from target remote at this time is unhelpful:

The problem is that kgdb is not listening for a connection at this point. You need to interrupt the kernel before you can enter into an interactive GDB session with it. Unfortunately, just typing Ctrl + C in GDB, as you would with an application, does not work. You have to force a trap into the kernel by launching another shell on the target, via ssh, for example, and writing a g to /proc/sysrq-trigger on the target board:

The target stops dead at this point. Now you can connect to kgdb via the serial device at the host end of the cable:

At last, GDB is in charge. You can set breakpoints, examine variables, look at backtraces, and so on. As an example, set a break on sys_sync, as follows:

Now the target comes back to life. Typing sync on the target calls sys_sync and hits the breakpoint.

If you have finished the debug session and want to disable kgdboc, just set the kgdboc terminal to null:

The preceding example works in cases where the code you are interested in is executed when the system is fully booted. If you need to get in early, you can tell the kernel to wait during boot by adding kgdbwait to the command line, after the kgdboc option:

Now, when you boot, you will see this on the console:

At this point, you can close the console and connect from GDB in the usual way.

Debugging kernel modules presents an additional challenge because the code is relocated at runtime and so you need to find out at what address it resides. The information is presented via sysfs. The relocation addresses for each section of the module are stored in /sys/module/<module name>/sections. Note that, since ELF sections begin with a dot, '.', they appear as hidden files and you will have to use ls -a if you want to list them. The important ones are .text, .data, and .bss.

Take as an example a module named mbx:

Now you can use these numbers in GDB to load the symbol table for the module at those addresses:

Everything should now work as normal: you can set breakpoints and inspect global and local variables in the module just as you can in vmlinux:

Then, force the device driver to call mbx_write and it will hit the breakpoint:

Although kdb does not have the features of kgdb and GDB, it does have its uses and, being self-hosted, there are no external dependencies to worry about. kdb has a simple command-line interface which you can use on a serial console. You can use it to inspect memory, registers, process lists, dmesg, and even set breakpoints to stop in a certain location.

To configure kgd for access via a serial console, enable kgdb as shown previously and then enable this additional option:

Now, when you force the kernel to a trap, instead of entering into a GDB session, you will see the kdb shell on the console:

There are quite a few things you can do in the kdb shell. The help command will print all of the options. Here is an overview.

Getting information:

Breakpoints:

Inspect memory and registers:

Here is a quick example of setting a break point:

The kernel returns to life and the console shows the normal bash prompt. If you type sync, it hits the breakpoint and enters kdb again:

kdb is not a source debugger so you can't see the source code, or single step. However, you can display a backtrace using the bt command, which is useful to get an idea of program flow and call hierarchy.

When the kernel performs an invalid memory access or executes an illegal instruction, a kernel oops message is written to the kernel log. The most useful part of this is the backtrace, and I want to show you how to use the information there to locate the line of code that caused the fault. I will also address the problem of preserving oops messages if they cause the system to crash.

An oops message looks like this:

PC is at mbx_write+0x14/0x98 [mbx] tells you most of what you want to know: the last instruction was in the mbx_write function in a kernel module named mbx. Furthermore, it was at offset 0x14 bytes from the start of the function, which is 0x98 bytes long.

Next, take a look at the backtrace:

In this case, we don't learn much more, merely that mbx_write is called from the virtual filesystem code.

It would be very nice to find the line of code that relates to mbx_write+0x14, for which we can use objdump. We can see from objdump -S that mbx_write is at offset 0x8c in mbx.ko, so that last instruction executed is at 0x8c + 0x14 = 0xa0. Now, we just need to look at that offset and see what is there:

This shows the instruction where it stopped. The last line of code is shown here:

You can see that m has the type struct mbx_data *. Here is the place where that structure is defined:

So, it looks like the m variable is a null pointer, and that is causing the oops.

Decoding an oops is only possible if you can capture it in the first place. If the system crashes during boot before the console is enabled, or after a suspend, you won't see it. There are mechanisms to log kernel oops and messages to an MTD partition or to persistent memory, but here is a simple technique that works in many cases and needs little prior thought.

So long as the contents of memory are not corrupted during a reset (and usually they are not), you can reboot into the bootloader and use it to display memory. You need to know the location of the kernel log buffer, remembering that it is a simple ring buffer of text messages. The symbol is __log_buf. Look this up in System.map for the kernel:

Then, map that kernel logical address into a physical address that U-Boot can understand by subtracting PAGE_OFFSET, 0xc0000000, and adding the physical start of RAM, 0x80000000 on a BeagleBone, so c0f72428 – 0xc0000000 + 0x80000000 = 80f72428.

Then use the U-Boot md command to show the log: