内核模块调试

本文详细介绍了在Linux内核开发中如何进行调试,包括通过printk进行打印调试,通过查询调试如使用/proc文件系统,以及使用gdb、kdb等内核调试工具。文章强调了在不同阶段选择不同调试方法的重要性,如printk的灵活性和可能的性能影响,以及gdb的便利性但限制。此外,还提到了kdb的断点设置和堆栈跟踪,以及内核崩溃转储分析器和用户模式Linux虚拟机在调试中的应用。
摘要由CSDN通过智能技术生成

对 于任何一位内核代码的编写者来说,最急迫的问题之一就是如何完成调试。由于内核是一个不与特定进程相关的功能集合,所以内核代码无法轻易地放在调试器中执 行,而且也很难跟踪。同样,要想复现内核代码中的错误也是相当困难的,因为这种错误可能导致整个系统崩溃,这样也就破坏了可以用来跟踪它们的现场。

本章将介绍在这种令人痛苦的环境下监视内核代码并跟踪错误的技术。

4.1  通过打印调试
最普通的调试技术就是监视,即在应用程序编程中,在一些适当的地点调用printf 显示监视信息。调试内核代码的时候,则可以用 printk 来完成相同的工作。

4.1.1  printk
在前面的章节中,我们只是简单假设 printk 工作起来和 printf 很类似。现在则是介绍它们之间一些不同点的时候了。

其 中一个差别就是,通过附加不同日志级别(loglevel),或者说消息优先级,可让 printk根据这些级别所标示的严重程度,对消息进行分类。一般采用宏来指示日志级别,例如,KERN_INFO,我们在前面已经看到它被添加在一些打 印语句的前面,它就是一个可以使用的消息日志级别。日志级别宏展开为一个字符串,在编译时由预处理器将它和消息文本拼接在一起;这也就是为什么下面的例子 中优先级和格式字串间没有逗号的原因。下面有两个 printk 的例子,一个是调试信息,一个是临界信息:

printk(KERN_DEBUG "Here I am: %s:%i/n", _ _FILE_ _, _ _LINE_ _);
printk(KERN_CRIT "I'm trashed; giving up on %p/n", ptr);

在头文件 <linux/kernel.h> 中定义了 8 种可用的日志级别字符串。

KERN_EMERG
用于紧急事件消息,它们一般是系统崩溃之前提示的消息。

KERN_ALERT
用于需要立即采取动作的情况。

KERN_CRIT
临界状态,通常涉及严重的硬件或软件操作失败。

KERN_ERR
用于报告错误状态;设备驱动程序会经常使用 KERN_ERR 来报告来自硬件的问题。

KERN_WARNING
对可能出现问题的情况进行警告,这类情况通常不会对系统造成严重问题。

KERN_NOTICE
有必要进行提示的正常情形。许多与安全相关的状况用这个级别进行汇报。

KERN_INFO
提示性信息。很多驱动程序在启动的时候,以这个级别打印出它们找到的硬件信息。

KERN_DEBUG
用于调试信息。

每个字符串(以宏的形式展开)代表一个尖括号中的整数。整数值的范围从0到7,数值越小,优先级就越高。

没 有指定优先级的 printk 语句默认采用的级别是 DEFAULT_MESSAGE_LOGLEVEL,这个宏在文件 kernel/printk.c 中指定为一个整数值。在 Linux 的开发过程中,这个默认的级别值已经有过好几次变化,所以我们建议读者始终指定一个明确的级别。

根 据日志级别,内核可能会把消息打印到当前控制台上,这个控制台可以是一个字符模式的终端、一个串口打印机或是一个并口打印机。如果优先级小于 console_loglevel 这个整数值的话,消息才能显示出来。如果系统同时运行了 klogd  和 syslogd,则无论 console_loglevel 为何值,内核消息都将追加到 /var/log/messages 中(否则的话,除此之外的处理方式就依赖于对 syslogd 的设置)。如果 klogd 没有运行,这些消息就不会传递到用户空间,这种情况下,就只好查看 /proc/kmsg 了。

