linux内核调试方法

来源:互联网 发布:淘宝为什么让马云有钱 编辑:程序博客网 时间:2024/06/01 09:28

内核开发比用户空间开发更难的一个因素就是内核调试艰难。内核错误往往会导致系统宕机,很难保留出错时的现场。调试内核的关键在于你的对内核的深刻理解。


一  调试前的准备

在调试一个bug之前,我们所要做的准备工作有:

  • 有一个被确认的bug。

  • 包含这个bug的内核版本号,需要分析出这个bug在哪一个版本被引入,这个对于解决问题有极大的帮助。可以采用二分查找法来逐步锁定bug引入版本号。

  • 对内核代码理解越深刻越好,同时还需要一点点运气。

  • 该bug可以复现。如果能够找到复现规律,那么离找到问题的原因就不远了。

  • 最小化系统。把可能产生bug的因素逐一排除掉。

二  内核中的bug

内核中的bug也是多种多样的。它们的产生有无数的原因,同时表象也变化多端。从隐藏在源代码中的错误到展现在目击者面前的bug,其发作往往是一系列连锁反应的事件才可能出发的。虽然内核调试有一定的困难,但是通过你的努力和理解,说不定你会喜欢上这样的挑战。

三  内核调试配置选项

学习编写驱动程序要构建安装自己的内核(标准主线内核)。最重要的原因之一是:内核开发者已经建立了多项用于调试的功能。但是由于这些功能会造成额外的输出,并导致能下降,因此发行版厂商通常会禁止发行版内核中的调试功能。

1  内核配置

为了实现内核调试,在内核配置上增加了几项:

 Kernel hacking  --->      [*]   Magic SysRq key [*]   Kernel debugging [*]   Debug slab memory allocations   [*]   Spinlock and rw-lock debuggingbasic checks [*]   Spinlock debuggingsleep-inside-spinlock checking            [*]   Compile the kernel with debug info   Device Drivers  --->              Generic Driver Options  --->            [*]   Driver Core verbose debug messages General setup  --->            [*]   Configure standard kernel features (for small systems)  --->            [*]   Load all symbols for debugging/ksymoops

启用选项例如:

slab layer debugging(slab层调试选项) high-memory debugging(高端内存调试选项) I/O mapping debugging(I/O映射调试选项) spin-lock debugging(自旋锁调试选项) stack-overflow checking(栈溢出检查选项) sleep-inside-spinlock checking(自旋锁内睡眠选项)


2  调试原子操作

从内核2.5开发,为了检查各类由原子操作引发的问题,内核提供了极佳的工具。
内核提供了一个原子操作计数器,它可以配置成,一旦在原子操作过程中,进城进入睡眠或者做了一些可能引起睡眠的操作,就打印警告信息并提供追踪线索。
所以,包括在使用锁的时候调用schedule(),正使用锁的时候以阻塞方式请求分配内存等,各种潜在的bug都能够被探测到。
下面这些选项可以最大限度地利用该特性:

CONFIG_PREEMPT = y CONFIG_DEBUG_KERNEL = y CONFIG_KLLSYMS = y CONFIG_SPINLOCK_SLEEP = y


四  引发bug并打印信息

1  BUG()和BUG_ON()

一些内核调用可以用来方便标记bug,提供断言并输出信息。最常用的两个是BUG()和BUG_ON()。


定义在<include/asm-generic>中:

#ifndef HAVE_ARCH_BUG #define BUG() do {    printk("BUG: failure at %s:%d/%s()! "__FILE__, __LINE__, __FUNCTION__);    panic("BUG!");   /* 引发更严重的错误,不但打印错误消息,而且整个系统业会挂起 */ } while (0#endif #ifndef HAVE_ARCH_BUG_ON    #define BUG_ON(condition) do { if (unlikely(condition)) BUG(); } while(0) #endif

当调用这两个宏的时候,它们会引发OOPS,导致栈的回溯和错误消息的打印。
※ 可以把这两个调用当作断言使用,如:BUG_ON(bad_thing);

2  dump_stack()

有些时候,只需要在终端上打印一下栈的回溯信息来帮助你调试。这时可以使用dump_stack()。这个函数只在终端上打印寄存器上下文和函数的跟踪线索。

   if (!debug_check) {        printk(KERN_DEBUG “provide some information…/n”);        dump_stack();    }


五  printk()

内核提供的格式化打印函数。

1  printk函数的健壮性

      健壮性是printk最容易被接受的一个特质,几乎在任何地方,任何时候内核都可以调用它(中断上下文、进程上下文、持有锁时、多处理器处理时等)。


2  printk函数脆弱之处

      在系统启动过程中,终端初始化之前,在某些地方是不能调用的。如果真的需要调试系统启动过程最开始的地方,有以下方法可以使用:

  • 使用串口调试,将调试信息输出到其他终端设备。

  • 使用early_printk(),该函数在系统启动初期就有打印能力。但它只支持部分硬件体系。

3  LOG等级

       printk和printf一个主要的区别就是前者可以指定一个LOG等级。内核根据这个等级来判断是否在终端上打印消息。内核把比指定等级高的所有消息显示在终端。
       可以使用下面的方式指定一个LOG级别:
printk(KERN_CRIT  “Hello, world!\n”);
注意,第一个参数并不一个真正的参数,因为其中没有用于分隔级别(KERN_CRIT)和格式字符的逗号(,)。KERN_CRIT本身只是一个普通的字符串(事实上,它表示的是字符串 "<2>";表 1 列出了完整的日志级别清单)。作为预处理程序的一部分,C 会自动地使用一个名为 字符串串联 的功能将这两个字符串组合在一起。组合的结果是将日志级别和用户指定的格式字符串包含在一个字符串中。

