Linux 内核调试器内幕 KDB 入门指南--技巧和诀窍
- UID
- 1066743
|
Linux 内核调试器内幕 KDB 入门指南--技巧和诀窍
调试一个问题涉及到:使用调试器(或任何其它工具)找到问题的根源以及使用源代码来跟踪导致问题的根源。单单使用源代码来确定问题是极其困难的,只有老练的内核黑客才有可能做得到。相反,大多数的新手往往要过多地依靠调试器来修正错误。这种方法可能会产生不正确的问题解决方案。我们担心的是这种方法只会修正表面症状而不能解决真正的问题。此类错误的典型示例是添加错误处理代码以处理 NULL 指针或错误的引用,却没有查出无效引用的真正原因。
结合研究代码和使用调试工具这两种方法是识别和修正问题的最佳方案。
调试器的主要用途是找到错误的位置、确认症状(在某些情况下还有起因)、确定变量的值,以及确定程序是如何出现这种情况的(即,建立调用堆栈)。有经验的黑客会知道对于某种特定的问题应使用哪一个调试器,并且能迅速地根据调试获取必要的信息,然后继续分析代码以识别起因。
因此,这里为您介绍了一些技巧,以便您能使用 KDB 快速地取得上述结果。当然,要记住,调试的速度和精确度来自经验、实践和良好的系统知识(硬件和内核内部机理等)。
技巧 #1在 KDB 中,在提示处输入地址将返回与之最为匹配的符号。这在堆栈分析以及确定全局数据的地址/值和函数地址方面极其有用。同样,输入符号名则返回其虚拟地址。
示例
表明函数 sys_read 从地址 0xc013db4c 开始: 1
2
3
| [0]kdb> 0xc013db4c
<br><br>
0xc013db4c = 0xc013db4c (sys_read)
|
同样,
同样,表明 sys_write 位于地址 0xc013dcc8: 1
2
3
| [0]kdb> sys_write
<br><br>
sys_write = 0xc013dcc8 (sys_write)
|
这些有助于在分析堆栈时找到全局数据和函数地址。
技巧 #2在编译带 KDB 的内核时,只要 CONFIG_FRAME_POINTER 选项出现就使用该选项。为此,需要在配置内核时选择“Kernel hacking”部分下面的“Compile the kernel with frame pointers”选项。这确保了帧指针寄存器将被用作帧指针,从而产生正确的回溯。实际上,您可以手工转储帧指针寄存器的内容并跟踪整个堆栈。例如,在 i386 机器上,%ebp 寄存器可以用来回溯整个堆栈。
例如,在函数 rmqueue() 上执行第一个指令后,堆栈看上去类似于下面这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| [0]kdb> md %ebp
<br><br>
0xc74c9f38 c74c9f60 c0136c40 000001f0 00000000
<br>
0xc74c9f48 08053328 c0425238 c04253a8 00000000
<br>
0xc74c9f58 000001f0 00000246 c74c9f6c c0136a25
<br>
0xc74c9f68 c74c8000 c74c9f74 c0136d6d c74c9fbc
<br>
0xc74c9f78 c014fe45 c74c8000 00000000 08053328
<br><br>
[0]kdb> 0xc0136c40
<br><br>
0xc0136c40 = 0xc0136c40 (__alloc_pages +0x44)
<br><br>
[0]kdb> 0xc0136a25
<br><br>
0xc0136a25 = 0xc0136a25 (_alloc_pages +0x19)
<br><br>
[0]kdb> 0xc0136d6d
<br><br>
0xc0136d6d = 0xc0136d6d (__get_free_pages +0xd)
|
我们可以看到 rmqueue() 被 __alloc_pages 调用,后者接下来又被 _alloc_pages 调用,以此类推。
每一帧的第一个双字(double word)指向下一帧,这后面紧跟着调用函数的地址。因此,跟踪堆栈就变成一件轻松的工作了。
技巧 #3go 命令可以有选择地以一个地址作为参数。如果您想在某个特定地址处继续执行,则可以提供该地址作为参数。另一个办法是使用 rm 命令修改指令指针寄存器,然后只要输入 go 。如果您想跳过似乎会引起问题的某个特定指令或一组指令,这就会很有用。但是,请注意,该指令使用不慎会造成严重的问题,系统可能会严重崩溃。
技巧 #4您可以利用一个名为 defcmd 的有用命令来定义自己的命令集。例如,每当遇到断点时,您可能希望能同时检查某个特殊变量、检查某些寄存器的内容并转储堆栈。通常,您必须要输入一系列命令,以便能同时执行所有这些工作。 defcmd 允许您定义自己的命令,该命令可以包含一个或多个预定义的 KDB 命令。然后只需要用一个命令就可以完成所有这三项工作。其语法如下:
1
2
3
4
5
| [0]kdb> defcmd name "usage" "help"
<br><br>
[0]kdb> [defcmd] type the commands here
<br><br>
[0]kdb> [defcmd] endefcmd
|
例如,可以定义一个(简单的)新命令 hari ,它显示从地址 0xc000000 开始的一行内存、显示寄存器的内容并转储堆栈:
1
2
3
4
5
6
7
8
9
| [0]kdb> defcmd hari "" "no arguments needed"
<br><br>
[0]kdb> [defcmd] md 0xc000000 1
<br><br>
[0]kdb> [defcmd] rd
<br><br>
[0]kdb> [defcmd] md %ebp 1
<br><br>
[0]kdb> [defcmd] endefcmd
|
该命令的输出会是:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| [0]kdb> hari
<br><br>
[hari]kdb> md 0xc000000 1
<br><br>
0xc000000 00000001 f000e816 f000e2c3 f000e816
<br><br>
[hari]kdb> rd
<br><br>
eax = 0x00000000 ebx = 0xc0105330 ecx = 0xc0466000 edx = 0xc0466000
<br>
....
<br>
...
<br><br>
[hari]kdb> md %ebp 1
<br><br>
0xc0467fbc c0467fd0 c01053d2 00000002 000a0200
<br><br>
[0]kdb>
|
技巧 #5可以使用 bph 和 bpha 命令(假如体系结构支持使用硬件寄存器)来应用读写断点。这意味着每当从某个特定地址读取数据或将数据写入该地址时,我们都可以对此进行控制。当调试数据/内存毁坏问题时这可能会极其方便,在这种情况中您可以用它来识别毁坏的代码/进程。
示例
每当将四个字节写入地址 0xc0204060 时就进入内核调试器:1
| [0]kdb> bph 0xc0204060 dataw 4
|
在读取从 0xc000000 开始的至少两个字节的数据时进入内核调试器:1
| [0]kdb> bph 0xc000000 datar 2
|
|
|
|
|
|
|