首页 / 操作系统 / Linux / 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/000:00:38 linux 7340 pts/000:00:00 linux 7341 pts/000:00:00 linux 7342 pts/000:00:00 linux 7343 pts/000:00:00 linux 7481 pts/000:00:00 linux 7546 pts/000:00:00 linux 9864 pts/000:00:00 linux 9866 pts/000:00:00 linux 9868 pts/000:00:00 linux# 启动 GDB[cpp@dark linux-2.6.36]$ gdb linuxGNU gdb (GDB) Fedora (7.1-18.fc13)Copyright (C) 2010 Free Software Foundation, Inc.License GPLv3+: GNU GPL version 3 or laterThis 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_moduleBreakpoint 1 at 0x60052421: file kernel/module.c, line 2696.# 继续执行(gdb) cContinuing.# 在虚拟机中加载 go.ko 模块localhost ~ # insmod go.ko# 主机GDB截获断点Breakpoint 1, sys_init_module (umod=0x603030, len=124282,uargs=0x603010) at kernel/module.c:26962696 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", " 00"# 查看模块段个数(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
”, 10)) (gdb)当然加载符号时还可以查看 .data 段和 .bss 段的基址,加载go.ko时同时可以设置二者的基址,以便可以调试全局变量等等,初看起来这个过程比较复杂,其实就是通过调试内核加载位置来确定模块最终将要加载的基址,而后通过机制加载模块符号。这个复杂的过程如果每次都需要手动处理是非常烦人的一件事,好在GDB本身有脚本扩展(甚至可执行Python脚本),来简化这个过程,这里来试着写一个简单的打印模块section名字和基址的脚本。GDB脚本运行可以由两种方式,一种是在GDB启动时,在当前目录查找.gdbinit文件解释执行,另一种在GDB运行期间使用 source script-file 命令来执行,脚本的说明可查阅文档,以下是简单的打印段信息的脚本:define modsecset $index = 0while $index < mod->sect_attrs->nsectionsprintf "Name %s Address 0x%x
", mod->sect_attrs->attrs[$index]->name, mod->sect_attrs->attrs[$index]->addressset $index = $index + 1endend以下是加载并执行脚本效果:# 加载脚本(gdb) source modsec(gdb) cContinuing.# 在虚拟机中加载 go.ko 模块# 并单步执行到 mod = load_module(umod, len, uargs);Breakpoint 1, sys_init_module (umod=0x603030, len=124282,uargs=0x603010) at kernel/module.c:26962696 if (!capable(CAP_SYS_MODULE) || modules_disabled)(gdb) n2691 {(gdb) n2696 if (!capable(CAP_SYS_MODULE) || modules_disabled)(gdb) n2700 mod = load_module(umod, len, uargs);(gdb) n2701 if (IS_ERR(mod))# 调用定义命令打印段(gdb) modsecName .note.gnu.build-id Address 0x6286f09cName .text Address 0x6286f000Name .exit.text Address 0x6286f050Name .init.text Address 0x62872000Name .rodata.str1.1 Address 0x6286f0c0Name .eh_frame Address 0x6286f118Name .data Address 0x6286f1e0Name .gnu.linkonce.this_module Address 0x6286f2b0Name .bss Address 0x6286f490Name .symtab Address 0x62872098Name .strtab Address 0x628725c0如此便可稍微方便一点的打印所有段名和段基址,如果添加其他脚本扩展,可以更加方便且高效的调试Linux内核。相关系列文章:
Linux 内核调试1-UML http://www.linuxidc.com/Linux/2012-07/66410.htm
Linux 内核调试2-UML调试内核 http://www.linuxidc.com/Linux/2012-07/66411.htm
Linux 内核调试3-UML网络配置 http://www.linuxidc.com/Linux/2012-07/66412.htm
Linux 内核调试4-Qemu调试Linux内核 http://www.linuxidc.com/Linux/2012-07/66413.htm
Linux 内核调试5-UML和Qemu调试模块 http://www.linuxidc.com/Linux/2012-07/66414.htm
Linux 内核调试6-使用KGDB双机调试 http://www.linuxidc.com/Linux/2012-07/66415.htm