发新话题
打印

内核调试(四)

内核调试(四)

内核调试器的传奇

很多内核开发者一直以来都希望能拥有一个用于内核的调试器。不幸的是,Linus不愿意在他的内核源代码树中加入一个调试器。他认为调试器会误导开发者,从而导致引入不良的修正。没有人能对他的逻辑提出异议——从真正理解代码出发,确实更能保证修正的正确性。然而,许多内核开发者们还是希望有一个官方发布的,用于内核的调试器。因为这个要求看起来不会马上被满足,所以许多补丁应运而生了,它们为标准内核附加上了内核调试的支持。虽然这都是一些不被官方认可的附加布丁,但它们确实功能完善,十分强大。在我们深入这些解决方案之前,让我们先看看标准的Linux调试器gdb能够给我们一些什么帮助。

gdb
你可以使用标准的GNU调试器对正在运行的内核进行察看。针对内核启动调试器的方法与针对进程的方法大致相同:

gdb vmlinux /proc/kcore

其中vmlinux文件是未经压缩的内核映像,不是压缩过的zImagebzImage,它存放在源代码树的根目录上。
       /proc/kcore作为一个参数选项,是作为core文件来用的,通过它你能够访问到内核驻留的高端内存。只有超级用户才能读取此文件的数据。
       你可以使用gdb的所有命令来获取信息。举个例子,为了打印一个变量的值,你可以:
p gloable_variable
反汇编一个函数
disassemble function
        如果你编译内核的时候使用了-g参数(在内核的Makefile文件的CFLAGS变量中加入-g),gdb还可以提供更多的信息。比如,你可以打印出结构体中存放的信息或是跟踪一个指针。当然,你编译出的内核会大很多,所以不要把编译带调试信息的内核当作一种习惯。
    下来,就要说不幸的那一面了,gdb还是有很多局限的。它没有任何办法修改内核数据。它也不能单步执行内核代码,不能加断点。不能修改内核数据是个非常大的缺陷。尽管在必要时反汇编函数无疑是个非常有用的功能,但是能够修改数据的却更为有用。

kgdb
kgdb是一个补丁,它可以让我们在远端主机上通过串口利用gdb的所有功能对内核进行调试。这需要两台计算机。第一台运行带有kgdb补丁的内核。第二台通过串行线(不通过modem,直接连接两台机器的电缆)使用gdb对第一台进行调试。通过kgdbgdb的所用功能都能使用:读取或修改变量值,设置断点,设置关注变量,单步执行等等。某些版本的gdb甚至允许执行函数。
    设置kgdb和连接串行线比较麻烦,但是一旦做完了,调试就变得很简单了。该补丁会在Documentation/目录下安装很多说明文件,你可以把它们挑出来研究一下。
       不同体系结构、不同内核版本使用的kgdb由不同的人员维护,为了给需要调试的内核找到合适的补丁,还是在网上搜索一下比较好。

kdb
kdbkgdb的一种替代品。不象kgdb,它不是一个远端调试器。kdb这个补丁对内核源代码进行了很多修改,使调试内核在本地主机上就可以进行。它提供了变量修改、设置断点、单步执行等等许多功能。运行调试器非常简单,在终端上敲一下break键就可以了。内核执行oops的时候,它也会自动投入运行。安装kdb之后,可以在Documentation/kdb中找到有关它的详细文档。
       http://oss.sgi.com/可以找到kdb补丁。

在系统中钻营打探

伴随着你调试内核的经验越来越丰富,你获得的在内核中钻营打探以获取答案的技巧也会越来越多。由于调试内核是一种复杂度非常高的考验,所以掌握好任何技巧和窍门都会有所帮助。下面就有一些:

UID作为选择条件
如果你开发的是进程相关的部分,有些时候,你可以在提供替代物的同时不打破原有代码的可执行性。这在你开发重要系统调用的时候,或者在你希望进行调试时系统功能依旧健全的情况下非常有用。

举个例子,假设为了加入一个激动人心的新特性,你重写了fork()系统调用。除非你第一次的尝试就完美无缺,否则系统调试就是一场噩梦。如果fork()系统调用不正常的话,你压根就不用指望整个系统还能正常工作。当然,和任何时候一样,希望总是存在的。
       一般情况下,只要保留原有的算法而把你的新算法加入到其它位置上,基本就能保证安全。你可以利用把用户idUID)作为选择条件来实现这种功能,通过这种选择条件,你可以安排到底执行哪种算法:
if (current->uid != 7777) {
       /* 老算法.. */
} else {
       /* 新算法.. */
}

除了7777以外,其它所有的用户都用的是老算法。你可以创建一个UID7777的用户,专门来测试新算法。对于要求很严格的进程相关部分的代码来说,这种方法使得测试变得容易了许多。

使用条件变量
如果代码与进程无关,或者你希望有一个针对所有情况都能使用的机制来控制某个特性,你可以使用条件变量。这比使用UID还来得简单。你只需要创建一个全局变量作为一个条件选择开关。如果该变量为零,你就是用一个分支上的代码。如果它不为零,你就选择另外一个分支。你可以通过某种界面提供对这个变量的操控,也可以直接通过调试器进行操控。

使用统计量
有些时候你需要掌握某个特定事件的发生规律。有些时候你需要比较多个事件并从中得出规律。通过创建统计量并提供某种机制访问其统计结果,很容易就能满足这种需求。
       举个例子,假设我们希望得到foobar的发生频率,那么在某个文件中,当然最好是在事件所发生的那个文件里,定义两个全局变量:
