内核调试器:Linux 内核调试器内幕


调试内核问题时能够跟踪内核执行情况并查看其内存和数据结构是非常有用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 压缩文件解压缩补丁文件:

#bzip2 -d kdb-v4.2-2.4.20-common-1.bz2

#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 文件

现在应用这些补丁:

#patch -p1
#patch -p1 这些补丁应该干净利落地加以应用查找任何以 .rej 结尾文件这个扩展名表明这些是失败补丁如果内核树没问题那么补丁应用就不会有任何问题

接下来需要构建内核以支持 KDB步是设置 CONFIG_KDB 选项使用您喜欢配置机制(xconfig 和 menuconfig 等)来完成这转到结尾处“Kernel hacking”部分并选择“Built-in Kernel Debugger support”选项

您还可以根据自己偏好选择其它两个选项选择“Compile the kernel with frame poers”选项(如果有话)则设置 CONFIG_FRAME_POINTER 标志这将产生更好堆栈回溯帧指针寄存器被用作帧指针而不是通用寄存器您还可以选择“KDB off by default”选项这将设置 CONFIG_KDB_OFF 标志并且在缺省情况下将关闭 KDB我们将在后面节中对此进行详细介绍

保存配置然后退出重新编译内核建议在构建内核之前执行“make clean”用常用方式安装内核并引导它

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

激活 KDB
如果编译期间没有选中 CONFIG_KDB_OFF那么在缺省情况下 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 处于打开状态那么只要内核中有紧急情况就自动按下键盘上 PAUSE 键将手工 KDB KDB 种方式是通过串行控制台当然要做到这需要设置串行控制台(请参阅参考资料以获取这方面帮助)并且需要个从串行控制台进行读取按键序列 Ctrl-A 将从串行控制台 KDB

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

内存显示和修改
类别中最常用命令是 md、mdr、mm 和 mmW

md 命令以个地址/符号和行计数为参数显示从该地址开始 line-count 行内存如果没有指定 line-count那么就使用环境变量所指定缺省值如果没有指定地址那么 md 就从上次打印地址继续地址打印在开头转换打印在结尾

mdr 命令带有地址/符号以及字节计数显示从指定地址开始 -count 字节数内存内容它本质上和 md 但是它不显示起始地址并且不在结尾显示转换mdr 命令较少使用

mm 命令修改内存内容它以地址/符号和新内容作为参数-contents 替换地址处内容

mmW 命令更改从地址开始 W 个字节请注意mm 更改个机器字

示例

显示从 0xc000000 开始 15 行内存:
[0]kdb> md 0xc000000 15
将内存位置为 0xc000000 上内容更改为 0x10:
[0]kdb> mm 0xc000000 0x10
寄存器显示和修改
类别中命令有 rd、rm 和 ef

rd 命令(不带任何参数)显示处理器寄存器内容它可以有选择地带三个参数如果传递了 c 参数则 rd 显示处理器控制寄存器;如果带有 d 参数那么它就显示调试寄存器;如果带有 u 参数则显示上次进入内核当前任务寄存器组

rm 命令修改寄存器内容它以寄存器名称和 -contents 作为参数-contents 修改寄存器寄存器名称与特定体系结构有关目前不能修改控制寄存器

ef 命令以个地址作为参数它显示指定地址处异常帧

示例

显示通用寄存器组:
[0]kdb> rd
将寄存器 ebx 内容设置成 0x25:
[0]kdb> rm %ebx 0x25
断点
常用断点命令有 bp、bc、bd、be 和 bl

bp 命令以个地址/符号作为参数它在地址处应用断点当遇到该断点时则停止执行并将控制权交予 KDB该命令有几个有用变体bpa 命令对 SMP 系统中所有处理器应用断点bph 命令强制在支持硬件寄存器系统上使用它bpha 命令类似于 bpa 命令差别在于它强制使用硬件寄存器

bd 命令禁用特殊断点它接收断点号作为参数该命令不是从断点表中除去断点而只是禁用它断点号从 0 开始根据可用性顺序分配给断点

be 命令启用断点该命令参数也是断点号

bl 命令列出当前断点集它包含了启用和禁用断点

bc 命令从断点表中除去断点它以具体断点号或 * 作为参数在后种情况下它将除去所有断点

示例

sys_write 设置断点:
[0]kdb> bp sys_write
列出断点表中所有断点:
[0]kdb> bl
清除断点号 1:
[0]kdb> bc 1
堆栈跟踪
主要堆栈跟踪命令有 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> btp 575
其它命令
下面是在内核调试过程中非常有用其它几个 KDB 命令

id 命令以个地址/符号作为参数它对从该地址开始指令进行反汇编环境变量 IDCOUNT 确定要显示多少行输出

ss 命令单步执行指令然后将控制返回给 KDB该指令个变体是 ssb它执行从当前指令指针地址开始指令(在屏幕上打印指令)直到它遇到将引起分支转移指令为止分支转移指令典型示例有 call、 和 jump

go 命令让系统继续正常执行直执行到遇到断点为止(如果已应用了个断点话)