变 量 console_loglevel 的初始值是 DEFAULT_CONSOLE_LOGLEVEL,而且还可以通过sys_syslog 系统调用进行修改。调用 klogd 时可以指定 -c 开关选项来修改这个变量, klogd 的 man 手册页对此有详细说明。注意,要修改它的当前值,必须先杀掉 klogd,再加 -c选项重新启动它。此外,还可以编写程序来改变控制台日志级别。读者可以在 O’Reilly 的 FTP 站点提供的源文件 miscprogs/setlevel.c 里找到这样的一段程序。新优先级被指定为一个 1 到 8 之间的整数值。如果值被设为 1,则只有级别为 0(KERN_EMERG) 的消息才能到达控制台;如果设为 8,则包括调试信息在内的所有消息都能显示出来。

如 果在控制台上工作,而且常常遇到内核错误(参见本章后面的“调试系统故障”一节)的话,就有必要降低日志级别,因为出错处理代码会把 console_loglevel 增为它的最大数值,导致随后的所有消息都显示在控制台上。如果需要查看调试信息,就有必要提高日志级别;这在远程调试内核,并且在交互会话未使用文本控制 台的情况下,是很有帮助的。

从2.1.31这个版本起,可以通过文本文件 /proc/sys/kernel/printk 来读取和修改控制台的日志级别。这个文件容纳了 4 个整数值。读者可能会对前面两个感兴趣:控制台的当前日志级别和默认日志级别。例如,在最近的这些内核版本中,可以通过简单地输入下面的命令使所有的内核 消息得到显示:

# echo 8 > /proc/sys/kernel/printk

不过,如果仍在 2.0 版本下的话,就需要使用 setlevel 这样的工具了。

现在大家应该清楚为什么在 hello.c范例中使用 <1> 这些标记了,它们用来确保这些消息能在控制台上显示出来。

对 于控制台日志策略,Linux考虑到了某些灵活性,也就是说,可以发送消息到一个指定的虚拟控制台(假如控制台是文本屏幕的话)。默认情况下,“控制台” 就是当前地虚拟终端。可以在任何一个控制台设备上调用 ioctl(TIOCLINUX),来指定接收消息的虚拟终端。下面的 setconsole  程序,可选择专门用来接收内核消息的控制台;这个程序必须由超级用户运行,在 misc-progs 目录里可以找到它。下面是程序的代码:

int main(int argc, char **argv)
{
  char bytes[2] = {11,0}; /* 11 is the TIOCLINUX cmd number */

  if (argc==2) bytes[1] = atoi(argv[1]); /* the chosen console */
  else {
      fprintf(stderr, "%s: need a single arg/n",argv[0]); exit(1);
  }
  if (ioctl(STDIN_FILENO, TIOCLINUX, bytes)<0) {    /* use stdin */
      fprintf(stderr,"%s: ioctl(stdin, TIOCLINUX): %s/n",
              argv[0], strerror(errno));
      exit(1);
  }
  exit(0);
}

setconsole 使用了特殊的ioctl命令:TIOCLINUX ,这个命令可以完成一些特定的 Linux 功能。使用 TIOCLINUX 时,需要传给它一个指向字节数组的指针参数。数组的第一个字节指定所请求子命令的数字,接下去的字节所具有的功能则由这个子命令决定。在 setconsole 中,使用的子命令是 11,后面那个字节(存于bytes[1]中)标识虚拟控制台。关于 TIOCLINUX 的详尽描述可以在内核源码中的 drivers/char/tty_io.c 文件得到。

4.1.2  消息如何被记录
printk 函数将消息写到一个长度为 LOG_BUF_LEN(定义在 kernel/printk.c 中)字节的循环缓冲区中,然后唤醒任何正在等待消息的进程,即那些睡眠在 syslog 系统调用上的进程,或者读取 /proc/kmesg 的进程。这两个访问日志引擎的接口几乎是等价的,不过请注意,对 /proc/kmesg 进行读操作时,日志缓冲区中被读取的数据就不再保留,而 syslog 系统调用却能随意地返回日志数据,并保留这些数据以便其它进程也能使用。一般而言,读 /proc 文件要容易些,这使它成为 klogd 的默认方法。

