原文地址:[url]http://www-900.ibm.com/developerWorks/cn/Linux/l-kdbug/index.sHtml[/url]
内容:
[size=18:6ddc15f4ad]入门
初始化并设置环境变量
激活 KDB
KDB 命令
技巧和诀窍
结束语
参考资料[/size:6ddc15f4ad]
[size=18:6ddc15f4ad]KDB 入门指南[/size:6ddc15f4ad]
Hariprasad Nellitheertha(
[email protected])
软件工程师,IBM
2003 年 9 月
调试内核问题时,能够跟踪内核执行情况并查看其内存和数据结构是非常有用的。Linux 中的内置内核调试器 KDB 提供了这种功能。在本文中您将了解如何使用 KDB 所提供的功能,以及如何在 Linux 机器上安装和设置 KDB。您还将熟悉 KDB 中可以使用的命令以及设置和显示选项。
Linux 内核调试器(KDB)允许您调试 Linux 内核。这个恰如其名的工具实质上是内核代码的补丁,它允许高手访问内核内存和数据结构。KDB 的主要优点之一就是它不需要用另一台机器进行调试:您可以调试正在运行的内核。
设置一台用于 KDB 的机器需要花费一些工作,因为需要给内核打补丁并进行重新编译。KDB 的用户应当熟悉 Linux 内核的编译(在一定程度上还要熟悉内核内部机理),但是如果您需要编译内核方面的帮助,请参阅本文结尾处的参考资料一节。
在本文中,我们将从有关下载 KDB 补丁、打补丁、(重新)编译内核以及启动 KDB 方面的信息着手。然后我们将了解 KDB 命令并研究一些较常用的命令。最后,我们将研究一下有关设置和显示选项方面的一些详细信息。
入门
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 压缩的文件解压缩补丁文件:
[code:1:6ddc15f4ad]#bzip2 -d kdb-v4.2-2.4.20-common-1.bz2
#bzip2 -d kdb-v4.2-2.4.20-i386-1.bz2 [/code:1:6ddc15f4ad]
您将获得 kdb-v4.2-2.4.20-common-1 和 kdb-v4.2-2.4-i386-1 文件。
现在,应用这些补丁:
[code:1:6ddc15f4ad]#patch -p1 <kdb-v4.2-2.4.20-common-1
#patch -p1 <kdb-v4.2-2.4.20-i386-1 [/code:1:6ddc15f4ad]
这些补丁应该干净利落地加以应用。查找任何以 .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。我们将在后面一节中对此进行详细介绍。
[1] [2] [3] [4] [5] [6] [7] [8] [9] [10] 下一页
保存配置,然后退出。重新编译内核。建议在构建内核之前执行“make clean”。用常用方式安装内核并引导它。
[size=18:6ddc15f4ad]初始化并设置环境变量[/size:6ddc15f4ad]
您可以定义将在 KDB 初始化期间执行的 KDB 命令。需要在纯文本文件 kdb_cmds 中定义这些命令,该文件位于 Linux 源代码树(当然是在打了补丁之后)的 KDB 目录中。该文件还可以用来定义设置显示和打印选项的环境变量。文件开头的注释提供了编辑文件方面的帮助。使用这个文件的缺点是,在您更改了文件之后需要重新构建并重新安装内核。
[size=18:6ddc15f4ad]激活 KDB[/size:6ddc15f4ad]
如果编译期间没有选中 CONFIG_KDB_OFF,那么在缺省情况下 KDB 是活动的。否则,您需要显式地激活它 - 通过在引导期间将 kdb=on 标志传递给内核或者通过在挂装了 /proc 之后执行该工作:
[code:1:6ddc15f4ad]#echo "1" >/proc/sys/kernel/kdb [/code:1:6ddc15f4ad]
倒过来执行上述步骤则会取消激活 KDB。也就是说,如果缺省情况下 KDB 是打开的,那么将 kdb=off 标志传递给内核或者执行下面这个操作将会取消激活 KDB:
[code:1:6ddc15f4ad]#echo "0" >/proc/sys/kernel/kdb[/code:1:6ddc15f4ad]
在引导期间还可以将另一个标志传递给内核。kdb=early 标志将导致在引导过程的初始阶段就把控制权传递给 KDB。如果您需要在引导过程初始阶段进行调试,那么这将有所帮助。
调用 KDB 的方式有很多。如果 KDB 处于打开状态,那么只要内核中有紧急情况就自动调用它。按下键盘上的 PAUSE 键将手工调用 KDB。调用 KDB 的另一种方式是通过串行控制台。当然,要做到这一点,需要设置串行控制台(请参阅参考资料以获取这方面的帮助)并且需要一个从串行控制台进行读取的程序。按键序列 Ctrl-A 将从串行控制台调用 KDB。
[size=18:6ddc15f4ad]KDB 命令[/size:6ddc15f4ad]
KDB 是一个功能非常强大的工具,它允许进行几个操作,比如内存和寄存器修改、应用断点和堆栈跟踪。根据这些,可以将 KDB 命令分成几个类别。下面是有关每一类中最常用命令的详细信息。
[size=18:6ddc15f4ad]内存显示和修改[/size:6ddc15f4ad]
这一类别中最常用的命令是 md、mdr、mm 和 mmW。
[size=18:6ddc15f4ad]md[/size:6ddc15f4ad] 命令以一个地址/符号和行计数为参数,显示从该地址开始的 line-count 行的内存。如果没有指定 line-count,那么就使用环境变量所指定的缺省值。如果没有指定地址,那么 md 就从上一次打印的地址继续。地址打印在开头,字符转换打印在结尾。
[size=18:6ddc15f4ad]mdr[/size:6ddc15f4ad] 命令带有地址/符号以及字节计数,显示从指定的地址开始的 byte-count 字节数的初始内存内容。它本质上和 md 一样,但是它不显示起始地址并且不在结尾显示字符转换。mdr 命令较少使用。
[size=18:6ddc15f4ad]mm[/size:6ddc15f4ad] 命令修改内存内容。它以地址/符号和新内容作为参数,用 new-contents 替换地址处的内容。
[size=18:6ddc15f4ad]mmW[/size:6ddc15f4ad] 命令更改从地址开始的 W 个字节。请注意,mm 更改一个机器字。
[size=18:6ddc15f4ad]示例[/size:6ddc15f4ad]
[code:1:6ddc15f4ad]显示从 0xc000000 开始的 15 行内存:
[0]kdb> md 0xc000000 15
将内存位置为 0xc000000 上的内容更改为 0x10:
[0]kdb> mm 0xc000000 0x10 [/code:1:6ddc15f4ad]
[size=18:6ddc15f4ad]寄存器显示和修改[/size:6ddc15f4ad]
这一类别中的命令有 rd、rm 和 ef。
[size=18:6ddc15f4ad]rd[/size:6ddc15f4ad] 命令(不带任何参数)显示处理器寄存器的内容。它可以有选择地带三个参数。如果传递了 c 参数,则 rd 显示处理器的控制寄存器;如果带有 d 参数,那么它就显示调试寄存器;如果带有 u 参数,则显示上一次进入内核的当前任务的寄存器组。
[size=18:6ddc15f4ad]rm[/size:6ddc15f4ad] 命令修改寄存器的内容。它以寄存器名称和 new- contents 作为参数,用 new-contents 修改寄存器。寄存器名称与特定的体系结构有关。目前,不能修改控制寄存器。
[size=18:6ddc15f4ad]ef[/size:6ddc15f4ad] 命令以一个地址作为参数,它显示指定地址处的异常帧。
[size=18:6ddc15f4ad]示例[/size:6ddc15f4ad]
显示通用寄存器组:
[code:1:6ddc15f4ad][0]kdb> rd
将寄存器 ebx 的内容设置成 0x25:
[0]kdb> rm %ebx 0x25 [/code:1:6ddc15f4ad]
[size=18:6ddc15f4ad]断点[/size:6ddc15f4ad]
常用的断点命令有 bp、bc、bd、be 和 bl。
[size=18:6ddc15f4ad]bp[/size:6ddc15f4ad] 命令以一个地址/符号作为参数,它在地址处应用断点。当遇到该断点时则停止执行并将控制权交予 KDB。该命令有几个有用的变体。[size=18:6ddc15f4ad]bpa[/size: 6ddc15f4ad] 命令对 SMP 系统中的所有处理器应用断点。bph 命令强制在支持硬件寄存器的系统上使用它。bpha 命令类似于 bpa 命令,差别在于它强制使用硬件寄存器。
[size=18:6ddc15f4ad]bd[/size:6ddc15f4ad] 命令禁用特殊断点。它接收断点号作为参数。该命令不是从断点表中除去断点,而只是禁用它。断点号从 0 开始,根据可用性顺序分配给断点。
[size=18:6ddc15f4ad]be[/size:6ddc15f4ad] 命令启用断点。该命令的参数也是断点号。
上一页 [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] 下一页
[size=18:6ddc15f4ad]bl [/size:6ddc15f4ad]命令列出当前的断点集。它包含了启用的和禁用的断点。
[size=18:6ddc15f4ad]bc[/size:6ddc15f4ad] 命令从断点表中除去断点。它以具体的断点号或 * 作为参数,在后一种情况下它将除去所有断点。
[size=18:6ddc15f4ad]示例[/size:6ddc15f4ad]
[code:1:6ddc15f4ad]对函数 sys_write() 设置断点:
[0]kdb> bp sys_write
列出断点表中的所有断点:
[0]kdb> bl
清除断点号 1:
[0]kdb> bc 1[/code:1:6ddc15f4ad]
[size=18:6ddc15f4ad]堆栈跟踪[/size:6ddc15f4ad]
主要的堆栈跟踪命令有 BT、btp、btc 和 bta。
[size=18:6ddc15f4ad]bt [/size:6ddc15f4ad]命令设法提供有关当前线程的堆栈的信息。它可以有选择地将堆栈帧地址作为参数。如果没有提供地址,那么它采用当前寄存器来回溯堆栈。否则,它假定所提供的地址是有效的堆栈帧起始地址并设法进行回溯。如果内核编译期间设置了 CONFIG_FRAME_POINTER 选项,那么就用帧指针寄存器来维护堆栈,从而就可以正确地执行堆栈回溯。如果没有设置 CONFIG_FRAME_POINTER,那么 bt 命令可能会产生错误的结果。
[size=18:6ddc15f4ad]btp[/size:6ddc15f4ad] 命令将进程标识作为参数,并对这个特定进程进行堆栈回溯。
[size=18:6ddc15f4ad]btc[/size:6ddc15f4ad] 命令对每个活动 CPU 上正在运行的进程执行堆栈回溯。它从第一个活动 CPU 开始执行 bt,然后切换到下一个活动 CPU,以此类推。
[size=18:6ddc15f4ad]bta[/size:6ddc15f4ad] 命令对处于某种特定状态的所有进程执行回溯。若不带任何参数,它就对所有进程执行回溯。可以有选择地将各种参数传递给该命令。将根据参数处理处于特定状态的进程。选项以及相应的状态如下:
?D:不可中断状态
?R:正运行
?S:可中断休眠
?T:已跟踪或已停止
?Z:僵死
?U:不可运行
这类命令中的每一个都会打印出一大堆信息。请查阅下面的参考资料以获取这些字段的详细文档。
[size=18:6ddc15f4ad]示例[/size:6ddc15f4ad]
[code:1:6ddc15f4ad]跟踪当前活动线程的堆栈:
[0]kdb> bt
跟踪标识为 575 的进程的堆栈:
[0]kdb> btp 575 [/code:1:6ddc15f4ad]
[size=18:6ddc15f4ad]其它命令[/size:6ddc15f4ad]
下面是在内核调试过程中非常有用的其它几个 KDB 命令。
[size=18:6ddc15f4ad]id [/size:6ddc15f4ad]命令以一个地址/符号作为参数,它对从该地址开始的指令进行反汇编。环境变量 IDCOUNT 确定要显示多少行输出。
[size=18:6ddc15f4ad]ss [/size:6ddc15f4ad]命令单步执行指令然后将控制返回给 KDB。该指令的一个变体是 ssb,它执行从当前指令指针地址开始的指令(在屏幕上打印指令),直到它遇到将引起分支转移的指令为止。分支转移指令的典型示例有 call、 return 和 jump。
[size=18:6ddc15f4ad]go[/size:6ddc15f4ad] 命令让系统继续正常执行。一直执行到遇到断点为止(如果已应用了一个断点的话)。
[size=18:6ddc15f4ad]reboot [/size:6ddc15f4ad]命令立刻重新引导系统。它并没有彻底关闭系统,因此结果是不可预测的。
[size=18:6ddc15f4ad]ll[/size:6ddc15f4ad] 命令以地址、偏移量和另一个 KDB 命令作为参数。它对链表中的每个元素反复执行作为参数的这个命令。所执行的命令以列表中当前元素的地址作为参数。
[size=18:6ddc15f4ad]示例[/size:6ddc15f4ad]
[code:1:6ddc15f4ad]反汇编从例程 schedule 开始的指令。所显示的行数取决于环境变量 IDCOUNT:
[0]kdb> id schedule
执行指令直到它遇到分支转移条件(在本例中为指令 jne)为止:
[0]kdb> ssb
0xc0105355 default_idle+0x25: cli
0xc0105356 default_idle+0x26: mov 0x14(%edx),%eax
0xc0105359 default_idle+0x29: test %eax, %eax
0xc010535b default_idle+0x2b: jne 0xc0105361 default_idle+0x31 [/code:1:6ddc15f4ad]
[size=18:6ddc15f4ad]技巧和诀窍[/size:6ddc15f4ad]
调试一个问题涉及到:使用调试器(或任何其它工具)找到问题的根源以及使用源代码来跟踪导致问题的根源。单单使用源代码来确定问题是极其困难的,只有老练的内核黑客才有可能做得到。相反,大多数的新手往往要过多地依靠调试器来修正错误。这种方法可能会产生不正确的问题解决方案。我们担心的是这种方法只会修正表面症状而不能解决真正的问题。此类错误的典型示例是添加错误处理代码以处理 NULL 指针或错误的引用,却没有查出无效引用的真正原因。
结合研究代码和使用调试工具这两种方法是识别和修正问题的最佳方案。
调试器的主要用途是找到错误的位置、确认症状(在某些情况下还有起因)、确定变量的值,以及确定程序是如何出现这种情况的(即,建立调用堆栈)。有经验的黑客会知道对于某种特定的问题应使用哪一个调试器,并且能迅速地根据调试获取必要的信息,然后继续分析代码以识别起因。
因此,这里为您介绍了一些技巧,以便您能使用 KDB 快速地取得上述结果。当然,要记住,调试的速度和精确度来自经验、实践和良好的系统知识(硬件和内核内部机理等)。
上一页 [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] 下一页
[size=18:6ddc15f4ad]技巧 #1[/size:6ddc15f4ad]
在 KDB 中,在提示处输入地址将返回与之最为匹配的符号。这在堆栈分析以及确定全局数据的地址/值和函数地址方面极其有用。同样,输入符号名则返回其虚拟地址。
示例
[code:1:6ddc15f4ad]表明函数 sys_read 从地址 0xc013db4c 开始:
[0]kdb> 0xc013db4c
0xc013db4c = 0xc013db4c (sys_read)
同样,
同样,表明 sys_write 位于地址 0xc013dcc8:
[0]kdb> sys_write
sys_write = 0xc013dcc8 (sys_write)
这些有助于在分析堆栈时找到全局数据和函数地址。[/code:1:6ddc15f4ad]
[size=18:6ddc15f4ad]技巧 #2[/size:6ddc15f4ad]在编译带 KDB 的内核时,只要 CONFIG_FRAME_POINTER 选项出现就使用该选项。为此,需要在配置内核时选择“Kernel hacking”部分下面的 “Compile the kernel with frame pointers”选项。这确保了帧指针寄存器将被用作帧指针,从而产生正确的回溯。实际上,您可以手工转储帧指针寄存器的内容并跟踪整个堆栈。例如,在 i386 机器上,%ebp 寄存器可以用来回溯整个堆栈。
例如,在函数 rmqueue() 上执行第一个指令后,堆栈看上去类似于下面这样:
[code:1:6ddc15f4ad][0]kdb> md %ebp
0xc74c9f38 c74c9f60 c0136c40 000001f0 00000000
0xc74c9f48 08053328 c0425238 c04253a8 00000000
0xc74c9f58 000001f0 00000246 c74c9f6c c0136a25
0xc74c9f68 c74c8000 c74c9f74 c0136d6d c74c9fbc
0xc74c9f78 c014fe45 c74c8000 00000000 08053328
[0]kdb> 0xc0136c40
0xc0136c40 = 0xc0136c40 (__alloc_pages +0x44)
[0]kdb> 0xc0136a25
0xc0136a25 = 0xc0136a25 (_alloc_pages +0x19)
[0]kdb> 0xc0136d6d
0xc0136d6d = 0xc0136d6d (__get_free_pages +0xd)[/code:1:6ddc15f4ad]
我们可以看到 rmqueue() 被 __alloc_pages 调用,后者接下来又被 _alloc_pages 调用,以此类推。
每一帧的第一个双字(double Word)指向下一帧,这后面紧跟着调用函数的地址。因此,跟踪堆栈就变成一件轻松的工作了。
[size=18:6ddc15f4ad]技巧 #3[/size:6ddc15f4ad]
go 命令可以有选择地以一个地址作为参数。如果您想在某个特定地址处继续执行,则可以提供该地址作为参数。另一个办法是使用 rm 命令修改指令指针寄存器,然后只要输入 go。如果您想跳过似乎会引起问题的某个特定指令或一组指令,这就会很有用。但是,请注意,该指令使用不慎会造成严重的问题,系统可能会严重崩溃。
[size=18:6ddc15f4ad]技巧 #4[/size:6ddc15f4ad]
您可以利用一个名为 defcmd 的有用命令来定义自己的命令集。例如,每当遇到断点时,您可能希望能同时检查某个特殊变量、检查某些寄存器的内容并转储堆栈。通常,您必须要输入一系列命令,以便能同时执行所有这些工作。defcmd 允许您定义自己的命令,该命令可以包含一个或多个预定义的 KDB 命令。然后只需要用一个命令就可以完成所有这三项工作。其语法如下:
[code:1:6ddc15f4ad][0]kdb> defcmd name "usage" "help"
[0]kdb> [defcmd] type the commands here
[0]kdb> [defcmd] endefcmd [/code:1:6ddc15f4ad]
例如,可以定义一个(简单的)新命令 hari,它显示从地址 0xc000000 开始的一行内存、显示寄存器的内容并转储堆栈:
[code:1:6ddc15f4ad][0]kdb> defcmd hari "" "no arguments needed"
[0]kdb> [defcmd] md 0xc000000 1
[0]kdb> [defcmd] rd
[0]kdb> [defcmd] md %ebp 1
[0]kdb> [defcmd] endefcmd [/code:1:6ddc15f4ad]
该命令的输出会是:
[code:1:6ddc15f4ad][0]kdb> hari
[hari]kdb> md 0xc000000 1
0xc000000 00000001 f000e816 f000e2c3 f000e816
[hari]kdb> rd
eax = 0x00000000 ebx = 0xc0105330 ecx = 0xc0466000 edx = 0xc0466000
....
...
[hari]kdb> md %ebp 1
0xc0467fbc c0467fd0 c01053d2 00000002 000a0200
[0]kdb> [/code:1:6ddc15f4ad]
[size=18:6ddc15f4ad]技巧 #5[/size:6ddc15f4ad]
可以使用 bph 和 bpha 命令(假如体系结构支持使用硬件寄存器)来应用读写断点。这意味着每当从某个特定地址读取数据或将数据写入该地址时,我们都可以对此进行控制。当调试数据/内存毁坏问题时这可能会极其方便,在这种情况中您可以用它来识别毁坏的代码/进程。
上一页 [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] 下一页
示例
[code:1:6ddc15f4ad]每当将四个字节写入地址 0xc0204060 时就进入内核调试器:
[0]kdb> bph 0xc0204060 dataw 4
在读取从 0xc000000 开始的至少两个字节的数据时进入内核调试器:
[0]kdb> bph 0xc000000 datar 2[/code:1:6ddc15f4ad]
[size=18:6ddc15f4ad]结束语[/size:6ddc15f4ad]
对于执行内核调试,KDB 是一个方便的且功能强大的工具。它提供了各种选项,并且使我们能够分析内存内容和数据结构。最妙的是,它不需要用另一台机器来执行调试。
[size=18:6ddc15f4ad]参考资料[/size:6ddc15f4ad]
?请在 Documentation/kdb 目录中查找 KDB 手册页。
?有关设置串行控制台的信息,请查找 Documentation 目录中的 serial-console.txt。
?请在 SGI 的内核调试器项目网站上下载 KDB。
?有关几个基于方案的 Linux 调试技术的概述,请阅读“掌握 Linux 调试技术”(developerWorks,2002 年 8 月)。
?教程“编译 Linux 内核”(developerWorks,2000 年 8 月)让您完整地了解配置、编译和安装内核的过程。
?IBM AIX 用户可以在 KDB Kernel Debugger and Command 页面上获取有关用于 AIX 的 KDB 的使用帮助。
?那些寻求有关调试 OS/2 信息的读者应该阅读 IBM 红皮书 The OS/2 Debugging Handbook(共四卷)的第 II 卷。
?在 developerWorks Linux 专区中查找更多针对 Linux 开发人员的参考资料。
【发表回复】【查看CU论坛原帖】【关闭】
zhchhui 回复于:2003-09-15 10:38:56
掌握 Linux 调试技术
内容:
常见调试方法
第 1 种情况:内存调试工具
MEMWATCH
YAMD
Electric Fence
第 2 种情况:使用 strace
第 3 种情况:使用 gdb 和 Oops
kgdb
Oops 分析
kdb
第 4 种情况:使用魔术键控顺序获取反跟踪
结束语
zhchhui 回复于:2003-09-15 10:42:18
在 Linux 上找出并解决程序错误的主要方法
Steve Best(
[email protected])
JFS 核心小组成员,IBM
2002 年 8 月
您可以用各种方法来监控运行着的用户空间程序:可以为其运行调试器并单步调试该程序,添加打印语句,或者添加工具来分析程序。本文描述了几种可以用来调试在 Linux 上运行的程序的方法。我们将回顾四种调试问题的情况,这些问题包括段错误,内存溢出和泄漏,还有挂起。
本文讨论了四种调试 Linux 程序的情况。在第 1 种情况中,我们使用了两个有内存分配问题的样本程序,使用 MEMWATCH 和 Yet Another Malloc Debugger(YAMD)工具来调试它们。在第 2 种情况中,我们使用了 Linux 中的 strace 实用程序,它能够跟踪系统调用和信号,从而找出程序发生错误的地方。在第 3 种情况中,我们使用 Linux 内核的 Oops 功能来解决程序的段错误,并向您展示如何设置内核源代码级调试器(kernel source level debugger,kgdb),以使用 GNU 调试器(GNU debugger,gdb)来解决相同的问题;kgdb 程序是使用串行连接的 Linux 内核远程 gdb。在第 4 种情况中,我们使用 Linux 上提供的魔术键控顺序(magic key sequence)来显示引发挂起问题的组件的信息。
[size=18:b0b26de3a8][b:b0b26de3a8]常见调试方法[/b:b0b26de3a8][/size:b0b26de3a8]
当您的程序中包含错误时,很可能在代码中某处有一个条件,您认为它为真(true),但实际上是假(false)。找出错误的过程也就是在找出错误后推翻以前一直确信为真的某个条件过程。
以下几个示例是您可能确信成立的条件的一些类型:
?在源代码中的某处,某变量有特定的值。
?在给定的地方,某个结构已被正确设置。
?对于给定的 if-then-else 语句,if 部分就是被执行的路径。
?当子例程被调用时,该例程正确地接收到了它的参数。
找出错误也就是要确定上述所有情况是否存在。如果您确信在子例程被调用时某变量应该有特定的值,那么就检查一下情况是否如此。如果您相信 if 结构会被执行,那么也检查一下情况是否如此。通常,您的假设都会是正确的,但最终您会找到与假设不符的情况。结果,您就会找出发生错误的地方。
调试是您无法逃避的任务。进行调试有很多种方法,比如将消息打印到屏幕上、使用调试器,或只是考虑程序执行的情况并仔细地揣摩问题所在。
在修正问题之前,您必须找出它的源头。举例来说,对于段错误,您需要了解段错误发生在代码的哪一行。一旦您发现了代码中出错的行,请确定该方法中变量的值、方法被调用的方式以及关于错误如何发生的详细情况。使用调试器将使找出所有这些信息变得很简单。如果没有调试器可用,您还可以使用其它的工具。(请注意,产品环境中可能并不提供调试器,而且 Linux 内核没有内建的调试器。)
[size=18:b0b26de3a8][b:b0b26de3a8]实用的内存和内核工具[/b:b0b26de3a8][/size:b0b26de3a8]
您可以使用 Linux 上的调试工具,通过各种方式跟踪用户空间和内核问题。请使用下面的工具和技术来构建和调试您的源代码:
[size=18:b0b26de3a8][b:b0b26de3a8]用户空间工具[/b:b0b26de3a8][/size:b0b26de3a8]:
?内存工具:MEMWATCH 和 YAMD
?strace
?GNU 调试器(gdb)
?魔术键控顺序
[size=18:b0b26de3a8][b:b0b26de3a8]内核工具[/b:b0b26de3a8][/size:b0b26de3a8]:
?内核源代码级调试器(kgdb)
?内建内核调试器(kdb)
?Oops
本文将讨论一类通过人工检查代码不容易找到的问题,而且此类问题只在很少见的情况下存在。内存错误通常在多种情况同时存在时出现,而且您有时只能在部署程序之后才能发现内存错误。
zhchhui 回复于:2003-09-15 10:46:42
上一页 [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] 下一页
[size=18:ff78191c7b][b] 第 1 种情况:内存调试工具[/b[/size:ff78191c7b]]
C 语言作为 Linux 系统上标准的编程语言给予了我们对动态内存分配很大的控制权。然而,这种自由可能会导致严重的内存管理问题,而这些问题可能导致程序崩溃或随时间的推移导致性能降级。
内存泄漏(即 malloc() 内存在对应的 free() 调用执行后永不被释放)和缓冲区溢出(例如对以前分配到某数组的内存进行写操作)是一些常见的问题,它们可能很难检测到。这一部分将讨论几个调试工具,它们极大地简化了检测和找出内存问题的过程。
[color=blue:ff78191c7b]MEMWATCH[/color:ff78191c7b]
MEMWATCH 由 Johan Lindh 编写,是一个开放源代码 C 语言内存错误检测工具,您可以自己下载它(请参阅本文后面部分的参考资料)。只要在代码中添加一个头文件并在 gcc 语句中定义了 MEMWATCH 之后,您就可以跟踪程序中的内存泄漏和错误了。MEMWATCH 支持 ANSI C,它提供结果日志纪录,能检测双重释放(double-free)、错误释放(erroneous free)、没有释放的内存(unfreed memory)、溢出和下溢等等。
清单 1. 内存样本(test1.c)
[code:1:ff78191c7b]#include <stdlib.h>
#include <stdio.h>
#include "memwatch.h"
int main(void)
{
char *ptr1;
char *ptr2;
ptr1 = malloc(512);
ptr2 = malloc(512);
ptr2 = ptr1;
free(ptr2);
free(ptr1);
}[/code:1:ff78191c7b]
清单 1 中的代码将分配两个 512 字节的内存块,然后指向第一个内存块的指针被设定为指向第二个内存块。结果,第二个内存块的地址丢失,从而产生了内存泄漏。
现在我们编译清单 1 的 memwatch.c。下面是一个 makefile 示例:
test1
[code:1:ff78191c7b]gcc -DMEMWATCH -DMW_STDIO test1.c memwatch c -o test1[/code:1:ff78191c7b]
当您运行 test1 程序后,它会生成一个关于泄漏的内存的报告。清单 2 展示了示例 memwatch.log 输出文件。
清单 2. test1 memwatch.log 文件
[code:1:ff78191c7b]MEMWATCH 2.67 Copyright (C) 1992-1999 Johan Lindh
...
double-free: <4> test1.c(15), 0x80517b4 was freed from test1.c(14)
...
unfreed: <2> test1.c(11), 512 bytes at 0x80519e4
{FE FE FE FE FE FE FE FE FE FE FE FE ..............}
Memory usage statistics (global):
N)umber of allocations made: 2
L)argest memory usage : 1024
T)otal of all alloc() calls: 1024
U)nfreed bytes totals : 512[/code:1:ff78191c7b]
MEMWATCH 为您显示真正导致问题的行。如果您释放一个已经释放过的指针,它会告诉您。对于没有释放的内存也一样。日志结尾部分显示统计信息,包括泄漏了多少内存,使用了多少内存,以及总共分配了多少内存。
[color=blue:ff78191c7b]YAMD[/color:ff78191c7b]
YAMD 软件包由 Nate Eldredge 编写,可以查找 C 和 C++ 中动态的、与内存分配有关的问题。在撰写本文时,YAMD 的最新版本为 0.32。请下载 yamd-0.32.tar.gz(请参阅参考资料)。执行 make 命令来构建程序;然后执行 make install 命令安装程序并设置工具。
一旦您下载了 YAMD 之后,请在 test1.c 上使用它。请删除 #include memwatch.h 并对 makefile 进行如下小小的修改:
使用 YAMD 的 test1
gcc -g test1.c -o test1
清单 3 展示了来自 test1 上的 YAMD 的输出。
清单 3. 使用 YAMD 的 test1 输出
[code:1:ff78191c7b]YAMD version 0.32
Executable: /usr/src/test/yamd-0.32/test1
...
INFO: Normal allocation of this block
Address 0x40025e00, size 512
...
INFO: Normal allocation of this block
Address 0x40028e00, size 512
...
INFO: Normal deallocation of this block
Address 0x40025e00, size 512
...
ERROR: Multiple freeing At
free of pointer already freed
Address 0x40025e00, size 512
...
WARNING: Memory leak
Address 0x40028e00, size 512
WARNING: Total memory leaks:
1 unfreed allocations totaling 512 bytes
*** Finished at Tue ... 10:07:15 2002
Allocated a grand total of 1024 bytes 2 allocations
Average of 512 bytes per allocation
上一页 [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] 下一页
Max bytes allocated at one time: 1024
24 K alloced internally / 12 K mapped now / 8 K max
Virtual program size is 1416 K
End.[/code:1:ff78191c7b]
YAMD 显示我们已经释放了内存,而且存在内存泄漏。让我们在清单 4 中另一个样本程序上试试 YAMD。
清单 4. 内存代码(test2.c)
[code:1:ff78191c7b]#include <stdlib.h>
#include <stdio.h>
int main(void)
{
char *ptr1;
char *ptr2;
char *chptr;
int i = 1;
ptr1 = malloc(512);
ptr2 = malloc(512);
chptr = (char *)malloc(512);
for (i; i <= 512; i++) {
chptr[i] = 'S';
}
ptr2 = ptr1;
free(ptr2);
free(ptr1);
free(chptr);
}[/code:1:ff78191c7b]
您可以使用下面的命令来启动 YAMD:
[code:1:ff78191c7b]./run-yamd /usr/src/test/test2/test2 [/code:1:ff78191c7b]
清单 5 显示了在样本程序 test2 上使用 YAMD 得到的输出。YAMD 告诉我们在 for 循环中有“越界(out-of-bounds)”的情况。
清单 5. 使用 YAMD 的 test2 输出
[code:1:ff78191c7b]Running /usr/src/test/test2/test2
Temp output to /tmp/yamd-out.1243
*********
./run-yamd: line 101: 1248 Segmentation fault (core dumped)
YAMD version 0.32
Starting run: /usr/src/test/test2/test2
Executable: /usr/src/test/test2/test2
Virtual program size is 1380 K
...
INFO: Normal allocation of this block
Address 0x40025e00, size 512
...
INFO: Normal allocation of this block
Address 0x40028e00, size 512
...
INFO: Normal allocation of this block
Address 0x4002be00, size 512
ERROR: Crash
...
Tried to write address 0x4002c000
Seems to be part of this block:
Address 0x4002be00, size 512
...
Address in question is at offset 512 (out of bounds)
Will dump core after checking heap.
Done.[/code:1:ff78191c7b]
MEMWATCH 和 YAMD 都是很有用的调试工具,它们的使用方法有所不同。对于 MEMWATCH,您需要添加包含文件 memwatch.h 并打开两个编译时间标记。对于链接(link)语句,YAMD 只需要 -g 选项。
[color=blue:ff78191c7b]Electric Fence[/color:ff78191c7b]
多数 Linux 分发版包含一个 Electric Fence 包,不过您也可以选择下载它。Electric Fence 是一个由 Bruce Perens 编写的 malloc() 调试库。它就在您分配内存后分配受保护的内存。如果存在 fencepost 错误(超过数组末尾运行),程序就会产生保护错误,并立即结束。通过结合 Electric Fence 和 gdb,您可以精确地跟踪到哪一行试图访问受保护内存。 Electric Fence 的另一个功能就是能够检测内存泄漏。
zhchhui 回复于:2003-09-15 10:49:18
[b:95b8e28830] [size=18:95b8e28830]第 2 种情况:使用 strace[/size:95b8e28830][/b:95b8e28830]
strace 命令是一种强大的工具,它能够显示所有由用户空间程序发出的系统调用。strace 显示这些调用的参数并返回符号形式的值。 strace 从内核接收信息,而且不需要以任何特殊的方式来构建内核。将跟踪信息发送到应用程序及内核开发者都很有用。在清单 6 中,分区的一种格式有错误,清单显示了 strace 的开头部分,内容是关于调出创建文件系统操作(mkfs)的。strace 确定哪个调用导致问题出现。
清单 6. mkfs 上 strace 的开头部分
[code:1:95b8e28830]execve("/sbin/mkfs.jfs", ["mkfs.jfs", "-f", "/dev/test1"], &
...
open("/dev/test1", O_RDWRO_LARGEFILE) = 4
stat64("/dev/test1", {st_mode=&, st_rdev=makedev(63, 255), ...}) = 0
ioctl(4, 0x40041271, 0xbfffe128) = -1 EINVAL (Invalid argument)
write(2, "mkfs.jfs: warning - cannot setb" ..., 98mkfs.jfs: warning -
cannot set blocksize on block device /dev/test1: Invalid argument )
= 98
stat64("/dev/test1", {st_mode=&, st_rdev=makedev(63, 255), ...}) = 0
open("/dev/test1", O_RDONLYO_LARGEFILE) = 5
ioctl(5, 0x80041272, 0xbfffe124) = -1 EINVAL (Invalid argument)
上一页 [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] 下一页
write(2, "mkfs.jfs: can't determine device"..., ..._exit(1)
= ?[/code:1:95b8e28830]
清单 6 显示 ioctl 调用导致用来格式化分区的 mkfs 程序失败。ioctl BLKGETSIZE64 失败。(BLKGET- SIZE64 在调用 ioctl 的源代码中定义。) BLKGETSIZE64 ioctl 将被添加到 Linux 中所有的设备,而在这里,逻辑卷管理器还不支持它。因此,如果 BLKGETSIZE64 ioctl 调用失败,mkfs 代码将改为调用较早的 ioctl 调用;这使得 mkfs 适用于逻辑卷管理器。
zhchhui 回复于:2003-09-15 10:57:11
[b:627becdd94][size=18:627becdd94] 第 3 种情况:使用 gdb 和 Oops[/size:627becdd94][/b:627becdd94]
您可以从命令行使用 gdb 程序(Free Software Foundation 的调试器)来找出错误,也可以从诸如 Data Display Debugger(DDD)这样的几个图形工具之一使用 gdb 程序来找出错误。您可以使用 gdb 来调试用户空间程序或 Linux 内核。这一部分只讨论从命令行运行 gdb 的情况。
使用 gdb program name 命令启动 gdb。gdb 将载入可执行程序符号并显示输入提示符,让您可以开始使用调试器。您可以通过三种方式用 gdb 查看进程:
?使用 attach 命令开始查看一个已经运行的进程;attach 将停止进程。
?使用 run 命令执行程序并从头开始调试程序。
?查看已有的核心文件来确定进程终止时的状态。要查看核心文件,请用下面的命令启动 gdb。
gdb programname corefilename
要用核心文件进行调试,您不仅需要程序的可执行文件和源文件,还需要核心文件本身。要用核心文件启动 gdb,请使用 -c 选项:
gdb -c core programname
gdb 显示哪行代码导致程序发生核心转储。
在运行程序或连接到已经运行的程序之前,请列出您觉得有错误的源代码,设置断点,然后开始调试程序。您可以使用 help 命令查看全面的 gdb 在线帮助和详细的教程。
[color=blue:627becdd94]kgdb[/color:627becdd94]
kgdb 程序(使用 gdb 的远程主机 Linux 内核调试器)提供了一种使用 gdb 调试 Linux 内核的机制。kgdb 程序是内核的扩展,它让您能够在远程主机上运行 gdb 时连接到运行用 kgdb 扩展的内核机器。您可以接着深入到内核中、设置断点、检查数据并进行其它操作(类似于您在应用程序上使用 gdb 的方式)。这个补丁的主要特点之一就是运行 gdb 的主机在引导过程中连接到目标机器(运行要被调试的内核)。这让您能够尽早开始调试。请注意,补丁为 Linux 内核添加了功能,所以 gdb 可以用来调试 Linux 内核。
使用 kgdb 需要两台机器:一台是开发机器,另一台是测试机器。一条串行线(空调制解调器电缆)将通过机器的串口连接它们。您希望调试的内核在测试机器上运行;gdb 在开发机器上运行。gdb 使用串行线与您要调试的内核通信。
请遵循下面的步骤来设置 kgdb 调试环境:
1.下载您的 Linux 内核版本适用的补丁。
2.将组件构建到内核,因为这是使用 kgdb 最简单的方法。(请注意,有两种方法可以构建多数内核组件,比如作为模块或直接构建到内核中。举例来说,日志纪录文件系统(Journaled File System,JFS)可以作为模块构建,或直接构建到内核中。通过使用 gdb 补丁,我们就可以将 JFS 直接构建到内核中。)
3.应用内核补丁并重新构建内核。
4.创建一个名为 .gdbinit 的文件,并将其保存在内核源文件子目录中(换句话说就是 /usr/src/linux)。文件 .gdbinit 中有下面四行代码:
[code:1:627becdd94]oset remotebaud 115200
osymbol-file vmlinux
otarget remote /dev/ttyS0
oset output-radix 16 [/code:1:627becdd94]
5.将 append=gdb 这一行添加到 lilo,lilo 是用来在引导内核时选择使用哪个内核的引导载入程序。
[code:1:627becdd94]oimage=/boot/bzImage-2.4.17
olabel=gdb2417
oread-only
oroot=/dev/sda8
oappend="gdb gdbttyS=1 gdb-baud=115200 nmi_watchdog=0" [/code:1:627becdd94]
清单 7 是一个脚本示例,它将您在开发机器上构建的内核和模块引入测试机器。您需要修改下面几项:
?best@sfb:用户标识和机器名。
?/usr/src/linux-2.4.17:内核源代码树的目录。
?bzImage-2.4.17:测试机器上将引导的内核名。
?rcp 和 rsync:必须允许它在构建内核的机器上运行。
清单 7. 引入测试机器的内核和模块的脚本
[code:1:627becdd94]set -x
rcp best@sfb: /usr/src/linux-2.4.17/arch/i386/boot/bzImage /boot/bzImage-2.4.17
rcp best@sfb:/usr/src/linux-2.4.17/System.map /boot/System.map-2.4.17
rm -rf /lib/modules/2.4.17
rsync -a best@sfb:/lib/modules/2.4.17 /lib/modules
chown -R root /lib/modules/2.4.17
lilo[/code:1:627becdd94]
现在我们可以通过改为使用内核源代码树开始的目录来启动开发机器上的 gdb 程序了。在本示例中,内核源代码树位于 /usr/src/linux-2.4.17。输入 gdb 启动程序。
如果一切正常,测试机器将在启动过程中停止。输入 gdb 命令 cont 以继续启动过程。一个常见的问题是,空调制解调器电缆可能会被连接到错误的串口。如果 gdb 不启动,将端口改为第二个串口,这会使 gdb 启动。
[color=darkblue:627becdd94]使用 kgdb 调试内核问题[/color:627becdd94]
上一页 [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] 下一页
清单 8 列出了 jfs_mount.c 文件的源代码中被修改过的代码,我们在代码中创建了一个空指针异常,从而使代码在第 109 行产生段错误。
清单 8. 修改过后的 jfs_mount.c 代码
[code:1:627becdd94]int jfs_mount(struct super_block *sb)
{
...
int ptr; /* line 1 added */
jFYI(1, ("
Mount JFS
"));
/ *
* read/validate superblock
* (initialize mount inode from the superblock)
* /
if ((rc = chkSuper(sb))) {
goto errout20;
}
108 ptr=0; /* line 2 added */
109 printk("%d
",*ptr); /* line 3 added */[/code:1:627becdd94]
清单 9 在向文件系统发出 mount 命令之后显示一个 gdb 异常。kgdb 提供了几条命令,如显示数据结构和变量值以及显示系统中的所有任务处于什么状态、它们驻留在何处、它们在哪些地方使用了 CPU 等等。清单 9 将显示回溯跟踪为该问题提供的信息;where 命令用来执行反跟踪,它将告诉被执行的调用在代码中的什么地方停止。
清单 9. gdb 异常和反跟踪
[code:1:627becdd94]mount -t jfs /dev/sdb /jfs
Program received signal SIGSEGV, Segmentation fault.
jfs_mount (sb=0xf78a3800) at jfs_mount.c:109
109 printk("%d
",*ptr);
(gdb)where
#0 jfs_mount (sb=0xf78a3800) at jfs_mount.c:109
#1 0xc01a0dbb in jfs_read_super ... at super.c:280
#2 0xc0149ff5 in get_sb_bdev ... at super.c:620
#3 0xc014a89f in do_kern_mount ... at super.c:849
#4 0xc0160e66 in do_add_mount ... at namespace.c:569
#5 0xc01610f4 in do_mount ... at namespace.c:683
#6 0xc01611ea in sys_mount ... at namespace.c:716
#7 0xc01074a7 in system_call () at af_packet.c:1891
#8 0x0 in ?? ()
(gdb)[/code:1:627becdd94]
下一部分还将讨论这个相同的 JFS 段错误问题,但不设置调试器,如果您在非 kgdb 内核环境中执行清单 8 中的代码,那么它使用内核可能生成的 Oops 消息。
[color=darkblue:627becdd94]Oops 分析[/color:627becdd94]
Oops(也称 panic,慌张)消息包含系统错误的细节,如 CPU 寄存器的内容。在 Linux 中,调试系统崩溃的传统方法是分析在发生崩溃时发送到系统控制台的 Oops 消息。一旦您掌握了细节,就可以将消息发送到 ksymoops 实用程序,它将试图将代码转换为指令并将堆栈值映射到内核符号。在很多情况下,这些信息就足够您确定错误的可能原因是什么了。请注意,Oops 消息并不包括核心文件。
让我们假设系统刚刚创建了一条 Oops 消息。作为编写代码的人,您希望解决问题并确定什么导致了 Oops 消息的产生,或者您希望向显示了 Oops 消息的代码的开发者提供有关您的问题的大部分信息,从而及时地解决问题。Oops 消息是等式的一部分,但如果不通过 ksymoops 程序运行它也于事无补。下面的图显示了格式化 Oops 消息的过程。
[color=darkblue:627becdd94]格式化 Oops 消息[/color:627becdd94]见附图
ksymoops 需要几项内容:Oops 消息输出、来自正在运行的内核的 System.map 文件,还有 /proc/ksyms、 vmlinux 和 /proc/modules。关于如何使用 ksymoops,内核源代码 /usr/src/linux/Documentation/oops-tracing.txt 中或 ksymoops 手册页上有完整的说明可以参考。Ksymoops 反汇编代码部分,指出发生错误的指令,并显示一个跟踪部分表明代码如何被调用。
首先,将 Oops 消息保存在一个文件中以便通过 ksymoops 实用程序运行它。清单 10 显示了由安装 JFS 文件系统的 mount 命令创建的 Oops 消息,问题是由清单 8 中添加到 JFS 安装代码的那三行代码产生的。
清单 10. ksymoops 处理后的 Oops 消息
[code:1:627becdd94]ksymoops 2.4.0 on i686 2.4.17. Options used
... 15:59:37 sfb1 kernel: Unable to handle kernel NULL pointer dereference at
virtual address 0000000
... 15:59:37 sfb1 kernel: c01588fc
... 15:59:37 sfb1 kernel: *pde = 0000000
... 15:59:37 sfb1 kernel: Oops: 0000
... 15:59:37 sfb1 kernel: CPU: 0
... 15:59:37 sfb1 kernel: EIP: 0010:[jfs_mount+60/704]
... 15:59:37 sfb1 kernel: Call Trace: [jfs_read_super+287/688]
[get_sb_bdev+563/736] [do_kern_mount+189/336] [do_add_mount+35/208]
[do_page_fault+0/1264]
... 15:59:37 sfb1 kernel: Call Trace: [<c0155d4f>]...
... 15:59:37 sfb1 kernel: [<c0106e04 ...
上一页 [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] 下一页