📄 (ldd) ch04-调试技术(转载).htm
字号:
<DIV align=center><FONT color=#ffffff>|</FONT></DIV></TD>
<TD width="8%" height=4>
<DIV align=center><A href="mailto:joyfire@sina.com"><FONT
color=#ffffff>联系</FONT></A></DIV></TD></TR></TBODY></TABLE></TD></TR></TBODY></TABLE>
<TABLE borderColor=#666666 cellPadding=2 width="90%" align=center border=2>
<TBODY>
<TR>
<TD bgColor=#000000>
<P align=center><A href="http://joyfire.net/lsdp/index.htm"><FONT
color=#ffffff size=2>目录页</FONT></A> | <A
href="http://joyfire.net/lsdp/5.htm"><FONT color=#ffffff
size=2>上一页</FONT></A> | <A href="http://joyfire.net/lsdp/7.htm"><FONT
color=#ffffff size=2>下一页</FONT></A></P>
<P align=center><FONT face=黑体 color=#ffffff size=6>(LDD) Ch04-调试技术(转载)
</FONT></P><SPAN style="LINE-HEIGHT: 1; LETTER-SPACING: 0pt"><FONT
color=#ffffff size=3>
<P>发信人: Altmayer (alt), 信区: GNULinux<BR>标 题: (LDD) Ch04-调试技术(转载)<BR>发信站: 饮水思源 (2001年12月13日08:57:14 星期四), 站内信件<BR> <BR>【 以下文字转载自 <FONT
color=#00ff00>UNIXpost </FONT>讨论区 】<BR>【 原文由<FONT
color=#00ff00> altmayer.bbs@bbs.nju.edu.cn,</FONT> 所发表 】<BR> <BR>【 以下文字转载自 <FONT
color=#00ff00>altmayer </FONT>的信箱 】<BR> <BR>第4章 调试技术<BR> <BR> <BR>对于任何编写内核代码的人来说,最吸引他们注意的问题之一就是如何完成调试。由于<BR>内核是一个不与某个进程相关的功能集,其代码不能很轻松地放在调试器中执行,而且<BR>也不能跟踪。<BR> <BR>本章介绍你可以用来监视内核代码和跟踪错误的技术。<BR> <BR>用打印信息调试<BR>最一般的调试技术就是监视,就是在应用内部合适的点加上printf调用。当你调试内核<BR>代码的时候,你可以用printk完成这个任务。<BR> <BR>Printk<BR></P></FONT><FONT
color=#ffffff size=3>
<P>Printk<BR>在前些章中,我们简单假设printk工作起来和printf很类似。现在是介绍一下它们之间<BR>不同的时候了。<BR> <BR>其中一个不同点就是,printk允许你根据它们的严重程度,通过附加不同的“记录级”<BR>来对消息分类,或赋予消息优先级。你可以用宏来指示记录级。例如,KERN_INFO,我们<BR>前面已经看到它被加在打印语句的前面,它就是一种可能的消息记录级。记录级宏展开<BR>为一个字串,在编译时和消息文本拼接在一起;这也就是为什么下面的例子中优先级和<BR>格式字串间没有逗号。这有两个printk的例子,一个是调试信息,一个是关键信息:<BR> <BR>(代码)<BR> <BR>在<linux/kernel.h>中定义了8种记录级别串。没有指定优先级的printk语句默认使用DE<BR>FAULT_MESSAGE_LOGLEVEL优先级,它是一个在kernel/printk.c中定义的整数。默认记录<BR>级的具体数值在Linux的开发期间曾变化过若干次,所以我建议你最好总是指定一个合适<BR>的记录级。<BR> <BR>根据记录级,内核将消息打印到当前文本控制台上:如果优先级低于console_loglevel<BR>这个数值的话,该消息就显示在控制台上。如果系统同时运行了klogd和syslogd,无论c<BR>onsole_loglevel为何值,内核都将消息追加到/var/log/messages中。<BR> <BR>变量console_loglevel最初初始化为DEFAULT_CONSOLE_LOGLEVEL,但可以通过sys_syslo<BR>g系统调用修改。如klogd的手册所示,可以在启动klogd时指定-c开关来修改这个变量。<BR></P></FONT><FONT
color=#ffffff size=3>
<P>g系统调用修改。如klogd的手册所示,可以在启动klogd时指定-c开关来修改这个变量。<BR>此外,你还可以写个程序来改变控制台记录级。你可以在O’Reilly站点上的源文件中找<BR>到我写的一个这种功能的程序,miscprogs/setlevel.c。新优先级是通过一个1到8之间<BR>的整数值指定的。<BR> <BR>你也许需要在内核失效后降低记录级(见“调试系统故障”),这是因为失效处理代码<BR>会将console_loglevel提升到15,之后所有的消息都会出现在控制台上。为看到你的调<BR>试信息,如果你运行的是内核2.0.x话,你需要提升记录级。内核2.0发行降低了MINIMUM<BR>_CONSOLE_LOGLEVEL,而旧版本的klogd默认情况下要打印很多控制消息。如果你碰巧使<BR>用了这个旧版本的守护进程,除非你提升记录级,内核2.0会比你预期的打印出更少的消<BR>息。这就是为什么hello.c中使用了<1>标记,这样可以保证消息显示在控制台上。<BR> <BR>从1.3.43一来的内核版本通过允许你向指定虚控制台发送消息,藉此提供一个灵活的记<BR>录策略。默认情况下,“控制台”是当前虚终端。也可以选择不同的虚终端接收消息,<BR>你只需向所选的虚终端调用ioctl(TIOCLINUX)。如下程序,setconsole,可以用来选择<BR>哪个虚终端接收内核消息;它必须以超级用户身份运行。如果你对ioctl还不有把握,你<BR>可以跳过这至下一节,等到读完第5章“字符设备驱动程序的扩展操作”的“ioctl”一<BR>节后,再回到这里读这段代码。<BR> <BR>(代码)<BR> <BR>setconsole使用了用于Linux专用功能的特殊的ioctl命令TIOCLINUX。为了使用TIOCLINU<BR>X,你要传递给它一个指向字节数组的指针。数组的第一个字节是所请求的子命令的编码<BR></P></FONT><FONT
color=#ffffff size=3>
<P>X,你要传递给它一个指向字节数组的指针。数组的第一个字节是所请求的子命令的编码<BR>,随后的字节依命令而不同。在setconsole中使用了子命令11,后一个字节(存放在byt<BR>es[1]中)标别虚拟控制台。TIOCLINUX的完成介绍可以在内核源码drivers/char/tty_io<BR>..c中找到。<BR> <BR>消息是如何记录的<BR>printk函数将消息写到一个长度为LOG_BUF_LEN个字节的循环缓冲区中。然后唤醒任何等<BR>待消息的进程,即那些在调用syslog系统调用或读取/proc/kmesg过程中睡眠的进程。这<BR>两个访问记录引擎的接口是等价的。不过/proc/kmesg文件更象一个FIFO文件,从中读取<BR>数据更容易些。一跳简单的cat命令就可以读取消息。<BR> <BR>如果循环缓冲区填满了,printk就绕到缓冲区的开始处填写新数据,覆盖旧数据。于是<BR>记录进程就丢失了最旧的数据。这个问题与利用循环缓冲区所获得的好处相比可以忽略<BR>不计。例如,循环缓冲区可以使系统在没有记录进程的情况下照样运行,同时又不浪费<BR>内存。Linux处理消息的方法的另一个特点是,可以在任何地方调用printk,甚至在中断<BR>处理函数里也可以调用,而且对数据量的大小没有限制。这个方法的唯一缺点就是可能<BR>丢失某些数据。<BR> <BR>如果klogd正在运行,它读取内核消息并将它们分派到syslogd,它随后检查/etc/syslog<BR>..conf找到处理这些数据的方式。syslogd根据一个“设施”和“优先级”切分消息;可<BR>以使用的值定义在<sys/syslog.h>中。内核消息根据相应printk中指定的优先级记录到L<BR>OG_KERN设施中。如果klogd没有运行,数据将保存在循环缓冲区中直到有进程来读取数<BR>据或数据溢出。<BR></P></FONT><FONT
color=#ffffff size=3>
<P>据或数据溢出。<BR> <BR>如果你不希望因监视你的驱动程序的消息而把你的系统记录搞乱,你给klogd指定-f(文<BR>件)选项或修改/etc/syslog.conf将记录写到另一个文件中。另一种方法是一种强硬方<BR>法:杀掉klogd,将消息打印到不用的虚终端上*,或者在一个不用的xterm上执行cat<BR>/proc/kmesg显示消息。<BR> <BR>使用预处理方便监视处理<BR>在驱动程序开发早期,printk可以对调试和测试新代码都非常有帮助。然而当你正式发<BR>行驱动程序时,你应该去掉,或者至少关闭,这些打印语句。很不幸,你可能很快就发<BR>现,随着你想不再需要那些消息并去掉它们时,你可能又要加新功能,你又需要这些消<BR>息了。解决这些问题有几种方法――如何从全局打开和关闭消息以及如何打开和关闭个<BR>别消息。<BR> <BR>下面给出了我处理消息所用的大部分代码,它有如下一些功能:<BR> <BR>l 可以通过在宏名字加一个字母或去掉一个字母打开或关闭每一条语句。<BR> <BR>l 通过在编译前修改CFLAGS变量,可以一次关闭所有消息。<BR> <BR>l 同样的打印语句既可以用在内核态(驱动程序)也可以用在用户态(演示或测<BR>试程序)。<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>下面这些直接来自scull.h的代码片断实现了这些功能。<BR> <BR>(代码)<BR> <BR>符合PDEBUG和PDEBUGG依赖于是否定义了SCULL_DEBUG,它们都和printf调用很类似。<BR> <BR>为了进一步方便这个过程,在你的Makefile加上如下几行。<BR> <BR>(代码)<BR> <BR>本节所给出的代码依赖于gcc对ANSI C预编译器的扩展,gcc可以支持带可变数目参数的<BR>宏。这种对gcc的依赖并不是什么问题,因为内核对gcc特性的依赖更强。此外,Makefil<BR>e依赖于GNU的gmake;基于同样的道理,这也不是什么问题。<BR> <BR>如果你很熟悉C预编译器,你可以将上面的定义扩展为可以支持“调试级”概念的,可以<BR>为每级赋一个整数(或位图),说明这一级打印多么琐碎的消息。<BR> <BR>但是每一个驱动程序都有它自己的功能和监视需求。好的编程技巧会在灵活性和高效之<BR>间找到一个权衡点,这个我就不能说哪个对你最好了。记住,预编译器条件(还有代码<BR>中的常量表达式)只到编译时运行,你必须重新编译程序来打开或关闭消息。另一种方<BR>法就是使用C条件语句,它在运行时运行,因此可以让你在程序执行期间打开或关闭消息<BR>。这个功能很好,但每次代码执行系统都要进行额外的处理,甚至在消息关闭后仍然会<BR></P></FONT><FONT
color=#ffffff size=3>
<P>。这个功能很好,但每次代码执行系统都要进行额外的处理,甚至在消息关闭后仍然会<BR>影响性能。有时这种性能损失是无法接受的。<BR> <BR>个人观点,尽管上面给出的宏迫使你每次要增加或去掉消息时都要重新编译,重新加载<BR>模块,但我觉得用这些宏已经很好了。<BR> <BR>通过查询调试<BR>上一节谈到了printk是如何工作的以及如何使用它。但没有谈及它的缺点。<BR> <BR>由于syslogd会一直保持刷新它的输出文件,每打印一行都会引起一次磁盘操作,因此过<BR>量使用printk会严重降低系统性能。至少从syslogd的角度看是这样的。它会将所有的数<BR>据都一股脑地写到磁盘上,以防在打印消息后系统崩溃;然而,你不想因为调试信息的<BR>缘故而降低系统性能。这个问题可以通过在/etc/syslogd.conf中记录文件的名字前加一<BR>个波折号解决,但有时你不想修改你的配置文件。如果不这样,你还可以运行一个非klo<BR>gd的程序(如前面介绍的cat /proc/kmesg),但这样并不能为正常操作提供一个合适的<BR>环境。<BR> <BR>与这相比,最好的方法就是在你需要信息的时候,通过查询系统获得相关信息,而不是<BR>持续不断地产生数据。事实上,每一个Unix系统都提供了很多工具用来获得系统信息:p<BR>s,netstat,vmstat等等。<BR> <BR>有许多技术适合与驱动程序开发人员查询系统,简而言之就是,在/proc下创建文件和使<BR>用ioctl驱动程序方法。<BR></P></FONT><FONT
color=#ffffff size=3>
<P>用ioctl驱动程序方法。<BR> <BR>使用/proc文件系统<BR>Linux中的/proc文件系统与任何设备都没有关系――/proc中的文件都在被读取时有核心<BR>创建的。这些文件都是普通的文本文件,它们基本上可由普通人理解,也可被工具程序<BR>理解。例如,对于大多数Linux的ps实现而言,它都通过读取/proc文件系统获得进程表<BR>信息的。/proc虚拟文件的创意已由若干现代操作系统使用,且非常成功。<BR> <BR>/proc的当前实现可以动态创建i节点,允许用户模块为方便信息检索创建如何入口点。<BR> <BR>为了在/proc中创建一个健全的文件节点(可以read,write,seek等等),你需要定义f<BR>ile_operations结构和inode_operations结构,后者与前者有类似的作用和尺寸。创建<BR>这样一个i节点比起创建整个字符设备并没有什么不同。我们这里不讨论这个问题,如果<BR>你感兴趣,你可以在源码树fs/proc中获得进一步细节。<BR> <BR>与大多数/proc文件一样,如果文件节点仅仅用来读,创建它们是比较容易的,我将这里<BR>介绍这一技术。很不幸,这一技术只能在Linux 2.0及其后续版本中使用。<BR> <BR>这里是创建一个称为/proc/scullmem文件的scull代码,这个文件用来获取scull使用的<BR>内存信息。<BR> <BR>(代码)<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>填写/proc文件非常容易。你的函数获取一个空闲页面填写数据;它将数据写进缓冲区并<BR>返回所写数据的长度。其他事情都由/proc文件系统处理。唯一的限制就是所写的数据不<BR>能超过PAGE_SIZE个字节(宏PAGE_SIZE定义在头文件<asm/page.h>中;它是与体系结构<BR>相关的,但你至少可以它有4KB大小)。<BR> <BR>如果你需要写多于一个页面的数据,你必须实现功能健全的文件。<BR> <BR>注意,如果一个正在读你的/proc文件的进程发出了若干read调用,每一个都获取新数据<BR>,尽管只有少量数据被读取,你的驱动程序每次都要重写整个缓冲区。这些额外的工作<BR>会使系统性能下降,而且如果文件产生的数据与下一次的不同,以后的read调用要重新<BR>装配不相关的部分,这一会造成数据错位。事实上,由于每个使用C库的应用程序都大块<BR>地读取数据,性能并不是什么问题。然而,由于错位时有发生,它倒是一个值得考虑的<BR>问题。在获取数据后,库调用至少要调用1次read――只有当read返回0时才报告文件尾<BR>。如果驱动程序碰巧比前面产生了更多的数据,系统就返回到用户空间额外的字节并且<BR>与前面的数据块是错位的。我们将在第6章“时间流”的“任务队列”一节中涉及/proc/<BR>jiq*,那时我们还会遇到错位问题。<BR> <BR>cleanup_module中应该使用下面的语句注销/proc节点:<BR> <BR>(代码)<BR> <BR>传递给函数的参数是包含要撤销文件的目录名和文件的i节点号。由于i节点号是自动分<BR></P></FONT><FONT
color=#ffffff size=3>
<P>传递给函数的参数是包含要撤销文件的目录名和文件的i节点号。由于i节点号是自动分<BR>配的,在编译时是无法知道的,必须从数据结构中读取。<BR> <BR>ioctl方法<BR>ioctl,下一章将详细讨论,是一个系统调用,它可以操做在文件描述符上;它接收一个<BR>“命令”号和(可选的)一个参数,通常这是一个指针。<BR> <BR>做为替代/proc文件系统的方法,你可以为调试实现若干ioctl命令。这些命令从驱动程<BR>序空间复制相关数据到进程空间,在进程空间里检查这些数据。<BR> <BR>只有使用ioctl获取信息比起/proc来要困难一些,因为你一个程序调用ioctl并显示结果<BR>。必须编写这样的程序,还要编译,保持与你测试的模块间的一致性等。<BR> <BR>不过有时候这是最好的获取信息的方法,因为它比起读/proc来要快得多。如果在数据写<BR>到屏幕前必须完成某些处理工作,以二进制获取数据要比读取文本文件有效得多。此外<BR>,ioctl不限制返回数据的大小。<BR> <BR>ioctl方法的一个优点是,当调试关闭后调试命令仍然可以保留在驱动程序中。/proc文<BR>件对任何查看这个目录的人都是可见的,然而与/proc文件不同,未公开的ioctl命令通<BR>常都不会被注意到。此外,如果驱动程序有什么异常,它们仍然可以用来调试。唯一的<BR>缺点就是模块会稍微大一些。<BR> <BR>通过监视调试<BR></P></FONT><FONT
color=#ffffff size=3>
<P>通过监视调试<BR>有时你遇到的问题并不特别糟,通过在用户空间运行应用程序来查看驱动程序与系统之<BR>间的交互过程可以帮助你捕捉到一些小问题,并可以验证驱动程序确实工作正常。例如<BR>,看到scull的read实现如何处理不同数据量的read请求后,我对scull更有信心。<BR> <BR>有许多方法监视一个用户态程序的工作情况。你可以用调试器一步步跟踪它的函数,插<BR>入打印语句,或者用strace运行程序。在实际目的是查看内核代码时,最后一项技术非<BR>常有用。<BR> <BR>strace命令是一个功能非常强大的工具,它可以现实程序所调用的所有系统调用。它不<BR>仅可以显示调用,而且还能显示调用的参数,以符号方式显示返回值。当系统调用失败<BR>时,错误的符号值(如,ENOMEM)和对应的字串(Out of memory)同时显示。strace还<BR>有许多命令行选项;最常用的是-t,它用来显示调用发生的时间,-T,显示调用所花费<BR>的时间,以及-o,将输出重定向到一个文件中。默认情况下,strace将所有跟踪信息打<BR>印到stderr上。<BR> <BR>strace从内核接收信息。这意味着一个程序无论是否按调试方式编译(用gcc的-g选项)<BR>或是被去掉了符号信息都可以被跟踪。与调试器可以连接到一个运行进程并控制它类似<BR>,你还可以跟踪一个已经运行的进程。<BR> <BR>跟踪信息通常用来生成错误报告报告给应用开发人员,但是对内核编程人员来说也一样<BR>非常有用。我们可以看到系统调用是如何执行驱动程序代码的;strace允许我们检查每<BR>一次调用输入输出的一致性。<BR></P></FONT><FONT
color=#ffffff size=3>
<P>一次调用输入输出的一致性。<BR> <BR>例如,下面的屏幕输出给出了命令ls /dev > /dev/scull0的最后几行:<BR> <BR>(代码)<BR> <BR>很明显,在ls完成目标目录的检索后首次对write的调用中,它试图写4KB。很奇怪,只<BR>写了4000个字节,接着重试这一操作。然而,我们知道scull的write实现每次只写一个<BR>量子,我在这里看到了部分写。经过若干步骤之后,所有的东西都清空了,程序正常退<BR>出。<BR> <BR>另一个例子,让我们来读scull设备:<BR> <BR>(代码)<BR> <BR>正如所料,read每次只能读到4000个字节,但是数据总量是不变的。注意本例中重试工<BR>作是如何组织的,注意它与上面写跟踪的对比。wc专门为快速读数据进行了优化,它绕<BR>过了标准库,以便每次用一个系统调用读取更多的数据。你可以从跟踪的read行中看到w<BR>c每次要读16KB。<BR> <BR>Unix专家可以在strace的输出中找到很多有用信息。如果你被这些符号搞得满头雾水,<BR>我可以只看文件方法(open,read等等)是如何工作的。<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>个人认为,跟踪工具在查明系统调用的运行时错误过程中最有用。通常应用或演示程序<BR>中的perror调用不足以用来调试,而且对于查明到底是什么样的参数触发了系统调用的<BR>错误也很有帮助。<BR> <BR>调试系统故障<BR>即便你用了所有监视和调试技术,有时候驱动程序中依然有错误,当这样的驱动程序执<BR>行会会造成系统故障。当这种情况发生时,获取足够多的信息来解决问题是至关重要的<BR>。<BR> <BR>注意,“故障”不意味着“panic”。Linux代码非常鲁棒,可以很好地响应大部分错误<BR>:故障通常会导致当前进程的终止,但系统继续运行。如果在进程上下文之外发生故障<BR>,或是组成系统的重要部件发生故障时,系统可能panic。但问题出在驱动程序时,通常<BR>只会导致产生故障的进程终止――即那个使用驱动程序的进程。唯一不可恢复的损失就<BR>是当进程被终止时,进程上下文分配的内存丢失了;例如,由驱动程序通过kmalloc分配<BR>的动态链表可能丢失。然而,由于内核会对尚是打开的设备调用close,你的驱动程序可<BR>以释放任何有open方法分配的资源。<BR> <BR>我们已经说过,当内核行为异常时会在控制台上显示一些有用的信息。下一节将解释如<BR>何解码和使用这些消息。尽管它们对于初学者来说相当晦涩,处理器的给出数据都是些<BR>很有意思的信息,通常无需额外测试就可以查明程序错误。<BR> <BR>Oops消息<BR></P></FONT><FONT
color=#ffffff size=3>
<P>Oops消息<BR>大部分错误都是NULL指针引用或使用其他不正确的指针数值。这些错误通常会导致一个o<BR>ops消息。<BR> <BR>由处理器使用的地址都是“虚”地址,而且通过一个复杂的称为页表(见第13章“Mmap<BR>和DMA”中的“页表”一节)的结构映射为物理地址。当引用一个非法指针时,页面映射<BR>机制就不能将地址映射到物理地址,并且处理器向操作系统发出一个“页面失效”。如<BR>果地址确实是非法的,内核就无法从失效地址上“换页”;如果此时处理在超级用户太<BR>,系统于是就产生一个“oops”。值得注意的是,在版本2.1中内核处理失效的方式有所<BR>变化,它可以处理在超级用户态的非法地址引用了。新实现将在第17章“最近发展”的<BR>“处理内核空间失效”中介绍。<BR> <BR>oops显示故障时的处理器状态,模块CPU寄存器内容,页描述符表的位置,以及其他似乎<BR>不能理解的信息。这些是由失效处理函数(arch/*/kernel/traps.c)中的printk语句产<BR>生的,而且象前面“Printk”一节介绍的那样进行分派。<BR> <BR>让我们看看这样一个消息。这里给出的是传统个人电脑(x86平台),运行Linux 2.0或<BR>更新版本的oops――版本1.2的输出稍有不同。<BR> <BR>(代码)<BR> <BR>上面的消息是在一个有意加入错误的失效模块上运行cat所至。fault.c崩溃如下代码:<BR> <BR></P></FONT><FONT
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -