内核调试方法 二

九  KGDB

kgdb提供了一种使用 gdb调试 Linux 内核的机制。使用KGDB可以象调试普通的应用程序那样,在内核中进行设置断点、检查变量值、单步跟踪程序运行等操作。使用KGDB调试时需要两台机器,一台作为开发机(Development Machine),另一台作为目标机(Target Machine),两台机器之间通过串口或者以太网口相连。串口连接线是一根RS-232接口的电缆,在其内部两端的第2脚(TXD)与第3脚(RXD)交叉相连,第7脚(接地脚)直接相连。调试过程中,被调试的内核运行在目标机上,gdb调试器运行在开发机上。 
目前,kgdb发布支持i386、x86_64、32-bit PPC、SPARC等几种体系结构的调试器。 

1  kgdb的调试原理
安装kgdb调试环境需要为Linux内核应用kgdb补丁,补丁实现的gdb远程调试所需要的功能包括命令处理、陷阱处理及串口通讯3个主要的部分。kgdb补丁的主要作用是在Linux内核中添加了一个调试Stub。调试Stub是Linux内核中的一小段代码,提供了运行gdb的开发机和所调试内核之间的一个媒介。gdb和调试stub之间通过gdb串行协议进行通讯。gdb串行协议是一种基于消息的ASCII码协议,包含了各种调试命令。当设置断点时,kgdb负责在设置断点的指令前增加一条trap指令,当执行到断点时控制权就转移到调试stub中去。此时,调试stub的任务就是使用远程串行通信协议将当前环境传送给gdb,然后从gdb处接受命令。gdb命令告诉stub下一步该做什么,当stub收到继续执行的命令时,将恢复程序的运行环境,把对CPU的控制权重新交还给内核 

2  Kgdb的安装与设置
下面我们将以Linux 2.6.7内核为例详细介绍kgdb调试环境的建立过程。 


2.1  软硬件准备

以下软硬件配置取自笔者进行试验的系统配置情况: 
kgdb补丁的版本遵循如下命名模式:Linux-A-kgdb-B,其中A表示Linux的内核版本号,B为kgdb的版本号。以试验使用的kgdb补丁为例,linux内核的版本为linux-2.6.7,补丁版本为kgdb-2.2。 
物理连接好串口线后,使用以下命令来测试两台机器之间串口连接情况,stty命令可以对串口参数进行设置: 
在development机上执行: 
1 stty ispeed 115200 ospeed 115200 -F /dev/ttyS0
在target机上执行: 
1 stty ispeed 115200 ospeed 115200 -F /dev/ttyS0
在developement机上执行: 
1 echo hello > /dev/ttyS0
在target机上执行: 
1 cat /dev/ttyS0
如果串口连接没问题的话在将在target机的屏幕上显示"hello"。 


2.2  安装与配置

下面我们需要应用kgdb补丁到Linux内核,设置内核选项并编译内核。这方面的资料相对较少,笔者这里给出详细的介绍。下面的工作在开发机(developement)上进行,以上面介绍的试验环境为例,某些具体步骤在实际的环境中可能要做适当的改动: 


I、内核的配置与编译

1 [root@lisl tmp]# tar -jxvf linux-2.6.7.tar.bz2
2 [root@lisl tmp]#tar -jxvf linux-2.6.7-kgdb-2.2.tar.tar
3 [root@lisl tmp]#cd inux-2.6.7
请参照目录补丁包中文件README给出的说明,执行对应体系结构的补丁程序。由于试验在i386体系结构上完成,所以只需要安装一下补丁:core-lite.patch、i386-lite.patch、8250.patch、eth.patch、core.patch、i386.patch。应用补丁文件时,请遵循kgdb软件包内series文件所指定的顺序,否则可能会带来预想不到的问题。eth.patch文件是选择以太网口作为调试的连接端口时需要运用的补丁。 
应用补丁的命令如下所示: 
1 [root@lisl tmp]#patch -p1 <../linux-2.6.7-kgdb-2.2/core-lite.patch
如果内核正确,那么应用补丁时应该不会出现任何问题(不会产生*.rej文件)。为Linux内核添加了补丁之后,需要进行内核的配置。内核的配置可以按照你的习惯选择配置Linux内核的任意一种方式。 
1 [root@lisl tmp]#make menuconfig
在内核配置菜单的Kernel hacking选项中选择kgdb调试项,例如: 
1 [*] KGDB: kernel debugging with remote gdb
2       Method for KGDB communication (KGDB: On generic serial port (8250))  ---> 
3  [*] KGDB: Thread analysis
4  [*] KGDB: Console messages through gdb
5 [root@lisl tmp]#make
编译内核之前请注意Linux目录下Makefile中的优化选项,默认的Linux内核的编译都以-O2的优化级别进行。在这个优化级别之下,编译器要对内核中的某些代码的执行顺序进行改动,所以在调试时会出现程序运行与代码顺序不一致的情况。可以把Makefile中的-O2选项改为-O,但不可去掉-O,否则编译会出问题。为了使编译后的内核带有调试信息,注意在编译内核的时候需要加上-g选项。 
不过,当选择"Kernel debugging->Compile the kernel with debug info"选项后配置系统将自动打开调试选项。另外,选择"kernel debugging with remote gdb"后,配置系统将自动打开"Compile the kernel with debug info"选项。 
内核编译完成后,使用scp命令进行将相关文件拷贝到target机上(当然也可以使用其它的网络工具,如rcp)。 
1 [root@lisl tmp]#scp arch/i386/boot/bzImage root@192.168.6.13:/boot/vmlinuz-2.6.7-kgdb
2 [root@lisl tmp]#scp System.map root@192.168.6.13:/boot/System.map-2.6.7-kgdb
如果系统启动使所需要的某些设备驱动没有编译进内核的情况下,那么还需要执行如下操作: 
1 [root@lisl tmp]#mkinitrd /boot/initrd-2.6.7-kgdb 2.6.7
2 [root@lisl tmp]#scp initrd-2.6.7-kgdb root@192.168.6.13:/boot/ initrd-2.6.7-kgdb

II、kgdb的启动 
在将编译出的内核拷贝的到target机器之后,需要配置系统引导程序,加入内核的启动选项。以下是kgdb内核引导参数的说明: 
 