reboot 命令立刻重新引导系统它并没有彻底关闭系统因此结果是不可预测

ll 命令以地址、偏移量和另个 KDB 命令作为参数它对链表中每个元素反复执行作为参数这个命令所执行命令以列表中当前元素地址作为参数

示例

反汇编从例程 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
技巧和诀窍
调试个问题涉及到:使用调试器(或任何其它工具)找到问题根源以及使用源代码来跟踪导致问题根源单单使用源代码来确定问题是极其困难只有老练内核黑客才有可能做得到相反大多数新手往往要过多地依靠调试器来修正这种方法可能会产生不正确问题解决方案我们担心是这种方法只会修正表面症状而不能解决真正问题此类典型示例是添加处理代码以处理 NULL 指针或引用却没有查出无效引用真正原因

结合研究代码和使用调试工具这两种方法是识别和修正问题最佳方案

调试器主要用途是找到位置、确认症状(在某些情况下还有起因)、确定变量以及确定是如何出现这种情况(即建立堆栈)有经验黑客会知道对于某种特定问题应使用哪个调试器并且能迅速地根据调试获取必要信息然后继续分析代码以识别起因

因此这里为您介绍了些技巧以便您能使用 KDB 快速地取得上述结果当然要记住调试速度和精确度来自经验、实践和良好系统知识(硬件和内核内部机理等)

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

示例

表明 sys_read 从地址 0xc013db4c 开始:
[0]kdb> 0xc013db4c

0xc013db4c = 0xc013db4c (sys_read)
同样

同样表明 sys_write 位于地址 0xc013dcc8:
[0]kdb> sys_write

sys_write = 0xc013dcc8 (sys_write)
这些有助于在分析堆栈时找到全局数据和地址

技巧 #2
在编译带 KDB 内核时只要 CONFIG_FRAME_POINTER 选项出现就使用该选项为此需要在配置内核时选择“Kernel hacking”部分下面“Compile the kernel with frame poers”选项这确保了帧指针寄存器将被用作帧指针从而产生正确回溯实际上您可以手工转储帧指针寄存器内容并跟踪整个堆栈例如在 i386 机器上%ebp 寄存器可以用来回溯整个堆栈

例如 rmqueue 上执行第个指令后堆栈看上去类似于下面这样:

[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)
我们可以看到 rmqueue 被 __alloc_pages 后者接下来又被 _alloc_pages 以此类推

个双字(double word)指向下这后面紧跟着地址因此跟踪堆栈就变成件轻松工作了

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

技巧 #4
您可以利用个名为 defcmd 有用命令来定义自己命令集例如每当遇到断点时您可能希望能同时检查某个特殊变量、检查某些寄存器内容并转储堆栈通常您必须要输入系列命令以便能同时执行所有这些工作defcmd 允许您定义自己命令该命令可以包含个或多个预定义 KDB 命令然后只需要用个命令就可以完成所有这三项工作其语法如下:

[0]kdb> defcmd name "usage" "help"

[0]kdb> [defcmd] type the commands here

[0]kdb> [defcmd] endefcmd
例如可以定义个(简单)新命令 hari它显示从地址 0xc000000 开始行内存、显示寄存器内容并转储堆栈:

[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
该命令输出会是:

[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>
技巧 #5
可以使用 bph 和 bpha 命令(假如体系结构支持使用硬件寄存器)来应用读写断点这意味着每当从某个特定地址读取数据或将数据写入该地址时我们都可以对此进行控制当调试数据/内存毁坏问题时这可能会极其方便在这种情况中您可以用它来识别毁坏代码/进程

示例

每当将四个字节写入地址 0xc0204060 时就进入内核调试器:
[0]kdb> bph 0xc0204060 dataw 4
在读取从 0xc000000 开始至少两个字节数据时进入内核调试器:
[0]kdb> bph 0xc000000 datar 2
结束语
对于执行内核调试KDB 是个方便且功能强大工具它提供了各种选项并且使我们能够分析内存内容和数据结构最妙它不需要用另台机器来执行调试

参考资料

请在 document.tion/kdb 目录中查找 KDB 手册页


有关设置串行控制台信息请查找 document.tion 目录中 serial-console.txt


请在 SGI 内核调试器项目网站上下载 KDB


有关几个基于方案 Linux 调试技术概述请阅读“掌握 Linux 调试技术”(developerWorks2002 年 8 月)


教程“编译 Linux 内核”(developerWorks2000 年 8 月)让您完整地了解配置、编译和安装内核过程


IBM AIX 用户可以在 KDB Kernel Debugger and Command 页面上获取有关用于 AIX KDB 使用帮助


那些寻求有关调试 OS/2 信息读者应该阅读 IBM 红皮书 The OS/2 Debugging Handbook(共四卷)第 II 卷


在 developerWorks Linux 专区中查找更多针对 Linux 开发人员参考资料

Tags:  linux内核编译 深入理解linux内核 linux内核 内核调试器

延伸阅读

最新评论

发表评论