内核可加载模块的调试具有其特殊性。由于内核模块中各段的地址是在模块加载进内核的时候才最终确定的,所以
develop
机的
gdb
无法得到各种符号地址信息。所以,使用
kgdb
调试模块所需要解决的一个问题是,需要通过某种方法获得可加载模块的最终加载地址信息,并把这些信息加入到
gdb
环境中。
I
、在
Linux 2.4
内核中的内核模块调试方法
在
Linux2.4.x
内核中,可以使用
insmod -m
命令输出模块的加载信息,例如:
[root@lisl tmp]# insmod -m hello.ko >modaddr
查看模块加载信息文件
modaddr
如下:
.this 00000060 c88d8000 2**2
.text 00000035 c88d8060 2**2
.rodata 00000069 c88d80a0 2**5
……
.data 00000000 c88d833c 2**2
.bss 00000000 c88d833c 2**2
……
在这些信息中,我们关心的只有
4
个段的地址
:.text
、
.rodata
、
.data
、
.bss
。在
development
机上将以上地址信息加入到
gdb
中
,
这样就可以进行模块功能的测试了。
(gdb) Add-symbol-file hello.o 0xc88d8060 -s .data 0xc88d80a0 -s
.rodata 0xc88d80a0 -s .bss 0x c88d833c
这种方法也存在一定的不足,它不能调试模块初始化的代码,因为此时模块初始化代码已经执行过了。而如果不执行模块的加载又无法获得模块插入地址,更不可能在模块初始化之前设置断点了。对于这种调试要求可以采用以下替代方法。
在
target
机上用上述方法得到模块加载的地址信息,然后再用
rmmod
卸载模块。在
development
机上将得到的模块地址信息导入到
gdb
环境中,在内核代码的调用初始化代码之前设置断点。这样,在
target
机上再次插入模块时,代码将在执行模块初始化之前停下来,这样就可以使用
gdb
命令调试模块初始化代码了。
另外一种调试模块初始化函数的方法是:当插入内核模块时,内核模块机制将调用函数
sys_init_module(kernel/modle.c)
执行对内核模块的初始化,该函数将调用所插入模块的初始化函数。程序代码片断如下:
…… ……
if (mod->init != NULL)
ret = mod->init();
…… ……
在该语句上设置断点,也能在执行模块初始化之前停下来。
II
、在
Linux 2.6.x
内核中的内核模块调试方法
Linux 2.6之后的内核中,由于module-init-tools工具的更改,insmod命令不再支持-m
参数,只有采取其他的方法来获取模块加载到内核的地址。通过分析
ELF
文件格式,我们知道程序中各段的意义如下:
.text
(代码段):用来存放可执行文件的操作指令,也就是说是它是可执行程序在内存种的镜像。
.data
(数据段):数据段用来存放可执行文件中已初始化全局变量,也就是存放程序静态分配的变量和全局变量。
.bss
(
BSS
段):
BSS
段包含了程序中未初始化全局变量,在内存中
bss
段全部置零。
.rodata
(只读段):该段保存着只读数据,在进程映象中构造不可写的段。
通过在模块初始化函数中放置一下代码,我们可以很容易地获得模块加载到内存中的地址。
……
int bss_var;
static int hello_init(void)
{
printk(KERN_ALERT "Text location .text(Code Segment):%p/n",hello_init);
static int data_var=0;
printk(KERN_ALERT "Data Location .data(Data Segment):%p/n",&data_var);
printk(KERN_ALERT "BSS Location: .bss(BSS Segment):%p/n",&bss_var);
……
}
Module_init(hello_init);
这里,通过在模块的初始化函数中添加一段简单的程序,使模块在加载时打印出在内核中的加载地址。
.rodata
段的地址可以通过执行命令readelf -e hello.ko
,取得
.rodata
在文件中的偏移量并加上段的
align
值得出。
为了使读者能够更好地进行模块的调试,
kgdb项目还发布了一些脚本程序能够自动探测模块的插入并自动更新gdb
中模块的符号信息。这些脚本程序的工作原理与前面解释的工作过程相似,更多的信息请阅读参考资料
[4]
。