如表中所述,在kgdb 2.0版本之后内核的引导参数已经与以前的版本有所不同。使用grub引导程序时,直接将kgdb参数作为内核vmlinuz的引导参数。下面给出引导器的配置示例。 
1 title 2.6.7 kgdb
2 root (hd0,0)
3 kernel /boot/vmlinuz-2.6.7-kgdb ro root=/dev/hda1 kgdbwait kgdb8250=1,115200
在使用lilo作为引导程序时,需要把kgdb参放在由append修饰的语句中。下面给出使用lilo作为引导器时的配置示例。 
1 image=/boot/vmlinuz-2.6.7-kgdb
2 label=kgdb
3    read-only
4    root=/dev/hda3
5 append="gdb gdbttyS=1 gdbbaud=115200"
保存好以上配置后重新启动计算机,选择启动带调试信息的内核,内核将在短暂的运行后在创建init内核线程之前停下来,打印出以下信息,并等待开发机的连接。 
Waiting for connection from remote gdb... 
在开发机上执行: 
1 gdb
2 file vmlinux
3 set remotebaud 115200
4 target remote /dev/ttyS0
其中vmlinux是指向源代码目录下编译出来的Linux内核文件的链接,它是没有经过压缩的内核文件,gdb程序从该文件中得到各种符号地址信息。 
这样,就与目标机上的kgdb调试接口建立了联系。一旦建立联接之后,对Linux内的调试工作与对普通的运用程序的调试就没有什么区别了。任何时候都可以通过键入ctrl+c打断目标机的执行,进行具体的调试工作。 
在kgdb 2.0之前的版本中,编译内核后在arch/i386/kernel目录下还会生成可执行文件gdbstart。将该文件拷贝到target机器的/boot目录下,此时无需更改内核的启动配置文件,直接使用命令: 
1 [root@lisl boot]#gdbstart -s 115200 -t /dev/ttyS0
可以在KGDB内核引导启动完成后建立开发机与目标机之间的调试联系。 

2.3  通过网络接口进行调试 
kgdb也支持使用以太网接口作为调试器的连接端口。在对Linux内核应用补丁包时,需应用eth.patch补丁文件。配置内核时在Kernel hacking中选择kgdb调试项,配置kgdb调试端口为以太网接口,例如: 
1 [*]KGDB: kernel debugging with remote gdb
2 Method for KGDB communication (KGDB: On ethernet)  --->
3 ( ) KGDB: On generic serial port (8250)
4 (X) KGDB: On ethernet
另外使用eth0网口作为调试端口时,grub.list的配置如下: 
1 title 2.6.7 kgdb
2 root (hd0,0)
3 kernel /boot/vmlinuz-2.6.7-kgdb ro root=/dev/hda1 kgdbwait kgdboe=@192.168.5.13/,@192.168. 6.13/
其他的过程与使用串口作为连接端口时的设置过程相同。 
注意:尽管可以使用以太网口作为kgdb的调试端口,使用串口作为连接端口更加简单易行,kgdb项目组推荐使用串口作为调试端口。 

2.4  模块的调试方法 
内核可加载模块的调试具有其特殊性。由于内核模块中各段的地址是在模块加载进内核的时候才最终确定的,所以develop机的gdb无法得到各种符号地址信息。所以,使用kgdb调试模块所需要解决的一个问题是,需要通过某种方法获得可加载模块的最终加载地址信息,并把这些信息加入到gdb环境中。 
I、在Linux 2.4内核中的内核模块调试方法 
在Linux2.4.x内核中,可以使用insmod -m命令输出模块的加载信息,例如: 
1 [root@lisl tmp]# insmod -m hello.ko >modaddr
查看模块加载信息文件modaddr如下: 
1 .this           00000060  c88d8000  2**2
2 .text           00000035  c88d8060  2**2
3 .rodata         00000069  c88d80a0  2**5
4 ……
5 .data           00000000  c88d833c  2**2
6 .bss            00000000  c88d833c  2**2
7 ……
在这些信息中,我们关心的只有4个段的地址:.text、.rodata、.data、.bss。在development机上将以上地址信息加入到gdb中,这样就可以进行模块功能的测试了。 
1 (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)执行对内核模块的初始化,该函数将调用所插入模块的初始化函数。程序代码片断如下: 
1 …… ……
2 if (mod->init != NULL)
3 ret = mod->init();
4 …… ……
在该语句上设置断点,也能在执行模块初始化之前停下来。 


II、在Linux 2.6.x内核中的内核模块调试方法

Linux 2.6之后的内核中,由于module-init-tools工具的更改,insmod命令不再支持-m参数,只有采取其他的方法来获取模块加载到内核的地址。通过分析ELF文件格式,我们知道程序中各段的意义如下: 
.text(代码段):用来存放可执行文件的操作指令,也就是说是它是可执行程序在内存种的镜像。 
.data(数据段):数据段用来存放可执行文件中已初始化全局变量,也就是存放程序静态分配的变量和全局变量。 
.bss(BSS段):BSS段包含了程序中未初始化全局变量,在内存中 bss段全部置零。 
.rodata(只读段):该段保存着只读数据,在进程映象中构造不可写的段。 
通过在模块初始化函数中放置一下代码,我们可以很容易地获得模块加载到内存中的地址。 
01 ……
02 int bss_var;
03 static int hello_init(void)
04 {
05 printk(KERN_ALERT "Text location .text(Code Segment):%p\n",hello_init);
06 static int data_var=0;
07 printk(KERN_ALERT "Data Location .data(Data Segment):%p\n",&data_var);
08 printk(KERN_ALERT "BSS Location: .bss(BSS Segment):%p\n",&bss_var);
09 ……
10 }
11 Module_init(hello_init);
这里,通过在模块的初始化函数中添加一段简单的程序,使模块在加载时打印出在内核中的加载地址。.rodata段的地址可以通过执行命令readelf -e hello.ko,取得.rodata在文件中的偏移量并加上段的align值得出。 
为了使读者能够更好地进行模块的调试,kgdb项目还发布了一些脚本程序能够自动探测模块的插入并自动更新gdb中模块的符号信息。这些脚本程序的工作原理与前面解释的工作过程相似,更多的信息请阅读参考资料[4]。 

2.5  硬件断点 
kgdb提供对硬件调试寄存器的支持。在kgdb中可以设置三种硬件断点:执行断点(Execution Breakpoint)、写断点(Write Breakpoint)、访问断点(Access Breakpoint)但不支持I/O访问的断点。 目前,kgdb对硬件断点的支持是通过宏来实现的,最多可以设置4个硬件断点,这些宏的用法如下: 
 
在有些情况下,硬件断点的使用对于内核的调试是非常方便的。 

3  在VMware中搭建调试环境
kgdb调试环境需要使用两台微机分别充当development机和target机,使用VMware后我们只使用一台计算机就可以顺利完成kgdb调试环境的搭建。以windows下的环境为例,创建两台虚拟机,一台作为开发机,一台作为目标机。 

