Linux 内核调试5-UML和Qemu调试模块


这次来看如何调试内核模块,也就是驱动程序,模块的调试跟普通程序略有不同,不论是内核还是普通应用程序,在连接之后便以得知代码将要加载的位置,用户态程序有虚拟地址映射机制,而内核独占物理内存。内核运行与共享的内核地址空间,所以不能使用相同的线性地址,只能由内核加载模块时指定起始地址,模块中都以此为偏移运行。所以内核的调试不能使用普通的方式,需要知道模块的加载地址。

而且Qemu的调试原理与UML相似,也可用相同的方法进行模块的调试,这里仅以UML模块调试举例

首先需要完成一个内核模块,这里可以照搬之前的一篇日志 Linux 内核调试 ,保存源文件为go.c,完成Makefile和编译工作,内核源码以编译UML的内核为准:

# Makefile 内容
obj-m := go.o

# 编译命令
[cpp@dark go]$ make -C /home/cpp/fox/linux-2.6.36 M=$PWD modules ARCH=um

接下来启动带网络的UML,拷贝go.ko模块文件到UML虚拟机中,之后启动GDB:

# 查看 linux 进程 ID
[cpp@dark linux-2.6.36]$ ps -A | grep linux
 7333 pts/0    00:00:38 linux
 7340 pts/0    00:00:00 linux
 7341 pts/0    00:00:00 linux
 7342 pts/0    00:00:00 linux
 7343 pts/0    00:00:00 linux
 7481 pts/0    00:00:00 linux
 7546 pts/0    00:00:00 linux
 9864 pts/0    00:00:00 linux
 9866 pts/0    00:00:00 linux
 9868 pts/0    00:00:00 linux

# 启动 GDB
[cpp@dark linux-2.6.36]$ gdb linux
GNU gdb (GDB) Fedora (7.1-18.fc13)
Copyright (C) 2010 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-RedHat-linux-gnu".
For bug reporting instructions, please see:...
Reading symbols from /home/cpp/fox/linux-2.6.36/linux...done.

# attach 到 linux 进程,attach第一个进程
(gdb) attach 7333

# 忽略段错误
(gdb) handle SIGSEGV pass nostop noprint

# 对 sys_init_module 设置断点,截获模块加载事件
(gdb) br sys_init_module
Breakpoint 1 at 0x60052421: file kernel/module.c, line 2696.

# 继续执行
(gdb) c
Continuing.

# 在虚拟机中加载 go.ko 模块
localhost ~ # insmod go.ko

# 主机GDB截获断点
Breakpoint 1, sys_init_module (umod=0x603030, len=124282,
    uargs=0x603010) at kernel/module.c:2696
2696 if (!capable(CAP_SYS_MODULE) || modules_disabled)
(gdb)
# 模块加载 load_module 调用结果
(gdb) n 2691 {
(gdb) n 2696 if (!capable(CAP_SYS_MODULE) || modules_disabled)
(gdb) n 2700 mod = load_module(umod, len, uargs);
(gdb) n 2701 if (IS_ERR(mod))
(gdb) print mod
$1 = (struct module *) 0x628482b0

# 从 include/linux/module.h 查看 module.h结构
# struct module_sect_attrs *sect_attrs; 为模块的段属性
# 查看模块名
(gdb) print mod->name
$2 = "go", '\000'
# 查看模块段个数
(gdb) print mod->sect_attrs->nsections
$3 = 11
# 查看第一个段名
(gdb) print mod->sect_attrs->attrs[0]->name
$4 = 0x61daa3a0 ".note.gnu.build-id"
# 找到 .text 段,并显示基址
(gdb) print mod->sect_attrs->attrs[1]->name
$5 = 0x61daa3c0 ".text"
(gdb) print /x mod->sect_attrs->attrs[1]->address
$6 = 0x62848000
# 加载符号,使用 GDB 的 add-symbol-file 命令加载模块符号到基址
(gdb) add-symbol-file go/go.ko 0x62848000 add symbol table from file "go/go.ko" at .text_addr = 0x62848000 (y or n) y Reading symbols from /home/cpp/fox/linux-2.6.36/go/go.ko...done.

# 设置模块断点
(gdb) br simple_read Breakpoint 2 at 0x62848028: file /home/cpp/fox/linux-2.6.36/go/go.c, line 21.
# 继续运行
(gdb) c Continuing.

# UML 虚拟机中操作模块 localhost ~
# cat /dev/simple

# 主机断点生效 Breakpoint 2, simple_read (pfile=0x61c74180, buf=0x60d000
, size=32768, ppos=0x61e0bec0) at /home/cpp/fox/linux-2.6.36/go/go.c:21 21 { (gdb) # 查看信息并调试 (gdb) n 22 if (copy_to_user(buf, “test data\n”, 10)) (gdb)

当然加载符号时还可以查看 .data 段和 .bss 段的基址,加载go.ko时同时可以设置二者的基址,以便可以调试全局变量等等,初看起来这个过程比较复杂,其实就是通过调试内核加载位置来确定模块最终将要加载的基址,而后通过机制加载模块符号。

这个复杂的过程如果每次都需要手动处理是非常烦人的一件事,好在GDB本身有脚本扩展(甚至可执行Python脚本),来简化这个过程,这里来试着写一个简单的打印模块section名字和基址的脚本。

GDB脚本运行可以由两种方式,一种是在GDB启动时,在当前目录查找.gdbinit文件解释执行,另一种在GDB运行期间使用 source script-file 命令来执行,脚本的说明可查阅文档,以下是简单的打印段信息的脚本:

define modsec
        set $index = 0
        while $index < mod->sect_attrs->nsections
                printf "Name\t%s\tAddress\t0x%x\n", mod->sect_attrs->attrs[$index]->name, mod->sect_attrs->attrs[$index]->address
                set $index = $index + 1
        end
end

以下是加载并执行脚本效果:

# 加载脚本
(gdb) source modsec
(gdb) c
Continuing.

# 在虚拟机中加载 go.ko 模块
# 并单步执行到 mod = load_module(umod, len, uargs);
Breakpoint 1, sys_init_module (umod=0x603030, len=124282,
    uargs=0x603010) at kernel/module.c:2696
2696 if (!capable(CAP_SYS_MODULE) || modules_disabled)
(gdb) n
2691 {
(gdb) n
2696 if (!capable(CAP_SYS_MODULE) || modules_disabled)
(gdb) n
2700 mod = load_module(umod, len, uargs);
(gdb) n
2701 if (IS_ERR(mod))
# 调用定义命令打印段
(gdb) modsec
Name .note.gnu.build-id Address 0x6286f09c
Name .text Address 0x6286f000
Name .exit.text Address 0x6286f050
Name .init.text Address 0x62872000
Name .rodata.str1.1 Address 0x6286f0c0
Name .eh_frame Address 0x6286f118
Name .data Address 0x6286f1e0
Name .gnu.linkonce.this_module Address 0x6286f2b0
Name .bss Address 0x6286f490
Name .symtab Address 0x62872098
Name .strtab Address 0x628725c0

如此便可稍微方便一点的打印所有段名和段基址,如果添加其他脚本扩展,可以更加方便且高效的调试Linux内核。

相关系列文章:
Linux 内核调试1-UML
Linux 内核调试2-UML调试内核
Linux 内核调试3-UML网络配置
Linux 内核调试4-Qemu调试Linux内核
Linux 内核调试5-UML和Qemu调试模块
Linux 内核调试6-使用KGDB双机调试

相关内容