发新话题
打印

内核调试(一)

内核调试(一)

艰苦的调试工作是内核级的开发区别于用户级开发的一个显著特点。相对于用户级开发,内核开发确实要艰苦得多。更要命的是,内核的一个错误往往马上就能让系统崩溃——一点情面都不会留。
    越来越轻车熟路地驾驭内核调试的能力——当然,最终是为了能够成功的开发内核——很大一部分取决于你的经验和对整个操作系统的把握。没错,虽然你玉树临风会对别的事情有帮助,但是调试内核的关键还是在于你对内核的深刻理解。然而我们必须有个可以开始着手的地方,所以,在这里我们从调试内核的一种可能步骤开始。
开始前你需要准备什么?那么,你确定要开始了吗?这可能会是一个漫长而又困难重重的苦旅。不少bug已经让整个开发社区几个月都食不甘味了。幸运的是,虽然确实有许多这样难以对付的bug,但也由一些比较简单,而且容易消灭的小bug。所以,你现在要面对的是些简单琐碎的小bug。但不用怕,当然,除非你开始做一些调查,否则你不会清楚到底面对的是什么。现在,你需要的只是:
¨       一个bug。听起来很可笑,但你确实需要一个定义精确的bug。如果错误总是能够再现的话,那对我们会很有帮助,而且有一部分错误确实如此。然而不幸的是,大部分bug通常并非是行为可靠而又定义清楚的。
¨       藏匿bug的内核的版本(一般可以假设是在最新版本的内核里,否则谁又会去理睬它呢?)如果你能搞清楚这个bug最早出现在哪个版本中就再理想不过了。如果你不知道,我们待会儿会解释该怎么办。
¨       一点好运气。
如果你没办法让bug重现出来,下面要讲的这些步骤就毫无意义。问题的关键在于你是否能够让这些错误重复发生。如果你不能,消灭bug就只能通过抽象出问题,再从代码中搜索蛛丝马迹来进行了。虽然有时也得这么做(了解了吧,内核开发者其实也就是这么棒了),但如果你能够让错误重复出现,成功的机会要大许多。
有一个bug存在而有人没办法让它重现,这听起来可能感觉挺奇怪。在用户级的程序里,bug常常表现得很直截了当——比如,执行foo就会让程序立即产生核心信息转储(core dump。但内核全然不是这样。内核、用户程序和硬件之间的交互非常微妙。一个竞争条件往往把它狰狞的面目隐藏在某个算法百万次的迭代之中。设计不佳的甚至是包含错误的代码可能在某些系统上表现得相当不错,而在其它系统上却让人无法忍受。在跟踪bug的时候,对于一些特定的配置、一些特定的机器,付出额外的努力是司空见惯的,要不然的话它可能压根就不会显露原型。在跟踪bug的时候,你掌握的信息越多越好。许多时候,当你可以精确的重现一个bug的时候,你就已经成功了一大半了。
内核中的bug内核中的bug和用户空间应用程序中的bug一样多变。它们的产生可以有无数的原因,同时它们的表象也变化多端。从明白无误的错误代码(比如,没有把正确的值存放在正确的位置)到同步时发生的错误(比如,共享变量锁定不当),都是bug的温床。从降低所有东西的运行性能到毁坏数据,都可能是bug发作时的症状。
    从隐藏在源代码中的错误到展现在目击者面前的bug,其发作往往是一系列连锁反应的事件才可能触发的。举个例子,一个被共享的结构体,如果它没有引用计数,那么它就有可能会引发竞争条件。因为没有引用计数的话,一个进程可以在另外一个进程仍然需要使用该结构的时候就释放掉它。这样做可能导致引用一个空指针,也可能会读出一些垃圾数据,还可能并不产生什么恶果(如果该数据并没有被其它什么覆盖的话)。引用空指针会导致产生一个oops,而垃圾数据可能会导致系统崩溃(这种情形比oops还坏)。用户报告了oops或系统的错误现象之后,开发者回过头来观察错误情形,发现在释放数据之后还会对它进行读写,存在着一个竞争条件,于是就会进行修正,给这个共享的结构加上适当的引用计数。多说一句,对它的访问大多还需要由锁来保证。
    调试内核听起来很难,但事实上Linux内核与其它大型的软件项目也没有什么太大的不同。内核确实有一些独特的问题需要考虑,像定时限制和竞争条件等,它们都是允许多个线程在内核中同时运行产生的结果。我相信你有一定的理解能力也能够付出些努力,所以你应该可以调试内核中的存在的问题(说不定你还会喜欢上这种挑战呢)。

pintk()
内核提供的打印函数printk(),和C库提供的printf()函数功能几乎相同。实际上,在整个这本书中我们都没有用到不同的那些部分。从它实现的大部分意图来说,这个名字很不错;printk()就是内核的格式化打印函数。但是,差异确实也还是存在的。

printk()函数的健壮性
健壮性是printk()函数最容易让人们接受的一个特质。任何时候,任何地方都能调用它,内核中的printk()比比皆是。它可以在中断上下文和进程上下中调用;它可以在持有锁时调用;它可以在多处理器上同时调用,而且调用者连锁都不必使用。
    它是一个弹性极佳的函数。这一点相当重要,printk()之所以这么有用,就在于它随时都在那里而且随时都能用。

printk()的脆弱之处
printk()函数的健壮性也有漏洞。在系统启动过程中,终端还没有初始化之前,在某些地方不能使用它。不过说实在的,如果终端没有初始化,你又能输出到什么地方去呢?
    这一般不是一个什么问题,除非你要调试的是启动过程最开始的那些步骤(比如说在负责执行硬件体系结构相关的初始化动作的setup_arch()函数中)。着手进行这样的调试挑战性很强——没有任何打印函数能用确实让问题更加棘手。
    不过还是有一些指望的,虽然不多。核心硬件部分的黑客依靠此时能够工作的硬件设备(比如说一个串口)与外界通信。相信我,绝大部分人对此都不会感兴趣。一些被支持的体系结构确实靠这种方式实现了健全的解决方案,但是——其它系统(包括i386)都通过提供可用的补丁来扭转局面。
    解决的办法是提供一个printk()的变体函数,在启动过程的初期就具有在终端上打印的能力:early_printk()。它的功能与prink()完全相同,区别仅仅在于名字和它能够更早地工作。不过,由于该函数在一些内核支持的硬件体系结构上无法实现,所以这种这种办法缺少可移植性。可是,如果它能够工作的话,它就是你最好的助手。
    除非你在启动过程的初期就要在终端上输出,你可以认为printk()什么情况下都能工作。

Loglevels
printk()printf()一个最主要的区别就是前者可以指定一个记录级别。内核根据这个级别来判断是否在终端上打印消息。内核把级别比某个特定值低的所有消息显示在终端上。
       可以通过下面这种方式指定一个记录级别:

printk(KERN_WARNING “This is a warning!\n”);
printk(KERN_DEBUG “This is a debug notice!\n”);
printk(“ I did not specify a loglevel!\n”);

KERN_WARINGKERN_DEBUG都是<linux/kernel.h>中的简单宏定义。它们扩展开是像“<4>”或“<7>”这样的字符串,加进printk()函数要打印的消息的开头。内核用这个指定的记录等级和当前终端的记录等级console_loglevel来决定是不是向终端上打印。表1列举了所有可供使用的记录等级。
1 可供使用的记录等级
记录等级
描述
KERN_EMERG
一个紧急情况
KERN_ALERT
一个需要立即被注意到的错误
KERN_CRIT
一个临界情况
KERN_ERR
一个错误
KERN_WARNING
一个警告
KERN_NOTICE
一个普通的,不过也有可能需要注意的情况
KERN_INFO
一条非正式的消息
KERN_DEBUG
一条调试信息——一般是冗余信息

如果你没有特别指定一个记录等级,函数会选用默认的DEFAULT_MESSAGE_LOGLEVEL,现在默认等级是KERN_WARNING。由于这个默认值将来存在变化的可能性,所以还是应该给你自己的消息指定一个记录等级。
    内核将最重要的记录等级KERN_EMERG定为“<0>”,将无关紧要的记录等级“KERN_DEBUG”定为“<7>”。举例来说,当编译预处理完成之后,前例中的代码实际被编译成如下格式:

printk(“<4> This is a warning!\n”);
printk(“<7>This is a debug notice!\n”);
printk(“<4>I did not specify a loglevel!\n”);

怎样给你调用的printk()赋记录等级完全取决于你自己。那些正式的、需要你保持的消息应该有合适的记录等级。但是那些当你试图解决一个问题时加得到处都是的调试信息——必须承认,我们都这么干而且也确实行得通——可以按照你的想法赋给记录等级。一种选择是保持你的终端的默认记录等级不变,给你的所有调试信息KERN_CRIT或更低的等级。相反,你也可以给你的所有调试信息KERN_DEBUG等级,而调整你终端的默认记录等级。两种方法各有利弊,自己拿主意吧。

BTW:我的毕业设计的学生在调试内核代码时,遇到不少问题,在此把《LDK》中调试一章的内容专门贴出来,给大家参考。



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


有用!
谢谢老师~
发新话题