Remote Debugging of Loadable Kernel Modules with kgdb: a Knowledge-based Article for Getting Started

Lamphere describes a straightforward technique that allows kernel debugging in the safety of user space.
How the Communication Works

In order for us to use kgdb to debug loadable modules, we must understand how the remote kernel communicates with gdb on the development machine. Remember, the mechanism's default communication between gdb on the development machine and the debugging stub on the target machine transpires via a serial driver interface (called gdbserial.o). In order for the debugging process to begin, two things must happen from within this driver. First, the set_debug_traps( ) is initiated. This function is defined in the debugging stub and informs the remote kernel that all breakpoints, error conditions and other exception handling is to be intercepted and handled by gdb. Secondly, the serial driver must call the function breakpoint( ). This function is also defined in the debugging stub and is used to initiate the communication by issuing a breakpoint interrupt:

asm( " int $3");

Since gdb is now configured to intercept such a condition (i.e., the set_debug_traps( ) call), the kernel on the target machine halts and transfers control to gdb on the development machine. It is from this point that the user may begin normal debugging such as single-stepping, issuing of breakpoints, stack tracing, etc. However, if we were to begin stepping from this initial point, the code we would be examining would be in gdbserial immediately following the call to the debugging stub's breakpoint( ) (since this is where program execution has halted).

For example, Listing 2 is the excerpt of gdbserial in which the two calls to the previously explained stub functions are called.

Listing 2. An Excerpt of gdbserial

Now, if we initiate another debug session and issue a step command after gdb receives the stub's breakpoint interrupt, we would step to the next line of code in gdbserial after the breakpoint( ) is made (which should be gdb_null in the example in Listing 3).

Listing 3. Sample gdb Dialogue with Breakingpoints

As was mentioned before, gdb can return the remote kernel to a running state by issuing a continue command. If this is done, gdb patiently waits until the remote kernel returns control by issuing some sort of exception (such as a user-defined breakpoint, segmentation fault, etc.).

For a better understanding of how the entire process works review the debugging stub code found in /usr/src/linux/arch/i386/kernel/gdbstub.c and the serial driver interface, which can be found in /usr/src/linux/drivers/char/gdbserial.c.

Preparing to Debug a Loadable Module on the Target Machine

We now have almost all the information we need in order begin using the kgdb mechanism to debug loadable modules. Remember, the important information we must retain from kgdb's communication process in order to initiate module debugging is: 1) at the beginning of the debug session, the debugging stub informs gdb on the development machine that it is responsible for intercepting and handling all exceptions from the remote target kernel for example, gdbserial's initial call to set_debug"traps( ); 2) the initial debug process begins with the serial interface's call to the debugging stub's breakpoint( ) function; 3) gdb can return the remote kernel to a running state but will regain control once the remote kernel issues any type of exception.

Additionally, we must consider that because the module will be loaded on the target machine and the gdb session runs on another machine, gdb will have no idea where in the target machine's memory the module code will be loaded. We must therefore determine this location and inform gdb of its whereabouts before the debugging of the module can begin.

Locating Modularized Code in Memory

During the kernel-building process, the kernel produces a file that maps addresses in memory to function names for the modules/drivers that are built during the compile process. This file is usually placed in the root of the kernel source directory (i.e., /usr/src/linux/ and is used by the kernel to access those compiled devices properly in the correct memory location. However, at the construction of that file the kernel is unaware of where in memory a particular module may be loaded at a later time.

Fortunately, we can determine the memory location for modularized code during its load process. This is accomplished by using insmod with the -m parameter that informs insmod to produce a load map. This map informs us of where in memory the object-code sections reside. Ultimately, we must locate this information in order to inform gdb on our development machine of where our module's object code resides on the target machine. To illustrate, let's consider the module code shown in Listing 4, which we will refer to as the simple module.

Listing 4. Simple Module

As you can see, the simple module contains just enough functionality to to identify its memory address on the target machine when loaded.

  • On the development machine compile the module:

gcc -c -O2 -g simple.c

Be sure to include the -g option during compilation in order to enable debugging symbols for gdb.

  • Copy the compiled object code to the target machine.

  • Install the object code into the kernel using insmod's -m parameter:

insmod -m simple.o
This, of course, loads the module and produces a similar load map as shown in Listing 5.

Listing 5. Memory Map of Kernel Module Size and Location

  • Record the hex address of the .text section (0xc480004c in our example) for use later. This section represents the beginning of the module code in memory. We will use this value to inform the development machine running gdb of the module's whereabouts in memory.

  • Unload the module:

rmmod simple