Linux 内核调试器(KDB)

简介: Linux 内核调试器(KDB)允许您调试 Linux 内核。这个恰如其名的工具实质上是内核代码的补丁,它允许高手访问内核内存和数据结构。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 pointers”选项(如果有的话)则设置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 命令带有地址/符号以及字节计数,显示从指定的地址开始的 byte-count 字节数的初始内存内容。它本质上和 md 一样,但是它不显示起始地址并且不在结尾显示字符转换。 mdr 命令较少使用。

mm 命令修改内存内容。它以地址/符号和新内容作为参数,用 new-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 命令修改寄存器的内容。它以寄存器名称和 new-contents 作为参数,用 new-contents 修改寄存器。寄存器名称与特定的体系结构有关。目前,不能修改控制寄存器。

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

示例


显示通用寄存器组:
[0]kdb> rd



[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 、 return 和 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 pointers”选项。这确保了帧指针寄存器将被用作帧指针,从而产生正确的回溯。实际上,您可以手工转储帧指针寄存器的内容并跟踪整个堆栈。例如,在 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 是一个方便的且功能强大的工具。它提供了各种选项,并且使我们能够分析内存内容和数据结构。最妙的是,它不需要用另一台机器来执行调试。


参考资料

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文

  • 请在 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 卷。 



  • 在 developerWorksLinux 专区中查找更多 针对 Linux 开发人员的参考资料。 



关于作者

Hariprasad Nellitheertha 在印度班加罗尔(Bangalore)的 IBM Linux 技术中心工作。他目前正在 Linux Change Team 从事修正内核和其它 Linux 错误的工作。Hari 研究过 OS/2 内核和文件系统。他的兴趣包括 Linux 内核内部机理、文件系统和自主计算。可以通过 nharipra@in.ibm.com与 Hari 联系。

相关文章
|
23天前
|
算法 Linux 调度
深入理解Linux内核调度器:从基础到优化####
本文旨在通过剖析Linux操作系统的心脏——内核调度器,为读者揭开其高效管理CPU资源的神秘面纱。不同于传统的摘要概述,本文将直接以一段精简代码片段作为引子,展示一个简化版的任务调度逻辑,随后逐步深入,详细探讨Linux内核调度器的工作原理、关键数据结构、调度算法演变以及性能调优策略,旨在为开发者与系统管理员提供一份实用的技术指南。 ####
61 4
|
12天前
|
缓存 网络协议 Linux
深入探索Linux操作系统的内核优化策略####
本文旨在探讨Linux操作系统内核的优化方法,通过分析当前主流的几种内核优化技术,结合具体案例,阐述如何有效提升系统性能与稳定性。文章首先概述了Linux内核的基本结构,随后详细解析了内核优化的必要性及常用手段,包括编译优化、内核参数调整、内存管理优化等,最后通过实例展示了这些优化技巧在实际场景中的应用效果,为读者提供了一套实用的Linux内核优化指南。 ####
38 1
|
17天前
|
算法 Linux 开发者
Linux内核中的锁机制:保障并发控制的艺术####
本文深入探讨了Linux操作系统内核中实现的多种锁机制,包括自旋锁、互斥锁、读写锁等,旨在揭示这些同步原语如何高效地解决资源竞争问题,保证系统的稳定性和性能。通过分析不同锁机制的工作原理及应用场景,本文为开发者提供了在高并发环境下进行有效并发控制的实用指南。 ####
|
25天前
|
缓存 资源调度 安全
深入探索Linux操作系统的心脏——内核配置与优化####
本文作为一篇技术性深度解析文章,旨在引领读者踏上一场揭秘Linux内核配置与优化的奇妙之旅。不同于传统的摘要概述,本文将以实战为导向,直接跳入核心内容,探讨如何通过精细调整内核参数来提升系统性能、增强安全性及实现资源高效利用。从基础概念到高级技巧,逐步揭示那些隐藏在命令行背后的强大功能,为系统管理员和高级用户打开一扇通往极致性能与定制化体验的大门。 --- ###
58 9
|
24天前
|
缓存 负载均衡 Linux
深入理解Linux内核调度器
本文探讨了Linux操作系统核心组件之一——内核调度器的工作原理和设计哲学。不同于常规的技术文章,本摘要旨在提供一种全新的视角来审视Linux内核的调度机制,通过分析其对系统性能的影响以及在多核处理器环境下的表现,揭示调度器如何平衡公平性和效率。文章进一步讨论了完全公平调度器(CFS)的设计细节,包括它如何处理不同优先级的任务、如何进行负载均衡以及它是如何适应现代多核架构的挑战。此外,本文还简要概述了Linux调度器的未来发展方向,包括对实时任务支持的改进和对异构计算环境的适应性。
39 6
|
25天前
|
缓存 Linux 开发者
Linux内核中的并发控制机制:深入理解与应用####
【10月更文挑战第21天】 本文旨在为读者提供一个全面的指南,探讨Linux操作系统中用于实现多线程和进程间同步的关键技术——并发控制机制。通过剖析互斥锁、自旋锁、读写锁等核心概念及其在实际场景中的应用,本文将帮助开发者更好地理解和运用这些工具来构建高效且稳定的应用程序。 ####
39 5
|
25天前
|
算法 Unix Linux
深入理解Linux内核调度器:原理与优化
本文探讨了Linux操作系统的心脏——内核调度器(Scheduler)的工作原理,以及如何通过参数调整和代码优化来提高系统性能。不同于常规摘要仅概述内容,本摘要旨在激发读者对Linux内核调度机制深层次运作的兴趣,并简要介绍文章将覆盖的关键话题,如调度算法、实时性增强及节能策略等。
|
26天前
|
存储 监控 安全
Linux内核调优的艺术:从基础到高级###
本文深入探讨了Linux操作系统的心脏——内核的调优方法。文章首先概述了Linux内核的基本结构与工作原理,随后详细阐述了内核调优的重要性及基本原则。通过具体的参数调整示例(如sysctl、/proc/sys目录中的设置),文章展示了如何根据实际应用场景优化系统性能,包括提升CPU利用率、内存管理效率以及I/O性能等关键方面。最后,介绍了一些高级工具和技术,如perf、eBPF和SystemTap,用于更深层次的性能分析和问题定位。本文旨在为系统管理员和高级用户提供实用的内核调优策略,以最大化Linux系统的效率和稳定性。 ###
|
25天前
|
Java Linux Android开发
深入探索Android系统架构:从Linux内核到应用层
本文将带领读者深入了解Android操作系统的复杂架构,从其基于Linux的内核到丰富多彩的应用层。我们将探讨Android的各个关键组件,包括硬件抽象层(HAL)、运行时环境、以及核心库等,揭示它们如何协同工作以支持广泛的设备和应用。通过本文,您将对Android系统的工作原理有一个全面的认识,理解其如何平衡开放性与安全性,以及如何在多样化的设备上提供一致的用户体验。
|
25天前
|
缓存 运维 网络协议
深入Linux内核架构:操作系统的核心奥秘
深入Linux内核架构:操作系统的核心奥秘
44 2