内核使用这个指定LOG级别与当前终端LOG等级console_loglevel来决定是不是向终端打印。
下面是可使用的LOG等级:

#define KERN_EMERG      "<0>"   /* system is unusable                            */#define KERN_ALERT        "<1>"   /* action must be taken immediately     */ #define KERN_CRIT           "<2>"   /* critical conditions                                */#define KERN_ERR            "<3>"   /* error conditions                                   */#define KERN_WARNING  "<4>"   /* warning conditions                              */#define KERN_NOTICE       "<5>"   /* normal but significant condition         */#define KERN_INFO            "<6>"   /* informational                                       */#define KERN_DEBUG        "<7>"   /* debug-level messages                       */#define KERN_DEFAULT     "<d>"   /* Use the default kernel loglevel           */

注意,如果调用者未将日志级别提供给 printk,那么系统就会使用默认值 KERN_WARNING "<4>"(表示只有KERN_WARNING 级别以上的日志消息会被记录)。由于默认值存在变化,所以在使用时最好指定LOG级别。有LOG级别的一个好处就是我们可以选择性的输出LOG。比如平时我们只需要打印KERN_WARNING级别以上的关键性LOG,但是调试的时候,我们可以选择打印KERN_DEBUG等以上的详细LOG。而这些都不需要我们修改代码,只需要通过命令修改默认日志输出级别:

mtj@ubuntu :~$ cat /proc/sys/kernel/printk4 4 1 7mtj@ubuntu :~$ cat /proc/sys/kernel/printk_delay0mtj@ubuntu :~$ cat /proc/sys/kernel/printk_ratelimit5mtj@ubuntu :~$ cat /proc/sys/kernel/printk_ratelimit_burst10

第一项定义了 printk API 当前使用的日志级别。这些日志级别表示了控制台的日志级别、默认消息日志级别、最小控制台日志级别和默认控制台日志级别。printk_delay 值表示的是 printk 消息之间的延迟毫秒数(用于提高某些场景的可读性)。注意,这里它的值为 0,而它是不可以通过 /proc 设置的。printk_ratelimit 定义了消息之间允许的最小时间间隔(当前定义为每 5 秒内的某个内核消息数)。消息数量是由 printk_ratelimit_burst 定义的(当前定义为 10)。如果您拥有一个非正式内核而又使用有带宽限制的控制台设备(如通过串口), 那么这非常有用。注意,在内核中,速度限制是由调用者控制的,而不是在printk 中实现的。如果一个 printk 用户要求进行速度限制,那么该用户就需要调用printk_ratelimit 函数。

4  记录缓冲区

  内核消息都被保存在一个LOG_BUF_LEN大小的环形队列中。
  关于LOG_BUF_LEN定义:

 #define __LOG_BUF_LEN (1 << CONFIG_LOG_BUF_SHIFT)
  ※ 变量CONFIG_LOG_BUF_SHIFT在内核编译时由配置文件定义,对于i386平台,其值定义如下(在linux26/arch/i386/defconfig中):
CONFIG_LOG_BUF_SHIFT=18

  记录缓冲区操作:
  ① 消息被读出到用户空间时,此消息就会从环形队列中删除。
  ② 当消息缓冲区满时,如果再有printk()调用时,新消息将覆盖队列中的老消息。
  ③ 在读写环形队列时,同步问题很容易得到解决。

  ※ 这个纪录缓冲区之所以称为环形,是因为它的读写都是按照环形队列的方式进行操作的。

5  syslogd/klogd


6  dmesg

dmesg 命令也可用于打印和控制内核环缓冲区。这个命令使用 klogctl 系统调用来读取内核环缓冲区,并将它转发到标准输出(stdout)。这个命令也可以用来清除内核环缓冲区(使用 -c 选项),设置控制台日志级别(-n 选项),以及定义用于读取内核日志消息的缓冲区大小(-s 选项)。注意,如果没有指定缓冲区大小,那么 dmesg 会使用 klogctl 的SYSLOG_ACTION_SIZE_BUFFER 操作确定缓冲区大小。

7 注意 

a) 虽然printk很健壮,但是看了源码你就知道,这个函数的效率很低:做字符拷贝时一次只拷贝一个字节,且去调用console输出可能还产生中断。所以如果你的驱动在功能调试完成以后做性能测试或者发布的时候千万记得尽量减少printk输出,做到仅在出错时输出少量信息。否则往console输出无用信息影响性能。
b) printk的临时缓存printk_buf只有1K,所有一次printk函数只能记录<1K的信息到log buffer,并且printk使用的“ringbuffer”.


2  kallsyms

开发版2.5内核引入了kallsyms特性,它可以通过定义CONFIG_KALLSYMS编译选项启用。该选项可以载入内核镜像所对应的内存地址的符号名称(即函数名),所以内核可以打印解码之后的跟踪线索。相应,解码OOPS也不再需要System.map和ksymoops工具了。另外,
这样做,会使内核变大些,因为地址对应符号名称必须始终驻留在内核所在内存上。

#cat /proc/kallsyms c0100240   T    _stext c0100240   t    run_init_process c0100240   T      stext c0100269   t    init     …
https://my.oschina.net/fgq611/blog/113249




0 0
原创粉丝点击