3.1  虚拟机之间的串口连接 
虚拟机中的串口连接可以采用两种方法。一种是指定虚拟机的串口连接到实际的COM上,例如开发机连接到COM1,目标机连接到COM2,然后把两个串口通过串口线相连接。另一种更为简便的方法是:在较高一些版本的VMware中都支持把串口映射到命名管道,把两个虚拟机的串口映射到同一个命名管道。例如,在两个虚拟机中都选定同一个命名管道 \\.\pipe\com_1,指定target机的COM口为server端,并选择"The other end is a virtual machine"属性;指定development机的COM口端为client端,同样指定COM口的"The other end is a virtual machine"属性。对于IO mode属性,在target上选中"Yield CPU on poll"复选择框,development机不选。这样,可以无需附加任何硬件,利用虚拟机就可以搭建kgdb调试环境。 即降低了使用kgdb进行调试的硬件要求,也简化了建立调试环境的过程。 
 


3.2  VMware的使用技巧

VMware虚拟机是比较占用资源的,尤其是象上面那样在Windows中使用两台虚拟机。因此,最好为系统配备512M以上的内存,每台虚拟机至少分配128M的内存。这样的硬件要求,对目前主流配置的PC而言并不是过高的要求。出于系统性能的考虑,在VMware中尽量使用字符界面进行调试工作。同时,Linux系统默认情况下开启了sshd服务,建议使用SecureCRT登陆到Linux进行操作,这样可以有较好的用户使用界面。 

3.3  在Linux下的虚拟机中使用kgdb 
对于在Linux下面使用VMware虚拟机的情况,笔者没有做过实际的探索。从原理上而言,只需要在Linux下只要创建一台虚拟机作为target机,开发机的工作可以在实际的Linux环境中进行,搭建调试环境的过程与上面所述的过程类似。由于只需要创建一台虚拟机,所以使用Linux下的虚拟机搭建kgdb调试环境对系统性能的要求较低。(vmware已经推出了Linux下的版本)还可以在development机上配合使用一些其他的调试工具,例如功能更强大的cgdb、图形界面的DDD调试器等,以方便内核的调试工作。 
 


4  kgdb的一些特点和不足
使用kgdb作为内核调试环境最大的不足在于对kgdb硬件环境的要求较高,必须使用两台计算机分别作为target和development机。尽管使用虚拟机的方法可以只用一台PC即能搭建调试环境,但是对系统其他方面的性能也提出了一定的要求,同时也增加了搭建调试环境时复杂程度。另外,kgdb内核的编译、配置也比较复杂,需要一定的技巧,笔者当时做的时候也是费了很多周折。当调试过程结束后时,还需要重新制作所要发布的内核。使用kgdb并不能进行全程调试,也就是说kgdb并不能用于调试系统一开始的初始化引导过程。 
不过,kgdb是一个不错的内核调试工具,使用它可以进行对内核的全面调试,甚至可以调试内核的中断处理程序。如果在一些图形化的开发工具的帮助下,对内核的调试将更方便。 

参考: 
透过虚拟化技术体验kgdb 
Linux 系统内核的调试 
Debugging The Linux Kernel Using Gdb 


十  使用SkyEye构建Linux内核调试环境

SkyEye是一个开源软件项目(OPenSource Software),SkyEye项目的目标是在通用的Linux和Windows平台上模拟常见的嵌入式计算机系统。SkyEye实现了一个指令级的硬件模拟平台,可以模拟多种嵌入式开发板,支持多种CPU指令集。SkyEye 的核心是 GNU 的 gdb 项目,它把gdb和 ARM Simulator很好地结合在了一起。加入ARMulator 的功能之后,它就可以来仿真嵌入式开发板,在它上面不仅可以调试硬件驱动,还可以调试操作系统。Skyeye项目目前已经在嵌入式系统开发领域得到了很大的推广。 

1  SkyEye的安装和μcLinux内核编译
1.1  SkyEye的安装 
SkyEye的安装不是本文要介绍的重点,目前已经有大量的资料对此进行了介绍。有关SkyEye的安装与使用的内容请查阅参考资料[11]。由于skyeye面目主要用于嵌入式系统领域,所以在skyeye上经常使用的是μcLinux系统,当然使用Linux作为skyeye上运行的系统也是可以的。由于介绍μcLinux 2.6在skyeye上编译的相关资料并不多,所以下面进行详细介绍。 

1.2  μcLinux 2.6.x的编译 
要在SkyEye中调试操作系统内核,首先必须使被调试内核能在SkyEye所模拟的开发板上正确运行。因此,正确编译待调试操作系统内核并配置SkyEye是进行内核调试的第一步。下面我们以SkyEye模拟基于Atmel AT91X40的开发板,并运行μcLinux 2.6为例介绍SkyEye的具体调试方法。 

I、安装交叉编译环境 
先安装交叉编译器。尽管在一些资料中说明使用工具链arm-elf-tools-20040427.sh ,但是由于arm-elf-xxx与arm-linux-xxx对宏及链接处理的不同,经验证明使用arm-elf-xxx工具链在链接vmlinux的最后阶段将会出错。所以这里我们使用的交叉编译工具链是:arm-uclinux-tools-base-gcc3.4.0-20040713.sh,关于该交叉编译工具链的下载地址请参见[6]。注意以下步骤最好用root用户来执行。 
1 [root@lisl tmp]#chmod +x  arm-uclinux-tools-base-gcc3.4.0-20040713.sh
2 [root@lisl tmp]#./arm-uclinux-tools-base-gcc3.4.0-20040713.sh
安装交叉编译工具链之后,请确保工具链安装路径存在于系统PATH变量中。 

II、制作μcLinux内核 
得到μcLinux发布包的一个最容易的方法是直接访问uClinux.org站点[7]。该站点发布的内核版本可能不是最新的,但你能找到一个最新的μcLinux补丁以及找一个对应的Linux内核版本来制作一个最新的μcLinux内核。这里,将使用这种方法来制作最新的μcLinux内核。目前(笔者记录编写此文章时),所能得到的发布包的最新版本是uClinux-dist.20041215.tar.gz。 
下载uClinux-dist.20041215.tar.gz,文件的下载地址请参见[7]。 
下载linux-2.6.9-hsc0.patch.gz,文件的下载地址请参见[8]。 
下载linux-2.6.9.tar.bz2,文件的下载地址请参见[9]。 
现在我们得到了整个的linux-2.6.9源代码,以及所需的内核补丁。请准备一个有2GB空间的目录里来完成以下制作μcLinux内核的过程。 
1 [root@lisl tmp]# tar -jxvf uClinux-dist-20041215.tar.bz2
2 [root@lisl uClinux-dist]# tar -jxvf  linux-2.6.9.tar.bz2
3 [root@lisl uClinux-dist]# gzip -dc linux-2.6.9-hsc0.patch.gz | patch -p0
或者使用:
1 [root@lisl uClinux-dist]# gunzip linux-2.6.9-hsc0.patch.gz
2 [root@lisl uClinux-dist]patch -p0 < linux-2.6.9-hsc0.patch
执行以上过程后,将在linux-2.6.9/arch目录下生成一个补丁目录-armnommu。删除原来μcLinux目录里的linux-2.6.x(即那个linux-2.6.9-uc0),并将我们打好补丁的Linux内核目录更名为linux-2.6.x。 
1 [root@lisl uClinux-dist]# rm -rf linux-2.6.x/
2 [root@lisl uClinux-dist]# mv linux-2.6.9 linux-2.6.x