unsigned long foo_stat = 0;
unsigned long bar_stat = 0;
每当事件发生的时候,就让相应的变量加1。然后按照你喜欢的方式提供这两个变量的访问方法。比如,你可以在/proc目录中创建一个文件,还可以新创建一个系统调用。最简单的办法当然还是通过调试器直接访问它们。
       注意,这种实现并非是SMP安全的。理想的办法是通过原子操作进行实现。但是仅仅对于一个简单的每次加1的调试统计量,一般无需搞得这么麻烦。


重复频率限制
为了发现一个错误,开发者们往往在代码的某个部分加入很多错误检查语句(多数对应的都是一些print语句)。在内核中,有些函数每秒都要被调用很多次。如果你在这样的函数中加入了prink(),那么系统马上就会被显示调试信息这一个任务压得喘不过气来,很快就什么也干不成了。
    有两种相关的技巧可以用来防止此类问题的发生。第一种是重复频率限制,如果某种事件发生的非常频繁,而你又需要观察它的整体进展情况,你就可以让这种技巧施展身手了。为了避免调试信息发生井喷,你可以每隔几秒执行一次打印(或者是其它任何你想完成的操作)。 举个例子:
static unsigned long prev_jiffy = jiffies;           /* 频率限制 */

if (time_after(jiffies, prev_jiffy + 2*HZ)) {
       prev_jiffy = jiffies;
       printk(KERN_ERR “blah blah blah\n”);
}

此例中,调试信息最多两秒打印一次。这可以让你的终端不至于被汹涌而至的调试信息洪流充塞,也保证你的系统依旧能用。你完全可以根据自己的需要,或低或高的调整这种重复频率。
    在你观察某种频繁发生的时间时,还有另一种棘手的问题。与前面的例子不同,你想观察的不是整个事件在内核中进展过程,你只是想在某个事件发生时得到一个通知。可能只要得到一次或是两次就足够了。如果这种通知在被触发一次之后依旧不停的到来,那就比较麻烦了。下面这种技巧针对的就不再是如何限制重复频率了,它要实现的是发生次数限制
static unsigned long limit = 0;

if (limit < 5) {
       limit ++;
       printk(KERN_ERR “blah blah blah\n”);
}

此例中,调试信息输出五次就封顶了。五次之后,打印条件总是不能成立。
    不管是哪种技巧,用到的变量都应该是静态的(static),并且应该限制在函数的局部以内。像例子中展示的那样,这样才能保证在函数的多次调用中变量的值能够保留。
       这些例子的代码都不是SMP或抢占来安全的,不过,只需要用原子操作改造一下就没问题了。可是,这只不过是在调试中才会用到的代码,没有必要搞得那么复杂吧?

二分法查找引发罪恶的变更

知道bug是什么时候被引入内核源代码的通常都是很有用的。如果你知道2.4.18中出现了一个bug,而能肯定2.4.17中没有,那么你就能够很容易的对引发这个bug的代码变更进行定位。消灭bug变得唾手可得,要么取消这个变更,要么对其进行修正。
    可是,很多时候你并不知道到底是哪个内核版本引入了bug。你知道当前版本里bug是确确实实存在的,不过,它好像就是存在于当前版本中。这就需要做一些调查取证了,不过只需要花一点点力气,你就能找出引发问题的代码变更了。元凶在手,消灭bug就指日可待了。
       一开始,你需要一个可靠的可复制的错误。最好是一个系统一启动你就能查证的bug。下来,你需要一个能确保没问题的内核。你应该能够找到。举个例子,你知道几个月前的内核没有这种错误,那么就从那时使用的内核中选取一个。如果发现问题那时就存在了,那么就找更早的。这不会太难——除非bugLinux与生俱来的——一定要找到不含bug的内核。
    下来需要一个肯定有问题的内核。为了简单起见,你应该从已知最早出现该问题的内核开始。
    现在,你就可以在问题内核和良好的内核之间使用二分法了。举个例子,假定确保没有问题的内核版本是2.4.11,有问题的内核版本是2.4.20。从二者的正中选取一个内核版本,像2.4.15。检查2.4.15是否包含此bug。如果2.4.15没有问题,那么你就知道错误是发生在此版本之后了。所以,再从2.4.15开始,在它和2.4.20正中选取下一个版本,比如说2.4.17进行检查。如果2.4.15有问题,那么错误就可能发生在此版本之前了,那么你就该选2.4.13作为下一个待查目标了。就这样重复筛选。
    最终你肯定能把问题局限在两个版本之间——一个包含错误而另外一个不包含。你就能够很容易的对引发这个bug的代码变更进行定位了。
       这种方式比依次对每个版本的内核进行核查要好得多了。

当所有的努力都失败时:社区

或许你已经做完了所有你能想到的尝试。你在键盘上呕心沥血了几个小时——实际上,可能是无数日子——答案依旧没有眷顾你。此时,如果bug是在Linux内核的主流部分中,你可以在内核开发社区中寻求其他开发者的帮助。
    你应该向内核邮件列表发送一份电子邮件,对bug进行完整而又简洁的描述,你的发现可能会对找到最终的答案起到帮助作用。毕竟,没人希望bug存在。
    在后续,“补丁,开发和社区”会重点推荐社区和它最重要的论坛,Linux内核邮件列表(LKML)。



[ 本帖最后由 陈莉君 于 2008-5-12 14:35 编辑 ]
透析真谛,似拨云穿雾;共享智慧,如春风沐浴
http://www.kerneltravel.net
发新话题