首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

Linux 内核调试器内幕(2)

Linux 内核调试器内幕(2)

示例
对函数 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 是一个方便的且功能强大的工具。它提供了各种选项,并且使我们能够分析内存内容和数据结构。最妙的是,它不需要用另一台机器来执行调试。
继承事业,薪火相传
返回列表