手工读取内核消息时,在停止klogd之后,可以发现 /proc 文件很象一个FIFO,读进程会阻塞在里面以等待更多的数据。显然,如果已经有 klogd 或其它的进程正在读取相同的数据,就不能采用这种方法进行消息读取,因为会与这些进程发生竞争。

如 果循环缓冲区填满了,printk就绕回缓冲区的开始处填写新数据,覆盖最陈旧的数据,于是记录进程就会丢失最早的数据。但与使用循环缓冲区所带来的好处 相比,这个问题可以忽略不计。例如,循环缓冲区可以使系统在没有记录进程的情况下照样运行,同时覆盖那些不再会有人去读的旧数据,从而使内存的浪费减到最 少。Linux消息处理方法的另一个特点是,可以在任何地方调用printk,甚至在中断处理函数里也可以调用,而且对数据量的大小没有限制。而这个方法 的唯一缺点就是可能丢失某些数据。

klogd 运行时,会读取内核消息并将它们分发到 syslogd,syslogd 随后查看 /etc/syslog.conf ,找出处理这些数据的方法。syslogd 根据设施和优先级对消息进行区分;这两者的允许值均定义在 <sys/syslog.h> 中。内核消息由 LOG_KERN 设施记录,并以 printk 中使用的优先级记录(例如,printk 中使用的 KERN_ERR对应于syslogd 中的 LOG_ERR)。如果没有运行 klogd,数据将保留在循环缓冲区中,直到某个进程读取或缓冲区溢出为止。

如 果想避免因为来自驱动程序的大量监视信息而扰乱系统日志,则可以为 klogd 指定 -f (file) 选项,指示 klogd 将消息保存到某个特定的文件,或者修改 /etc/syslog.conf 来适应自己的需求。另一种可能的办法是采取强硬措施:杀掉klogd,而将消息详细地打印到空闲的虚拟终端上。*

注: 例如,使用下面的命令可设置 10 号终端用于消息的显示:
setlevel 8
setconsole 10

或者在一个未使用的 xterm 上执行cat /proc/kmesg来显示消息。

4.1.3  开启及关闭消息
在 驱动程序开发的初期阶段,printk 对于调试和测试新代码是相当有帮助的。不过,当正式发布驱动程序时,就得删除这些打印语句,或至少让它们失效。不幸的是,你可能会发现这样的情况,在删除 了那些已被认为不再需要的提示消息后,又需要实现一个新的功能(或是有人发现了一个 bug),这时,又希望至少把一部分消息重新开启。这两个问题可以通过几个办法解决,以便全局地开启或禁止消息,并能对个别消息进行开关控制。

我们在这里给出了一个编写 printk 调用的方法,可个别或全局地对它们进行开关;这个技巧是定义一个宏,在需要时,这个宏展开为一个printk(或printf)调用。

可以通过在宏名字中删减或增加一个字母,打开或关闭每一条打印语句。

编译前修改 CFLAGS 变量,则可以一次关闭所有消息。

同样的打印语句既可以用在内核态也可以用在用户态,因此,关于这些额外的信息,驱动和测试程序可以用同样的方法来进行管理。

下面这些来自 scull.h 的代码,就实现了这些功能。

#undef PDEBUG             /* undef it, just in case */
#ifdef SCULL_DEBUG
#  ifdef _ _KERNEL_ _
   /* This one if debugging is on, and kernel space */
#    define PDEBUG(fmt, args...) printk( KERN_DEBUG "scull: " fmt,
                                       ## args)
#  else
   /* This one for user space */
#    define PDEBUG(fmt, args...) fprintf(stderr, fmt, ## args)
#  endif
#else
#  define PDEBUG(fmt, args...) /* not debugging: nothing */
#endif