III、配置和编译μcLinux内核 
因为只是出于调试μcLinux内核的目的,这里没有生成uClibc库文件及romfs.img文件。在发布μcLinux时,已经预置了某些常用嵌入式开发板的配置文件,因此这里直接使用这些配置文件,过程如下: 
1 [root@lisl uClinux-dist]# cd linux-2.6.x
2 [root@lisl linux-2.6.x]#make ARCH=armnommu CROSS_COMPILE=arm-uclinux- atmel_deconfig
atmel_deconfig文件是μcLinux发布时提供的一个配置文件,存放于目录linux-2.6.x /arch/armnommu/configs/中。 
1 [root@lisl linux-2.6.x]#make ARCH=armnommu CROSS_COMPILE=arm-uclinux- oldconfig
下面编译配置好的内核: 
1 [root@lisl linux-2.6.x]# make ARCH=armnommu CROSS_COMPILE=arm-uclinux- v=1


一般情况下,编译将顺利结束并在Linux-2.6.x/目录下生成未经压缩的μcLinux内核文件vmlinux。需要注意的是为了调试μcLinux内核,需要打开内核编译的调试选项-g,使编译后的内核带有调试信息。打开编译选项的方法可以选择: 
"Kernel debugging->Compile the kernel with debug info"后将自动打开调试选项。也可以直接修改linux-2.6.x目录下的Makefile文件,为其打开调试开关。方法如下:。 
1 CFLAGS  += -g
最容易出现的问题是找不到arm-uclinux-gcc命令的错误,主要原因是PATH变量中没有 包含arm-uclinux-gcc命令所在目录。在arm-linux-gcc的缺省安装情况下,它的安装目录是/root/bin/arm-linux-tool/,使用以下命令将路径加到PATH环境变量中。 
1 Export PATH=$PATH:/root/bin/arm-linux-tool/bin

IV、根文件系统的制作 
Linux内核在启动的时的最后操作之一是加载根文件系统。根文件系统中存放了嵌入式 系统使用的所有应用程序、库文件及其他一些需要用到的服务。出于文章篇幅的考虑,这里不打算介绍根文件系统的制作方法,读者可以查阅一些其他的相关资料。值得注意的是,由配置文件skyeye.conf指定了装载到内核中的根文件系统。 

2  使用SkyEye调试
编译完μcLinux内核后,就可以在SkyEye中调试该ELF执行文件格式的内核了。前面已经说过利用SkyEye调试内核与使用gdb调试运用程序的方法相同。 
需要提醒读者的是,SkyEye的配置文件-skyeye.conf记录了模拟的硬件配置和模拟执行行为。该配置文件是SkyEye系统中一个及其重要的文件,很多错误和异常情况的发生都和该文件有关。在安装配置SkyEye出错时,请首先检查该配置文件然后再进行其他的工作。此时,所有的准备工作已经完成,就可以进行内核的调试工作了。 

3  使用SkyEye调试内核的特点和不足
在SkyEye中可以进行对Linux系统内核的全程调试。由于SkyEye目前主要支持基于ARM内核的CPU,因此一般而言需要使用交叉编译工具编译待调试的Linux系统内核。另外,制作SkyEye中使用的内核编译、配置过程比较复杂、繁琐。不过,当调试过程结束后无需重新制作所要发布的内核。 
SkyEye只是对系统硬件进行了一定程度上的模拟,所以在SkyEye与真实硬件环境相比较而言还是有一定的差距,这对一些与硬件紧密相关的调试可能会有一定的影响,例如驱动程序的调试。不过对于大部分软件的调试,SkyEye已经提供了精度足够的模拟了。 
SkyEye的下一个目标是和eclipse结合,有了图形界面,能为调试和查看源码提供一些方便。 

参考: 
Linux 系统内核的调试 

十一  KDB

Linux 内核调试器(KDB)允许您调试 Linux 内核。这个恰如其名的工具实质上是内核代码的补丁,它允许高手访问内核内存和数据结构。KDB 的主要优点之一就是它不需要用另一台机器进行调试:您可以调试正在运行的内核。 
设置一台用于 KDB 的机器需要花费一些工作,因为需要给内核打补丁并进行重新编译。KDB 的用户应当熟悉 Linux 内核的编译(在一定程度上还要熟悉内核内部机理)。 
在本文中,我们将从有关下载 KDB 补丁、打补丁、(重新)编译内核以及启动 KDB 方面的信息着手。然后我们将了解 KDB 命令并研究一些较常用的命令。最后,我们将研究一下有关设置和显示选项方面的一些详细信息。 

1  入门
KDB 项目是由 Silicon Graphics 维护的,您需要从它的 FTP 站点下载与内核版本有关的补丁。(在编写本文时)可用的最新 KDB 版本是 4.2。您将需要下载并应用两个补丁。一个是“公共的”补丁,包含了对通用内核代码的更改,另一个是特定于体系结构的补丁。补丁可作为 bz2 文件获取。例如,在运行 2.4.20 内核的 x86 机器上,您会需要 kdb-v4.2-2.4.20-common-1.bz2 和 kdb-v4.2-2.4.20-i386-1.bz2。 
这里所提供的所有示例都是针对 i386 体系结构和 2.4.20 内核的。您将需要根据您的机器和内核版本进行适当的更改。您还需要拥有 root 许可权以执行这些操作。 
将文件复制到 /usr/src/linux 目录中并从用 bzip2 压缩的文件解压缩补丁文件:
1 #bzip2 -d kdb-v4.2-2.4.20-common-1.bz2
2 #bzip2 -d kdb-v4.2-2.4.20-i386-1.bz2  
您将获得 kdb-v4.2-2.4.20-common-1 和 kdb-v4.2-2.4-i386-1 文件。 
现在,应用这些补丁:
1 #patch -p1 <kdb-v4.2-2.4.20-common-1
2 #patch -p1 <kdb-v4.2-2.4.20-i386-1
这些补丁应该干净利落地加以应用。查找任何以 .rej 结尾的文件。这个扩展名表明这些是失败的补丁。如果内核树没问题,那么补丁的应用就不会有任何问题。 
接下来,需要构建内核以支持 KDB。第一步是设置 CONFIG_KDB 选项。使用您喜欢的配置机制(xconfig 和 menuconfig 等)来完成这一步。转到结尾处的“Kernel hacking”部分并选择“Built-in Kernel Debugger support”选项。 
您还可以根据自己的偏好选择其它两个选项。选择“Compile the kernel with frame pointers”选项(如果有的话)则设置CONFIG_FRAME_POINTER 标志。这将产生更好的堆栈回溯,因为帧指针寄存器被用作帧指针而不是通用寄存器。您还可以选择“KDB off by default”选项。这将设置 CONFIG_KDB_OFF 标志,并且在缺省情况下将关闭 KDB。我们将在后面一节中对此进行详细介绍。 
保存配置,然后退出。重新编译内核。建议在构建内核之前执行“make clean”。用常用方式安装内核并引导它。 

2  初始化并设置环境变量
您可以定义将在 KDB 初始化期间执行的 KDB 命令。需要在纯文本文件 kdb_cmds 中定义这些命令,该文件位于 Linux 源代码树(当然是在打了补丁之后)的 KDB 目录中。该文件还可以用来定义设置显示和打印选项的环境变量。文件开头的注释提供了编辑文件方面的帮助。使用这个文件的缺点是,在您更改了文件之后需要重新构建并重新安装内核。 

3  激活 KDB
如果编译期间没有选中 CONFIG_KDB_OFF ,那么在缺省情况下 KDB 是活动的。否则,您需要显式地激活它 - 通过在引导期间将kdb=on 标志传递给内核或者通过在挂装了 /proc 之后执行该工作:
1 #echo "1" >/proc/sys/kernel/kdb
倒过来执行上述步骤则会取消激活 KDB。也就是说,如果缺省情况下 KDB 是打开的,那么将 kdb=off 标志传递给内核或者执行下面这个操作将会取消激活 KDB:
1 #echo "0" >/proc/sys/kernel/kdb
在引导期间还可以将另一个标志传递给内核。 kdb=early 标志将导致在引导过程的初始阶段就把控制权传递给 KDB。如果您需要在引导过程初始阶段进行调试,那么这将有所帮助。 
调用 KDB 的方式有很多。如果 KDB 处于打开状态,那么只要内核中有紧急情况就自动调用它。按下键盘上的 PAUSE 键将手工调用 KDB。调用 KDB 的另一种方式是通过串行控制台。当然,要做到这一点,需要设置串行控制台并且需要一个从串行控制台进行读取的程序。按键序列 Ctrl-A 将从串行控制台调用 KDB。 

4  KDB 命令
KDB 是一个功能非常强大的工具,它允许进行几个操作,比如内存和寄存器修改、应用断点和堆栈跟踪。根据这些,可以将 KDB 命令分成几个类别。下面是有关每一类中最常用命令的详细信息。 

4.1  内存显示和修改 
这一类别中最常用的命令是 md 、 mdr 、 mm 和 mmW 。 
md 命令以一个地址/符号和行计数为参数,显示从该地址开始的 line-count 行的内存。如果没有指定 line-count ,那么就使用环境变量所指定的缺省值。如果没有指定地址,那么 md 就从上一次打印的地址继续。地址打印在开头,字符转换打印在结尾。 
mdr 命令带有地址/符号以及字节计数,显示从指定的地址开始的 byte-count 字节数的初始内存内容。它本质上和 md 一样,但是它不显示起始地址并且不在结尾显示字符转换。 mdr 命令较少使用。 
mm 命令修改内存内容。它以地址/符号和新内容作为参数,用 new-contents 替换地址处的内容。 
mmW 命令更改从地址开始的 W 个字节。请注意, mm 更改一个机器字。 
示例 

显示从 0xc000000 开始的 15 行内存:
1 [0]kdb> md 0xc000000 15
将内存位置为 0xc000000 上的内容更改为 0x10:
1 [0]kdb> mm 0xc000000 0x10

4.2  寄存器显示和修改 
这一类别中的命令有 rd 、 rm 和 ef 。 
rd 命令(不带任何参数)显示处理器寄存器的内容。它可以有选择地带三个参数。如果传递了 c 参数,则 rd 显示处理器的控制寄存器;如果带有 d 参数,那么它就显示调试寄存器;如果带有 u 参数,则显示上一次进入内核的当前任务的寄存器组。 
rm 命令修改寄存器的内容。它以寄存器名称和 new-contents 作为参数,用 new-contents 修改寄存器。寄存器名称与特定的体系结构有关。目前,不能修改控制寄存器。 
ef 命令以一个地址作为参数,它显示指定地址处的异常帧。 
示例 
显示通用寄存器组:
1 [0]kdb> rd
2 [0]kdb> rm %ebx 0x25

4.3  断点 
常用的断点命令有 bp 、 bc 、 bd 、 be 和 bl 。 
bp 命令以一个地址/符号作为参数,它在地址处应用断点。当遇到该断点时则停止执行并将控制权交予 KDB。该命令有几个有用的变体。 bpa 命令对 SMP 系统中的所有处理器应用断点。 bph 命令强制在支持硬件寄存器的系统上使用它。 bpha 命令类似于 bpa 命令,差别在于它强制使用硬件寄存器。 
bd 命令禁用特殊断点。它接收断点号作为参数。该命令不是从断点表中除去断点,而只是禁用它。断点号从 0 开始,根据可用性顺序分配给断点。 
be 命令启用断点。该命令的参数也是断点号。 
bl 命令列出当前的断点集。它包含了启用的和禁用的断点。 
bc 命令从断点表中除去断点。它以具体的断点号或 * 作为参数,在后一种情况下它将除去所有断点。 

示例 
对函数 sys_write() 设置断点:
1 [0]kdb> bp sys_write
列出断点表中的所有断点:
1 [0]kdb> bl
清除断点号 1:
1 [0]kdb> bc 1

4.4  堆栈跟踪 
主要的堆栈跟踪命令有 bt 、 btp 、 btc 和 bta 。 
bt 命令设法提供有关当前线程的堆栈的信息。它可以有选择地将堆栈帧地址作为参数。如果没有提供地址,那么它采用当前寄存器来回溯堆栈。否则,它假定所提供的地址是有效的堆栈帧起始地址并设法进行回溯。如果内核编译期间设置了CONFIG_FRAME_POINTER 选项,那么就用帧指针寄存器来维护堆栈,从而就可以正确地执行堆栈回溯。如果没有设置CONFIG_FRAME_POINTER ,那么 bt 命令可能会产生错误的结果。 
btp 命令将进程标识作为参数,并对这个特定进程进行堆栈回溯。 
btc 命令对每个活动 CPU 上正在运行的进程执行堆栈回溯。它从第一个活动 CPU 开始执行 bt ,然后切换到下一个活动 CPU,以此类推。 
bta 命令对处于某种特定状态的所有进程执行回溯。若不带任何参数,它就对所有进程执行回溯。可以有选择地将各种参数传递给该命令。将根据参数处理处于特定状态的进程。选项以及相应的状态如下: 
  • D:不可中断状态
  • R:正运行
  • S:可中断休眠
  • T:已跟踪或已停止
  • Z:僵死
  • U:不可运行
这类命令中的每一个都会打印出一大堆信息。示例 

跟踪当前活动线程的堆栈:
1 [0]kdb> bt
跟踪标识为 575 的进程的堆栈:
1 [0]kdb> btp 575

4.5  其它命令 
下面是在内核调试过程中非常有用的其它几个 KDB 命令。 
id 命令以一个地址/符号作为参数,它对从该地址开始的指令进行反汇编。环境变量 IDCOUNT 确定要显示多少行输出。 
ss 命令单步执行指令然后将控制返回给 KDB。该指令的一个变体是 ssb ,它执行从当前指令指针地址开始的指令(在屏幕上打印指令),直到它遇到将引起分支转移的指令为止。分支转移指令的典型示例有 call 、 return 和 jump 。 
go 命令让系统继续正常执行。一直执行到遇到断点为止(如果已应用了一个断点的话)。 
reboot 命令立刻重新引导系统。它并没有彻底关闭系统,因此结果是不可预测的。 
ll 命令以地址、偏移量和另一个 KDB 命令作为参数。它对链表中的每个元素反复执行作为参数的这个命令。所执行的命令以列表中当前元素的地址作为参数。 
示例 
反汇编从例程 schedule 开始的指令。所显示的行数取决于环境变量 IDCOUNT :
1 [0]kdb> id schedule
执行指令直到它遇到分支转移条件(在本例中为指令 jne )为止:
1 [0]kdb> ssb
2 0xc0105355  default_idle+0x25:  cli
3 0xc0105356  default_idle+0x26:  mov  0x14(%edx),%eax
4 0xc0105359  default_idle+0x29:  test %eax, %eax
5 0xc010535b  default_idle+0x2b:  jne  0xc0105361 default_idle+0x31  

5  技巧和诀窍
调试一个问题涉及到:使用调试器(或任何其它工具)找到问题的根源以及使用源代码来跟踪导致问题的根源。单单使用源代码来确定问题是极其困难的,只有老练的内核黑客才有可能做得到。相反,大多数的新手往往要过多地依靠调试器来修正错误。这种方法可能会产生不正确的问题解决方案。我们担心的是这种方法只会修正表面症状而不能解决真正的问题。此类错误的典型示例是添加错误处理代码以处理 NULL 指针或错误的引用,却没有查出无效引用的真正原因。 
结合研究代码和使用调试工具这两种方法是识别和修正问题的最佳方案。 
调试器的主要用途是找到错误的位置、确认症状(在某些情况下还有起因)、确定变量的值,以及确定程序是如何出现这种情况的(即,建立调用堆栈)。有经验的黑客会知道对于某种特定的问题应使用哪一个调试器,并且能迅速地根据调试获取必要的信息,然后继续分析代码以识别起因。 
因此,这里为您介绍了一些技巧,以便您能使用 KDB 快速地取得上述结果。当然,要记住,调试的速度和精确度来自经验、实践和良好的系统知识(硬件和内核内部机理等)。 

5.1  技巧 #1 
在 KDB 中,在提示处输入地址将返回与之最为匹配的符号。这在堆栈分析以及确定全局数据的地址/值和函数地址方面极其有用。同样,输入符号名则返回其虚拟地址。 
示例 

表明函数 sys_read 从地址 0xc013db4c 开始:
1 [0]kdb> 0xc013db4c
2 0xc013db4c = 0xc013db4c (sys_read)
同样,表明 sys_write 位于地址 0xc013dcc8:
1 [0]kdb> sys_write
2 sys_write = 0xc013dcc8 (sys_write) 
这些有助于在分析堆栈时找到全局数据和函数地址。 

5.2  技巧 #2 
在编译带 KDB 的内核时,只要 CONFIG_FRAME_POINTER 选项出现就使用该选项。为此,需要在配置内核时选择“Kernel hacking”部分下面的“Compile the kernel with frame pointers”选项。这确保了帧指针寄存器将被用作帧指针,从而产生正确的回溯。实际上,您可以手工转储帧指针寄存器的内容并跟踪整个堆栈。例如,在 i386 机器上,%ebp 寄存器可以用来回溯整个堆栈。 
例如,在函数 rmqueue() 上执行第一个指令后,堆栈看上去类似于下面这样:
01 [0]kdb> md %ebp
02 0xc74c9f38 c74c9f60  c0136c40 000001f0 00000000
03 0xc74c9f48 08053328 c0425238 c04253a8 00000000
04 0xc74c9f58 000001f0  00000246 c74c9f6c c0136a25
05 0xc74c9f68 c74c8000  c74c9f74  c0136d6d c74c9fbc
06 0xc74c9f78 c014fe45  c74c8000  00000000 08053328
07 [0]kdb> 0xc0136c40
08 0xc0136c40 = 0xc0136c40 (__alloc_pages +0x44)
09 [0]kdb> 0xc0136a25
10 0xc0136a25 = 0xc0136a25 (_alloc_pages +0x19)
11 [0]kdb> 0xc0136d6d
12 0xc0136d6d = 0xc0136d6d (__get_free_pages +0xd)  
我们可以看到 rmqueue() 被 __alloc_pages 调用,后者接下来又被 _alloc_pages 调用,以此类推。 
每一帧的第一个双字(double word)指向下一帧,这后面紧跟着调用函数的地址。因此,跟踪堆栈就变成一件轻松的工作了。 

5.3  技巧 #3 
go 命令可以有选择地以一个地址作为参数。如果您想在某个特定地址处继续执行,则可以提供该地址作为参数。另一个办法是使用rm 命令修改指令指针寄存器,然后只要输入 go 。如果您想跳过似乎会引起问题的某个特定指令或一组指令,这就会很有用。但是,请注意,该指令使用不慎会造成严重的问题,系统可能会严重崩溃。 

