Qemu and the Kernel
Debugger
ByDebugging the kernel of a running operating system has always been tricky, but now the Qemu emulator supports cross-platform kernel and module debugging at the programming language level.
Some of the basic operations that a debugger supports include freezing code sequences and subsequently analyzing memory content. If the code sequences belong to an application, debugging is comparatively unproblematic, but if you freeze the kernel itself, you don’t have a run-time environment that accepts keyboard input, outputs data to the monitor, accesses memory content, or continues running the kernel later on. You could almost compare kernel debugging with trying to operate on yourself.
From a technical point of view, this problem is solved by offloading complex functions to a second system, which will typically have working memory and file management and help you search the source code for variables, data structures, functions, and lines of code. This means you only need a debug server for the kernel that you want to debug; the server can execute simple commands, such as reading or writing memory cells or setting breakpoints, on the system under investigation.
The Qemu emulator has a built-in debug server (see the “Kernel Debugging Variants” boxout). If you also use the Buildroot system generator, kernel debugging is comparatively simple to implement. The precondition for doing so is having a kernel with symbol information. This isn’t an issue thanks to Buildroot: Within a short time, the tool can give you a clear-cut userspace and a lean kernel that you can quickly reconfigure and modify.
All of the steps in this approach are shown in the “Brief Guide to Buildroot” boxout. You can start by downloading Buildroot and unpacking the archive. Then, create the default configuration – preferably for an x86 system – by typing make qemu_x86_defconfig. You need to modify four options for the ensuing make menu-config: In Toolchain, enable the Build gdb for the Host option; in Kernel | Kernel version, type 3.2; in System Configuration | Port to run getty (login prompt) on, enter tty1; and for Build options | Number of jobs to run simultaneously, enter the number of cores in the generator machine.
Another make triggers the first generator run. This will create the kernel sources, among other things, but you will need to modify the configuration again for kernel debugging some time later.
To do so, run make linux--menuconfig in the root directory of Buildroot. The relevant options in the menu that is subsequently displayed are located below the Kernel Hacking item (Figure 2).
The Kernel debugging and Compile the kernel with debug info options are needed here. Another call to make generates a kernel with the modified configuration.
The results of this are basically two files: You will have a vmlinux that contains both the code and the corresponding debug information in the directory of the kernel source code. The architecture subdirectory – this is arch/x86/boot/ for the x86 platform – contains the compressed kernel in bzImage. Other platforms might call the kernel zImage. Bootloaders such as GRUB need the compressed kernel (bzImage). The debugger itself also needs the kernel image but will use the uncompressed counterpart vmlinux, which contains the debug info. Of course, the debugger also needs access to the source code.
Once you have generated the kernel and the root filesystem with Build-root, you should first test both without debugging:
qemu -kernel output/images/bzImage -hda output/images/rootfs.ext2 -append "root=/dev/sda rw"
If everything works, you can start debugging by appending -s and -S:
qemu -kernel output/images/bzImage -hda output/images/rootfs.ext2 -append "root=/dev/sda rw" -s -S
The -s option launches the debug server (gdbserver), and -S stops the kernel at the outset.
Page Change
To help the GNU debugger (GDB) find the kernel C and header files, launch the tool in the Linux kernel source code directory (Figure 3).
If you built an x86 system, you can deploy the GNU debugger preinstalled on the developer system; otherwise, use the GDB built for the host system, which resides in the Buildroot output/host/usr/bin/ directory:
cd output/build/linux-3.2/ gdb
The gdb command opens the debugger session. To start, load the kernel code and symbols with: file vmlinux. If you see a no debug-symbols found message, you need to check the debug options in your kernel configuration and possibly rebuild the kernel. Including the symbols, vmlinux weighs in at more than 40MB.
Next, open a connection to the debug server by typing target remote :1234 (Figure 3). The gdb command then handles the further course of execution (Table 1). The continue command enables the Linux guest system, and pressing Ctrl+C interrupts execution.
Figure 3 shows the GDB command break vfs_mknod setting a breakpoint for the vfs_mknod function; for kernel 3.2, use sys_mknod instead because of changes in the Linux kernel. When a user on the Linux system runs the mknod /dev/hello c 254 0 command, execution stops, and you can inspect the variables. To continue program execution, enter the continue command. To isolate the debugger from the Linux system, first press Ctrl+C, then issue the GDB detach command to interrupt the connection to the server; quit terminates the debugger.
To the Modules …
Kernel modules can also be debugged at the programming language level with Qemu, but to do this, you need to activate Enable loadable module support in the submenu Module unloading. In other words, this process means reconfiguring and regenerating the kernel that you created with Build-root. Because it is also impossible to predict the module code’s address in main memory, you need to load the module, find the address, and tell the debugger. You need to check the /sys filesystem entries to do so, but more on that later.
Before debugging, first generate the module for the kernel created with Buildroot. The easiest way to do this is use a modified Makefile by pointing the KDIR variable to the path with the kernel sources you are using, which will be below the Buildroot root directory in this case. If the Linux system in Qemu that you are debugging is not designed for an x86 architecture, you need to set the CROSS_COMPILE and ARCH environment variables.
To debug the module, you need to have Listing 1 as your Makefile and Listing 2 as hello.c in a separate folder below the Buildroot root directory.
Listing 1: Makefile
01 ifneq ($(KERNELRELEASE),) 02 obj-m := hello.o 03 04 else 05 PWD := $(shell pwd) 06 KDIR := ~/buildroot-2011.08/output/build/linux-3.1/ 07 08 default: 09 $(MAKE) -C $(KDIR) M=$(PWD) modules 10 endif 11 12 clean: 13 rm -rf *.ko *.o *.mod.c *.mod.o modules.order 14 rm -rf Module.symvers .*.cmd .tmp_versions
Listing 2: Module hello.c
01 #include <linux/fs.h> 02 #include <linux/cdev.h> 03 #include <linux/device.h> 04 #include <linux/module.h> 05 #include <asm/uaccess.h> 06 07 static char hello_world[]="Hello World\n"; 08 static dev_t hello_dev_number; 09 static struct cdev *driver_object; 10 static struct class *hello_class; 11 static struct device *hello_dev; 12 13 static ssize_t driver_read( struct file *instanz,char __user *user, size_t count, loff_t *offset ) 14 { 15 unsigned long not_copied, to_copy; 16 17 to_copy = min( count, strlen(hello_world)+1 ); 18 not_copied=copy_to_user(user,hello_world,to_copy); 19 return to_copy-not_copied; 20 } 21 22 static struct file_operations fops = { 23 .owner= THIS_MODULE, 24 .read= driver_read, 25 }; 26 27 static int __init mod_init( void ) 28 { 29 if (alloc_chrdev_region(&hello_dev_number,0,1,"Hello")<0) 30 return -EIO; 31 driver_object = cdev_alloc(); 32 if (driver_object==NULL) 33 goto free_device_number; 34 driver_object->owner = THIS_MODULE; 35 driver_object->ops = &fops; 36 if (cdev_add(driver_object,hello_dev_number,1)) 37 goto free_cdev; 38 hello_class = class_create( THIS_MODULE, "Hello" ); 39 if (IS_ERR( hello_class )) { 40 pr_err( "hello: no udev support\n"); 41 goto free_cdev; 42 } 43 hello_dev = device_create( hello_class, NULL, hello_dev_number, NULL, "%s", "hello" ); 44 return 0; 45 free_cdev: 46 kobject_put( &driver_object->kobj ); 47 free_device_number: 48 unregister_chrdev_region( hello_dev_number, 1 ); 49 return -EIO; 50 } 51 52 static void __exit mod_exit( void ) 53 { 54 device_destroy( hello_class, hello_dev_number ); 55 class_destroy( hello_class ); 56 cdev_del( driver_object ); 57 unregister_chrdev_region( hello_dev_number, 1 ); 58 return; 59 } 60 61 module_init( mod_init ); 62 module_exit( mod_exit ); 63 MODULE_LICENSE("GPL");
Also, you might need to modify the path to the Linux sources in the KDIR variable. The modified Makefile builds the hello.ko module, which you can then copy to the root filesystem using, for example:
cp hello.ko ../output/target/root/
A call to make in the Buildroot directory regenerates the root filesystem. This puts the module in the superuser’s home directory after booting. The easiest approach is to omit the -S option but use -s when you call Qemu. This enables the debug server, but Linux still boots directly. After logging in as root, you can load the module by issuing the command -insmod hello.ko (Figure 4).
The following commands determine addresses for the code segment and the two data segments:
# cat /sys/module/hello/sections/.text # cat /sys/module/hello/sections/.data # cat /sys/module/hello/sections/.bss
Because the Linux system generated by Buildroot doesn’t include udev support, you also need to issue the mknod /dev/hello c 254 0 command to create the device file, which an application would use to access the driver in this example. You can discover whether the system uses the major number 254 by typing
cat /proc/devices | grep Hello
Next, launch GDB on the host system in the normal way from the Linux kernel source directory. After you type the file vmlinux and target remote :1234 commands, Qemu stops the Linux system. The following command tells the system the hex address of the code segment and the two data segments:
add-symbol-filepath/hello.ko 0xd8817000 -s .data 0xd88170e0 -s .bss 0xd8817294
Now you can set a breakpoint, for example, for the driver_read function. The continue command tells the Linux system to go back to work.
If you now enter the cat /dev/hello command in the terminal of the system you are debugging, driver_read is enabled and GDB stops the kernel at the breakpoint you set previously. You can now investigate the module’s memory cells and step through its process.
Clever
It can be confusing to see the debugger jump about between the lines of code, seemingly without any motivation. Access to the local variables explains the reason for this: value optimized out – the compiler has optimized the kernel code. This means that some of the defined variables are invisible in the debugger; code fragments have been remodeled. Because a number of macros are in use in the kernel, troubleshooting doesn’t become any easier, either. In many cases, a variable turns out to be a clever macro.
Kernel debugging thus continues to be a challenge that you need to face with much patience and practice.
next page » 1 2
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters
Support Our Work
Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.
News
-
The Gnome Foundation Struggling to Stay Afloat
The foundation behind the Gnome desktop environment is having to go through some serious belt-tightening due to continued financial problems.
-
Thousands of Linux Servers Infected with Stealth Malware Since 2021
Perfctl is capable of remaining undetected, which makes it dangerous and hard to mitigate.
-
Halcyon Creates Anti-Ransomware Protection for Linux
As more Linux systems are targeted by ransomware, Halcyon is stepping up its protection.
-
Valve and Arch Linux Announce Collaboration
Valve and Arch have come together for two projects that will have a serious impact on the Linux distribution.
-
Hacker Successfully Runs Linux on a CPU from the Early ‘70s
From the office of "Look what I can do," Dmitry Grinberg was able to get Linux running on a processor that was created in 1971.
-
OSI and LPI Form Strategic Alliance
With a goal of strengthening Linux and open source communities, this new alliance aims to nurture the growth of more highly skilled professionals.
-
Fedora 41 Beta Available with Some Interesting Additions
If you're a Fedora fan, you'll be excited to hear the beta version of the latest release is now available for testing and includes plenty of updates.
-
AlmaLinux Unveils New Hardware Certification Process
The AlmaLinux Hardware Certification Program run by the Certification Special Interest Group (SIG) aims to ensure seamless compatibility between AlmaLinux and a wide range of hardware configurations.
-
Wind River Introduces eLxr Pro Linux Solution
eLxr Pro offers an end-to-end Linux solution backed by expert commercial support.
-
Juno Tab 3 Launches with Ubuntu 24.04
Anyone looking for a full-blown Linux tablet need look no further. Juno has released the Tab 3.