调试内核问题时,才能跟踪内核执行情况并查看其显存和数据结构是十分有用的。Linux中的外置内核调试器KDB提供了这些功能。在本文中您将了解怎样使用KDB所提供的功能,以及怎样在Linux机器上安装和设置KDB。您还将熟悉KDB中可以使用的以及设置和显示选项。
Linux内核调试器(KDB)准许您调试Linux内核。这个恰如其名的工具实质上是内核代码的补丁,它容许前辈访问内核显存和数据结构。KDB的主要优点之一就是它不须要用另一台机器进行调试:您可以调试正在运行的内核。
设置一台用于KDB的机器须要耗费一些工作,由于须要给内核打补丁并进行重新编译。KDB的用户应该熟悉Linux内核的编译(在一定程度上还要熟悉内核内部机理),而且假如您须要编译内核方面的帮助,请参阅本文结尾处的参考资料一节。
在本文中,我们将从有关下载KDB补丁、打补丁、(重新)编译内核以及启动KDB方面的信息着手。之后我们将了解KDB并研究一些较常用的。最后,我们将研究一下有关设置和显示选项方面的一些详尽信息。
入门
KDB项目是由SiliconGraphics维护的(请参阅参考资料以获取链接),您须要从它的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压缩的文件解压缩补丁文件:
#bzip2-dkdb-v4.2-2.4.20-common-1.bz2
#bzip2-dkdb-v4.2-2.4.20-i386-1.bz2
您将获得kdb-v4.2-2.4.20-common-1和kdb-v4.2-2.4-i386-1文件。
现今,应用这种补丁:
#patch-p1
#patch-p1
这种补丁应当干净利落地加以应用。查找任何以.rej结尾的文件。这个扩充名表明这种是失败的补丁。假如内核树没问题,这么补丁的应用就不会有任何问题。
接出来,须要建立内核以支持KDB。第一步是设置CONFIG_KDB选项。使用您喜欢的配置机制(xconfig和menuconfig等)来完成这一步。转入结尾处的“Kernelhacking”部分并选择“Built-inKernelDebuggersupport”选项。
您还可以依照自己的偏好选择其它两个选项。选择“Compilethekernelwithframepointers”选项(假如有的话)则设置CONFIG_FRAME_POINTER标志。这将形成更好的堆栈回溯,由于帧表针寄存器被用作帧表针而不是通用寄存器。您还可以选择“KDBoffbydefault”选项。这将设置CONFIG_KDB_OFF标志,但是在缺省情况下将关掉KDB。我们将在前面一节中对此进行详尽介绍。
保存配置,之后退出。重新编译内核。建议在建立内核之前执行“makeclean”。用常用方法安装内核并引导它。
初始化并设置环境变量
您可以定义将在KDB初始化期间执行的KDB命令。须要在纯文本文件kdb_cmds中定义这种命令,该文件坐落Linux源代码树(其实是在打了补丁以后)的KDB目录中。该文件还可以拿来定义设置显示和复印选项的环境变量。文件开头的注释提供了编辑文件方面的帮助。使用这个文件的缺点是,在您修改了文件以后须要重新建立并重新安装内核。
激活KDB
假如编译期间没有选中CONFIG_KDB_OFF如何安装LINUX,这么在缺省情况下KDB是活动的。否则,您须要显式地激活它-通过在引导期间将kdb=on标志传递给内核或则通过在挂装了/proc以后执行该工作:
#echo"1">/proc/sys/kernel/kdb
倒过来执行上述步骤则会取消激活KDB。也就是说,假如缺省情况下KDB是打开的,这么将kdb=off标志传递给内核或则执行下边这个操作将会取消激活KDB:
#echo"0">/proc/sys/kernel/kdb
在引导期间还可以将另一个标志传递给内核。kdb=early标志将造成在引导过程的初始阶段就把控制权传递给KDB。假如您须要在引导过程初始阶段进行调试,这么这将有所帮助。
调用KDB的形式有好多。假如KDB处于打开状态linux服务器配置与管理,这么只要内核中有紧急情况就手动调用它。按下按键上的PAUSE键将手工调用KDB。调用KDB的另一种方法是通过串行控制台。其实,要做到这一点,须要设置串行控制台(请参阅参考资料以获取这方面的帮助)而且须要一个从串行控制台进行读取的程序。键盘序列Ctrl-A将从串行控制台调用KDB。
KDB命令
KDB是一个功能十分强悍的工具,它容许进行几个操作,例如显存和寄存器更改、应用断点和堆栈跟踪。按照这种,可以将KDB命令分成几个类别。下边是有关每一类中最常用命令的详尽信息。
显存显示和更改
这一类别中最常用的命令是md、mdr、mm和mmW。
md命令以一个地址/符号和行计数为参数,显示从该地址开始的line-count行的显存。若果没有指定line-count,这么就使用环境变量所指定的缺省值。若果没有指定地址,这么md就从上一次复印的地址继续。地址复印在开头,字符转换复印在结尾。
mdr命令带有地址/符号以及字节计数,显示从指定的地址开始的byte-count字节数的初始显存内容。它本质上和md一样,而且它不显示起始地址但是不在结尾显示字符转换。mdr命令较少使用。
mm命令更改显存内容。它以地址/符号和新内容作为参数,用new-contents替换地址处的内容。
mmW命令修改从地址开始的W个字节。请注意,mm修改一个机器字。
{{分页}}
示例
显示从0xcxc000000开始的15行显存:
[0]kdb>md0xcxc00000015
将显存位置为0xcxc000000上的内容修改为0x10:
[0]kdb>mm0xcxc0000000x10
寄存器显示和更改
这一类别中的命令有rd、rm和ef。
rd命令(不带任何参数)显示处理器寄存器的内容。它可以有选择地带三个参数。假如传递了c参数,则rd显示处理器的控制寄存器;假如带有d参数,这么它就显示调试寄存器;假如带有u参数,则显示上一次步入内核的当前任务的寄存器组。
rm命令更改寄存器的内容。它以寄存器名称和new-contents作为参数,用new-contents更改寄存器。寄存器名称与特定的体系结构有关。目前,不能更改控制寄存器。
ef命令以一个地址作为参数,它显示指定地址处的异常帧。
示例
显示通用寄存器组:
[0]kdb>rd
[0]kdb>rm%ebx0x25
断点
常用的断点命令有bp、bc、bd、be和bl。
bp命令以一个地址/符号作为参数,它在地址处应用断点。当遇见该断点时则停止执行并将控制权交予KDB。该命令有几个有用的变体。bpa命令对SMP中的所有处理器应用断点。bph命令强制在支持硬件寄存器的系统上使用它。bpha命令类似于bpa命令,差异在于它强制使用硬件寄存器。
bd命令禁用特殊断点。它接收断点号作为参数。该命令不是从断点表中去除断点,而只是禁用它。断点号从0开始,按照可用性次序分配给断点。
be命令启用断点。该命令的参数也是断点号。
bl命令列举当前的断点集。它包含了启用的和禁用的断点。
bc命令从断点表中去除断点。它以具体的断点号或*作为参数,在后一种情况下它将去除所有断点。
示例
对函数sys_write()设置断点:
[0]kdb>bpsys_write
列举断点表中的所有断点:
[0]kdb>bl
去除断点号1:
[0]kdb>bc1
>堆栈跟踪
主要的堆栈跟踪命令有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:不可运行
这类命令中的每一个就会复印出一大堆信息。请查阅下边的参考资料以获取这种数组的详尽文档。
示例
跟踪当前活动线程的堆栈:
[0]kdb>bt
跟踪标示为575的进程的堆栈:
[0]kdb>btp575
其它命令
下边是在内核调试过程中十分有用的其它几个KDB命令。
id命令以一个地址/符号作为参数,它对从该地址开始的指令进行反汇编。环境变量IDCOUNT确定要显示多少行输出。
ss命令单步执行指令之后将控制返回给KDB。该指令的一个变体是ssb,它执行从当前指令表针地址开始的指令(在屏幕上复印指令),直至它碰到将导致分支转移的指令为止。分支转移指令的典型示例有call、return和jump。
go命令让系统继续正常执行。仍然执行到遇见断点为止(假如已应用了一个断点的话)。
reboot命令立即重新引导系统。它并没有彻底关掉系统,因而结果是不可预测的。
ll命令以地址、偏移量和另一个KDB命令作为参数。它对数组中的每位元素反复执行作为参数的这个命令。所执行的命令以列表中当前元素的地址作为参数。
示例
反汇编从类库schedule开始的指令。所显示的行数取决于环境变量IDCOUNT:
[0]kdb>idschedule
执行指令直至它碰到分支转移条件(在本例中为指令jne)为止:
{{分页}}
[0]kdb>ssb
0xc0105355default_idle+0x25:cli
0xc0105356default_idle+0x26:mov0x14(%edx),%eax
0xc0105359default_idle+0x29:test%eax,%eax
0xc010535bdefault_idle+0x2b:jne0xcxc0105361default_idle+0x31
方法和诀窍
调试一个问题涉及到:使用调试器(或任何其它工具)找到问题的症结以及使用源代码来跟踪造成问题的症结。单单使用源代码来确定问题是非常困难的,只有老练的内核黑客才有可能做得到。相反,大多数的菜鸟常常要过多地借助调试器来修正错误。这些方式可能会形成不正确的问题解决方案。我们担忧的是这些方式只会修正表面病症而不能解决真正的问题。这种错误的典型示例是添加错误处理代码以处理NULL表针或错误的引用,却没有查出无效引用的真正缘由。
结合研究代码和使用调试工具这两种方式是辨识和修正问题的最佳方案。
调试器的主要用途是找到错误的位置、确认病症(在个别情况下还有起因)、确定变量的值,以及确定程序是怎样出现此类情况的(即linux修改内核参数,构建调用堆栈)。有经验的黑客会晓得对于某种特定的问题应使用哪一个调试器,而且能迅速地按照调试获取必要的信息,之后继续剖析代码以辨识起因。
为此,这儿为您介绍了一些方法,便于您能使用KDB快速地取得上述结果。其实,要记住,调试的速率和精确度来自经验、实践和良好的系统知识(硬件和内核内部机理等)。
方法#1
在KDB中,在提示处输入地址将返回与之最为匹配的符号。这在堆栈剖析以及确定全局数据的地址/值和函数地址方面十分有用。同样,输入符号名则返回其虚拟地址。
示例
表明函数sys_read从地址0xcxc013dbdb44c开始:
[0]kdb>0xc013db4c
0xc013db4c=0xcxc013dbdb44c(sys_read)
同样,
同样,表明sys_write坐落地址0xcxc013dccdcc8:
[0]kdb>sys_write
sys_write=0xcxc013dccdcc8(sys_write)
这种有助于在剖析堆栈时找到全局数据和函数地址。
方法#2
在编译带KDB的内核时,只要CONFIG_FRAME_POINTER选项出现就使用该选项。因此,须要在配置内核时选择“Kernelhacking”部分下边的“Compilethekernelwithframepointers”选项。这确保了帧表针寄存器将被用作帧表针,因而形成正确的回溯。实际上,您可以手工轮询帧表针寄存器的内容并跟踪整个堆栈。比如,在i386机器上,%ebp寄存器可以拿来回溯整个堆栈。
比如,在函数rmqueue()上执行第一个指令后,堆栈看起来类似于下边这样:
[0]kdb>md%ebp
0xc74c9f38c74c9f60c0136c40000001f000000000
0xc74c9f4808053328c0425238c04253a800000000
0xc74c9f58000001f000000246c74c9f6cc0136a25
0xc74c9f68c74c8000c74c9f74c0136d6dc74c9fbc
0xc74c9f78c014014fe45c74c808053328
[0]kdb>0xc0136c40
0xc0136c40=0xcxc01360136cc4040(__alloc_pages+0x44)
[0]kdb>0xc0136a25
0xc0136a25=0xcxc01360136aa2525(_alloc_pages+0x19)
[0]kdb>0xc0136d6d
0xc0136d6d=0xcxc01360136dd66d(__get_free_pages+0xd)
我们可以看见rmqueue()被__alloc_pages调用,前者接出来又被_alloc_pages调用,以这种推。
每一帧的第一个双字(doubleword)指向下一帧,这前面紧跟随调用函数的地址。因而,跟踪堆栈就弄成一件轻松的工作了。
方法#3
go命令可以有选择地以一个地址作为参数。倘若您想在某个特定地址处继续执行,则可以提供该地址作为参数。另一个办法是使用rm命令更改指令表针寄存器,之后只要输入go。倘若您想跳过虽然会造成问题的某个特定指令或一组指令,这都会很有用。并且,请注意,该指令使用不慎会导致严重的问题,系统可能会严重崩溃。
方法#4
您可以借助一个名为defcmd的有用命令来定义自己的命令集。诸如,每每遇见断点时,您可能希望能同时检测某个特殊变量、检查个别寄存器的内容并存贮堆栈。一般,您必需要输入一系列命令,便于能同时执行所有那些工作。defcmd容许您定义自己的命令,该命令可以包含一个或多个预定义的KDB命令。之后只须要用一个命令就可以完成所有这三项工作。其句型如下:
[0]kdb>defcmdname"usage""help"
[0]kdb>[defcmd]typethecommandshere
[0]kdb>[defcmd]endefcmd
比如,可以定义一个(简单的)新命令hari,它显示从地址0xcxc000000开始的一行显存、显示寄存器的内容并存贮堆栈:
[0]kdb>defcmdhari"""noargumentsneeded"
[0]kdb>[defcmd]md0xcxc0000001
[0]kdb>[defcmd]rd
[0]kdb>[defcmd]md%ebp1
[0]kdb>[defcmd]endefcmd
该命令的输出会是:
[0]kdb>hari
[hari]kdb>md0xcxc0000001
0xc000f000e816f000e2c3f000e816
[hari]kdb>rd
eax=0x00000000ebx=0xcxc0105330ecx=0xcxc0466000edx=0xc0466000
....
...
[hari]kdb>md%ebp1
0xc0467fbcc04670467fdfd0c01053d200000002000a0200
[0]kdb>
{{分页}}
方法#5
可以使用bph和bpha命令(如果体系结构支持使用硬件寄存器)来应用读写断点。这意味着每每从某个特定地址读取数据或将数据写入该地址时,我们都可以对此进行控制。当调试数据/显存损坏问题时这可能会非常便捷,在这些情况中您可以用它来辨识损坏的代码/进程。
示例
每每将四个字节写入地址0xcxc0204060时就步入内核调试器:
[0]kdb>bph0xcxc0204060dataw4
在读取从0xcxc000000开始的起码两个字节的数据时步入内核调试器:
[0]kdb>bph0xcxc000000datar2
结束语
对于执行内核调试,KDB是一个便捷的且功能强悍的工具。它提供了各类选项,但是使我们能否剖析显存内容和数据结构。最妙的是,它不须要用另一台机器来执行调试。
参考资料
您可以参阅本文在developerWorks全球站点上的英语原文.
请在Documentation/kdb目录中查找KDB指南页。
有关设置串行控制台的信息,请查找Documentation目录中的serial-console.txt。
请在SGI的内核调试器项目网站上下载KDB。
有关几个基于方案的Linux调试技术的概述linux修改内核参数,请阅读“掌握Linux调试技术”(developerWorks,2002年8月)。
教程“编译Linux内核”(developerWorks,2000年8月)让您完整地了解配置、编译和安装内核的过程。
IBMAIX用户可以在KDBKernelDebuggerandCommand页面上获取有关用于AIX的KDB的使用帮助。
这些寻求有关调试OS/2信息的读者应当阅读IBM红皮书TheOS/2DebuggingHandbook(共四卷)的第II卷。
在developerWorksLinux专区中查找更多针对Linux开发人员的参考资料。
关于作者
HariprasadNellitheertha在美国旧金山(Bangalore)的IBMLinux技术中心工作。他目前正在LinuxChangeTeam从事修正内核和其它Linux错误的工作。Hari研究过OS/2内核和文件系统。他的兴趣包括Linux内核内部机理、文件系统和自主估算。可以通过与Hari联系。
本文原创地址://q13zd.cn/lnhtsqdszhxs.html编辑:刘遄,审核员:暂无