5.4  技巧 #4 
您可以利用一个名为 defcmd 的有用命令来定义自己的命令集。例如,每当遇到断点时,您可能希望能同时检查某个特殊变量、检查某些寄存器的内容并转储堆栈。通常,您必须要输入一系列命令,以便能同时执行所有这些工作。 defcmd 允许您定义自己的命令,该命令可以包含一个或多个预定义的 KDB 命令。然后只需要用一个命令就可以完成所有这三项工作。其语法如下:
1 [0]kdb> defcmd name "usage" "help"
2 [0]kdb> [defcmd] type the commands here
3 [0]kdb> [defcmd] endefcmd   
例如,可以定义一个(简单的)新命令 hari ,它显示从地址 0xc000000 开始的一行内存、显示寄存器的内容并转储堆栈:
1 [0]kdb> defcmd hari "" "no arguments needed"
2 [0]kdb> [defcmd] md 0xc000000 1
3 [0]kdb> [defcmd] rd
4 [0]kdb> [defcmd] md %ebp 1
5 [0]kdb> [defcmd] endefcmd  
该命令的输出会是:
01 [0]kdb> hari
02 [hari]kdb> md 0xc000000 1
03 0xc000000 00000001 f000e816 f000e2c3 f000e816
04 [hari]kdb> rd
05 eax = 0x00000000 ebx = 0xc0105330 ecx = 0xc0466000 edx = 0xc0466000
06 ....
07 ...
08 [hari]kdb> md %ebp 1
09 0xc0467fbc c0467fd0 c01053d2 00000002 000a0200
10 [0]kdb>    

5.5  技巧 #5 
可以使用 bph 和 bpha 命令(假如体系结构支持使用硬件寄存器)来应用读写断点。这意味着每当从某个特定地址读取数据或将数据写入该地址时,我们都可以对此进行控制。当调试数据/内存毁坏问题时这可能会极其方便,在这种情况中您可以用它来识别毁坏的代码/进程。 
示例 
每当将四个字节写入地址 0xc0204060 时就进入内核调试器:
1 [0]kdb> bph 0xc0204060 dataw 4
在读取从 0xc000000 开始的至少两个字节的数据时进入内核调试器:
1 [0]kdb> bph 0xc000000 datar 2

6  结束语
对于执行内核调试,KDB 是一个方便的且功能强大的工具。它提供了各种选项,并且使我们能够分析内存内容和数据结构。最妙的是,它不需要用另一台机器来执行调试。 

参考: 
Linux 内核调试器内幕 KDB入门指南 

十二  Kprobes

Kprobes 是 Linux 中的一个简单的轻量级装置,让您可以将断点插入到正在运行的内核之中。 Kprobes 提供了一个强行进入任何内核例程并从中断处理器无干扰地收集信息的接口。使用 Kprobes 可以 轻松地收集处理器寄存器和全局数据结构等调试信息。开发者甚至可以使用 Kprobes 来修改 寄存器值和全局数据结构的值。 
为完成这一任务,Kprobes 向运行的内核中给定地址写入断点指令,插入一个探测器。 执行被探测的指令会导致断点错误。Kprobes 钩住(hook in)断点处理器并收集调试信息。Kprobes 甚至可以单步执行被探测的指令。 

1  安装
要安装 Kprobes,需要从 Kprobes 主页下载最新的补丁。 打包的文件名称类似于 kprobes-2.6.8-rc1.tar.gz。解开补丁并将其安装到 Linux 内核: 
1 $tar -xvzf kprobes-2.6.8-rc1.tar.gz
2 $cd /usr/src/linux-2.6.8-rc1
3 $patch -p1 < ../kprobes-2.6.8-rc1-base.patch
Kprobes 利用了 SysRq 键,这个 DOS 时代的产物在 Linux 中有了新的用武之地。您可以在 Scroll Lock键左边找到 SysRq 键;它通常标识为 Print Screen。要为 Kprobes 启用 SysRq 键,需要安装 kprobes-2.6.8-rc1-sysrq.patch 补丁:
1 $patch -p1 < ../kprobes-2.6.8-rc1-sysrq.patch
使用 make xconfig/ make menuconfig/ make oldconfig 配置内核,并 启用 CONFIG_KPROBES 和 CONFIG_MAGIC_SYSRQ标记。 编译并引导到新内核。您现在就已经准备就绪,可以插入 printk 并通过编写简单的 Kprobes 模块来动态而且无干扰地 收集调试信息。 

2  编写 Kprobes 模块
对于每一个探测器,您都要分配一个结构体 struct kprobe kp; (参考 include/linux/kprobes.h 以获得关于此数据结构的详细信息)。 

清单 9. 定义 pre、post 和 fault 处理器
01 /* pre_handler: this is called just before the probed instruction is
02  * executed.
03  */
04 int handler_pre(struct kprobe *p, struct pt_regs *regs) {
05 printk("pre_handler: p->addr=0x%p, eflags=0x%lx\n",p->addr,
06 regs->eflags);
07 return 0;
08 }
09 /* post_handler: this is called after the probed instruction is executed
10  * (provided no exception is generated).
11  */
12 void handler_post(struct kprobe *p, struct pt_regs *regs, unsigned long flags) {
13 printk("post_handler: p->addr=0x%p, eflags=0x%lx \n", p->addr,
14 regs->eflags);
15 }
16 /* fault_handler: this is called if an exception is generated for any
17  * instruction within the fault-handler, or when Kprobes
18  * single-steps the probed instruction.
19  */
20 int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr) {
21 printk("fault_handler:p->addr=0x%p, eflags=0x%lx\n", p->addr,
22 regs->eflags);
23 return 0;
24 }

2.1  获得内核例程的地址 
在注册过程中,您还需要指定插入探测器的内核例程的地址。使用这些方法中的任意一个来获得内核例程 的地址: 
  1. 从 System.map 文件直接得到地址。
  2. 例如,要得到 do_fork 的地址,可以在命令行执行 $grep do_fork /usr/src/linux/System.map 。
  3. 使用 nm 命令。
  4. $nm vmlinuz |grep do_fork
  5. 从 /proc/kallsyms 文件获得地址。
  6. $cat /proc/kallsyms |grep do_fork
  7. 使用 kallsyms_lookup_name() 例程。
  8. 这个例程是在 kernel/kallsyms.c 文件中定义的,要使用它,必须启用 CONFIG_KALLSYMS 编译内核。kallsyms_lookup_name() 接受一个字符串格式内核例程名, 返回那个内核例程的地址。例如:kallsyms_lookup_name("do_fork");
然后在 init_moudle 中注册您的探测器: 