#undef PDEBUGG
#define PDEBUGG(fmt, args...) /* nothing: it's a placeholder */

符 号 PDEBUG 依赖于是否定义了SCULL_DEBUG,它能根据代码所运行的环境选择合适的方式显示信息:内核态运行时使用printk系统调用;用户态下则使用 libc调用fprintf,向标准错误设备进行输出。符号PDEBUGG则什么也不做;它可以用来将打印语句注释掉,而不必把它们完全删除。

为了进一步简化这个过程,可以在 Makefile加上下面几行:

# Comment/uncomment the following line to disable/enable debugging
DEBUG = y

# Add your debugging flag (or not) to CFLAGS
ifeq ($(DEBUG),y)
DEBFLAGS = -O -g -DSCULL_DEBUG # "-O" is needed to expand inlines
else
DEBFLAGS = -O2
endif

CFLAGS += $(DEBFLAGS)

本 节所给出的宏依赖于gcc 对ANSI C预编译器的扩展,这种扩展支持了带可变数目参数的宏。对 gcc 的这种依赖并不是什么问题,因为内核对 gcc 特性的依赖更强。此外,Makefile依赖于 GNU 的make 版本;基于同样的道理,这也不是什么问题。

如果读者熟悉 C 预编译器,可以将上面的定义进行扩展,实现“调试级别”的概念,这需要定义一组不同的级别,并为每个级别赋一个整数(或位掩码),用以决定各个级别消息的详细程度。

但 是每一个驱动程序都会有自身的功能和监视需求。良好的编程技术在于选择灵活性和效率的最佳折衷点,对读者来说,我们无法预知最合适的点在哪里。记住,预处 理程序的条件语句(以及代码中的常量表达式)只在编译时执行,要再次打开或关闭消息必须重新编译。另一种方法就是使用C条件语句,它在运行时执行,因此可 以在程序运行期间打开或关闭消息。这是个很好的功能,但每次代码执行时系统都要进行额外的处理,甚至在消息关闭后仍然会影响性能。有时这种性能损失是无法 接受的。

在很多情况下,本节提到的这些宏都已被证实是很有用的,仅有的缺点是每次开启和关闭消息显示时都要重新编译模块。

4.2  通过查询调试
上一节讲述了 printk 是如何工作的以及如何使用它,但还没谈到它的缺点。

由 于 syslogd 会一直保持对其输出文件的同步刷新,每打印一行都会引起一次磁盘操作,因此大量使用 printk 会严重降低系统性能。从 syslogd 的角度来看,这样的处理是正确的。它试图把每件事情都记录到磁盘上,以防系统万一崩溃时,最后的记录信息能反应崩溃前的状况;然而,因处理调试信息而使系 统性能减慢,是大家所不希望的。这个问题可以通过在 /etc/syslogd.conf 中日志文件的名字前面,前缀一个减号符解决。*

注: 这个减号是个“特殊”标记,避免 syslogd 在每次出现新信息时都去刷新磁盘文件,这些内容记述在 syslog.conf(5) 中,这个手册页很值得一读。


修 改配置文件带来的问题在于,在完成调试之后改动将依旧保留;即使在一般的系统操作中,当希望尽快把信息刷新到磁盘时,也是如此。如果不愿作这种持久性修改 的话,另一个选择是运行一个非 klogd 程序(如前面介绍的cat /proc/kmesg),但这样并不能为通常的系统操作提供一个合适的环境。

多数情况中,获取相关信息的最好方法是在需要的时候才去查询系统信息,而不是持续不断地产生数据。实际上,每个 Unix 系统都提供了很多工具,用于获取系统信息,如:ps、netstat、vmstat等等。

驱动程序开发人员对系统进行查询时,可以采用两种主要的技术:在 /proc 文件系统中创建文件,或者使用驱动程序的 ioctl 方法。/proc 方式的另一个选择是使用 devfs,不过

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值