前四节介绍如何定制自己的 linux 内核,以及如何利用 busybox 制作文件系统,如何使用 qemu 模拟器运行编译好的 linux 内核。上一节介绍了在没有界面和鼠标的 linux 环境下,如何使用 gdb 工具单步调试程序。linux 内核也可以看作是一个程序,所以本节将尝试使用 gdb 单步调试 linux 内核。
记得第二节在 linux 内核源码中添加了一行打印自己名字的代码,内核也确实在启动时打印了名字,不过这对于调试 linux 内核源码来说,还是不够过瘾,能够一行一行的单步调试 linux 内核代码才叫酷。
单步调试 linux 内核源码的方法
可是,怎样单步调试 linux 内核呢?要知道,单步调试需要操作系统的支持,而要是想单步调试,linux 系统就不能完全启动,这就有点像想拉着自己头发,把自己提起来的感觉。的确,这种方法比较难实现,不过编译好的linux内核不是使用 qemu 模拟运行的吗?qemu 是运行在我自己的 ubuntu (下文称为宿主机)上的,那么使用宿主机来调试 qemu 模拟运行的 linux 内核就是一个不错的选择。
gdb 支持远程调试,恰好 qemu 也支持,那调试方法就清晰了:先用 qemu 加载 linux 内核,留出远程调试接口,然后在宿主机上使用 gdb 工具连接 qemu 留出的调试接口,这样就能在宿主机上使用 gdb 调试 qemu 中模拟运行的 linux 内核了。
qemu 在模拟运行 linux 内核时,加上 -gdb tcp::1234 -S 参数:
qemu-system-x86_64 \
-m 512M \
-smp 1 \
-kernel ./bzImage \
-drive format=raw,file=./disk.raw \
-append "init=/linuxrc root=/dev/hda" \
-curses \
-gdb tcp::1234 -S
执行后,linux 内核不会启动,而是会停在下图这个界面,等待 gdb 远程连接。
接下来,在宿主机再开一个终端,使用 gdb工具远程连接 qemu:
# gdb linux-2.6.26/vmlinux
...
Reading symbols from linux-2.6.26/vmlinux...done.
(gdb) target remote:1234
(gdb) b start_kernel
Breakpoint 1 at 0xffffffff807dba50: file init/main.c, line 536.
不同于一般的 C 程序,linux 内核的入口函数不是 main 函数,而是 start_kernel 函数,即 linux 内核从 start_kernel 函数开始运行,所以我们在此处下断点。然后,输入 c ,会发现另一个终端里的 linux 内核启动了,
(gdb) c
Continuing.
Remote 'g' packet reply is too long: 2090...
(gdb)
但是,gdb 好像没有正常工作,输出一大堆混乱的字符,如下图。
解决 gdb 工具调试 linux 内核时的不正常
gdb 为什么不正常工作呢?既然 gdb 是开源的,那问谁都不如问源码,于是下载了 v8.0 版本的 gdb 源码,解压,搜索“Remote 'g' packet reply is too long”,应该能够发现出现这种现象的原因。
如何下载 gdb 源码就不赘述了,不会下载的朋友在评论区留言,我发过去。
果然,还真搜到了。
# tar xf gdb-8.0.tar.xz
# grep -r "Remote 'g' packet reply is too long"
gdb/po/gdb.pot:msgid "Remote 'g' packet reply is too long: %s"
gdb/remote.c: error (_("Remote 'g' packet reply is too long: %s"), rs->buf);
打开 gdb/remote.c,找到报错代码段:
看代码字面意思应该是 gdb 接收到的缓冲长度大于 rsa 包长的两倍了,因为现在重点是调试 linux 内核,所以暂时不细究 gdb 的这个问题。我们尝试忽略这个错误,将这段代码改掉试试。怎么改呢?继续往下看,发现 buf_len < 2 * rsa->sizeof_g_packet 的处理方法,如下图:
我们试试将报错代码也做类似修改:
保存,再编译一个 gdb 工具出来:
# mkdir ../install # 安装在此
# ./configure --prefix=<你的install绝对路径>
# make
# make install
因为宿主机自带的有 gdb 工具,所以为了不与原 gdb 冲突,我们将新的 gdb 工具安装在 install 目录,安装完毕后,我们再使用新的 gdb 工具调试内核试试。
使用修复错误的 gdb 工具调试 linux 内核
使用新编译好的 gdb 工具,重复上面的步骤:
# ./install/bin/gdb ../linux-2.6.26/vmlinux
...
Reading symbols from ../linux-2.6.26/vmlinux...done.
(gdb) target remote :1234
Remote debugging using :1234
(gdb) b start_kernel
Breakpoint 1 at 0xffffffff807dba50: file init/main.c, line 536.
(gdb) c
输入 c,gdb 果然没有继续报错,而是停在了 start_kernel 处,如下图:
输入 l 可以查看断点附近代码,我们输入 n,单步调试内核:
(gdb) n
540 smp_setup_processor_id();
(gdb) n
547 lockdep_init();
(gdb) n
551 local_irq_disable();
(gdb) n
559 lock_kernel();
(gdb) n
560 tick_init();
(gdb) n
561 boot_cpu_init();
(gdb) n
563 printk(KERN_NOTICE);
(gdb) n
567 setup_command_line(command_line);
...
发现 start_kernel 函数里其实就是很多模块的 init,此时虽然有 printk 函数,但是终端依然还没有信息被打印出来:
继续输入 n 单步调试,发现原来所有信息都需要 console_init 函数执行后才会被打印出来,而且我们之前添加在内核中的信息,也被打印出来了。
好了,到这里相信朋友们应该知道如何使用 gdb 单步调试 linux 内核了,以后我们一起学习 linux 内核的其他设计和实现时,也可以使用单步调试的手段来学习了。