清单 10. 注册一个探测器
01 /* specify pre_handler address
02  */
03 kp.pre_handler=handler_pre;
04 /* specify post_handler address
05  */
06 kp.post_handler=handler_post;
07 /* specify fault_handler address
08  */
09 kp.fault_handler=handler_fault;
10 /* specify the address/offset where you want to insert probe.
11  * You can get the address using one of the methods described above.
12  */
13 kp.addr = (kprobe_opcode_t *) kallsyms_lookup_name("do_fork");
14 /* check if the kallsyms_lookup_name() returned the correct value.
15  */
16 if (kp.add == NULL) {
17 printk("kallsyms_lookup_name could not find address
18 for the specified symbol name\n");
19 return 1;
20 }
21 /* or specify address directly.
22  * $grep "do_fork" /usr/src/linux/System.map
23  * or
24  * $cat /proc/kallsyms |grep do_fork
25  * or
26  * $nm vmlinuz |grep do_fork
27  */
28 kp.addr = (kprobe_opcode_t *) 0xc01441d0;
29 /* All set to register with Kprobes
30  */
31        register_kprobe(&kp);
一旦注册了探测器,运行任何 shell 命令都会导致一个对 do_fork 的调用,您将可以在控制台上或者运行 dmesg 命令来查看您的 printk。做完后要记得注销探测器: 
unregister_kprobe(&kp); 
下面的输出显示了 kprobe 的地址以及 eflags 寄存器的内容: 

$tail -5 /var/log/messages 
Jun 14 18:21:18 llm05 kernel: pre_handler: p->addr=0xc01441d0, eflags=0x202 
Jun 14 18:21:18 llm05 kernel: post_handler: p->addr=0xc01441d0, eflags=0x196 

2.2  获得偏移量 
您可以在例程的开头或者函数中的任意偏移位置插入 printk(偏移量必须在指令范围之内)。 下面的代码示例展示了如何来计算偏移量。首先,从对象文件中反汇编机器指令,并将它们 保存为一个文件: 
1 $objdump -D /usr/src/linux/kernel/fork.o > fork.dis
其结果是: 

清单 11. 反汇编的 fork
01 000022b0 <do_fork>:
02    22b0:       55                      push   %ebp
03    22b1:       89 e5                   mov    %esp,%ebp
04    22b3:       57                      push   %edi
05    22b4:       89 c7                   mov    %eax,%edi
06    22b6:       56                      push   %esi
07    22b7:       89 d6                   mov    %edx,%esi
08    22b9:       53                      push   %ebx
09    22ba:       83 ec 38                sub    $0x38,%esp
10    22bd:       c7 45 d0 00 00 00 00    movl   $0x0,0xffffffd0(%ebp)
11    22c4:       89 cb                   mov    %ecx,%ebx
12    22c6:       89 44 24 04             mov    %eax,0x4(%esp)
13    22ca:       c7 04 24 0a 00 00 00    movl   $0xa,(%esp)
14    22d1:       e8 fc ff ff ff          call   22d2 <do_fork+0x22>
15    22d6:       b8 00 e0 ff ff          mov    $0xffffe000,%eax
16    22db:       21 e0                   and    %esp,%eax
17    22dd:       8b 00                   mov    (%eax),%eax
要在偏移位置 0x22c4 插入探测器,先要得到与例程的开始处相对的偏移量 0x22c4 - 0x22b0 = 0x14 ,然后将这个偏移量添加到 do_fork 的地址 0xc01441d0 + 0x14 。(运行 $cat /proc/kallsyms | grep do_fork 命令以获得 do_fork 的地址。) 
您还可以将 do_fork 的相对偏移量 0x22c4 - 0x22b0 = 0x14 添加到 kallsyms_lookup_name("do_fork"); 的输入,即:0x14 + kallsyms_lookup_name("do_fork"); 

2.3  转储内核数据结构 
现在,让我们使用修改过的用来转储数据结构的 Kprobe post_handler 来转储运行在系统上的所有作业的一些组成部分: 

清单 12. 用来转储数据结构的修改过的 Kprope post_handler
01 void handler_post(struct kprobe *p, struct pt_regs *regs, unsigned long flags) {
02 struct task_struct *task;
03 read_lock(&tasklist_lock);
04 for_each_process(task) {
05 printk("pid =%x task-info_ptr=%lx\n", task->pid,
06 task->thread_info);
07 printk("thread-info element status=%lx,flags=%lx, cpu=%lx\n",
08 task->thread_info->status, task->thread_info->flags,
09 task->thread_info->cpu);
10 }
11 read_unlock(&tasklist_lock);
12 }
这个模块应该插入到 do_fork 的偏移位置。 

清单 13. pid 1508 和 1509 的结构体 thread_info 的输出
1 $tail -10 /var/log/messages
2 Jun 22 18:14:25 llm05 kernel: thread-info element status=0,flags=0, cpu=1
3 Jun 22 18:14:25 llm05 kernel: pid =5e4 task-info_ptr=f5948000
4 Jun 22 18:14:25 llm05 kernel: thread-info element status=0,flags=8, cpu=0
5 Jun 22 18:14:25 llm05 kernel: pid =5e5 task-info_ptr=f5eca000

2.4  启用奇妙的 SysRq 键 
为了支持 SysRq 键,我们已经进行了编译。这样来启用它: 
1 $echo 1 > /proc/sys/kernel/sysrq
现在,您可以使用 Alt+SysRq+W 在控制台上或者到 /var/log/messages 中去查看所有插入的内核探测器。 

清单 14. /var/log/messages 显示出在 do_fork 插入了一个 Kprobe
1 Jun 23 10:24:48 linux-udp4749545uds kernel: SysRq : Show kprobes
2 Jun 23 10:24:48 linux-udp4749545uds kernel:
3 Jun 23 10:24:48 linux-udp4749545uds kernel: [<c011ea60>] do_fork+0x0/0x1de

3  使用 Kprobes 更好地进行调试
由于探测器事件处理器是作为系统断点中断处理器的扩展来运行,所以它们很少或者根本不依赖于系统 工具 —— 这样可以被植入到大部分不友好的环境中(从中断时间和任务时间到禁用的上下文间切换和支持 SMP 的代码路径)—— 都不会对系统性能带来负面影响。 
使用 Kprobes 的好处有很多。不需要重新编译和重新引导内核就可以插入 printk。为了进行调试可以记录 处理器寄存器的日志,甚至进行修改 —— 不会干扰系统。类似地,同样可以无干扰地记录 Linux 内核数据结构的日志,甚至 进行修改。您甚至可以使用 Kprobes 调试 SMP 系统上的竞态条件 —— 避免了您自己重新编译和重新引导的所有 麻烦。您将发现内核调试比以往更为快速和简单。 

参考: 
使用 Kprobes 调试内核 

本文参考: 
1  Linux内核设计与实现   P243 第十八章 调试 
3   linux内核调试指南
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值