目录[-]
内核开发比用户空间开发更难的一个因素就是内核调试艰难。内核错误往往会导致系统宕机,很难保留出错时的现场。调试内核的关键在于你的对内核的深刻理解。
在调试一个bug之前,我们所要做的准备工作有:
有一个被确认的bug。
包含这个bug的内核版本号,需要分析出这个bug在哪一个版本被引入,这个对于解决问题有极大的帮助。可以采用二分查找法来逐步锁定bug引入版本号。
对内核代码理解越深刻越好,同时还需要一点点运气。
该bug可以复现。如果能够找到复现规律,那么离找到问题的原因就不远了。
最小化系统。把可能产生bug的因素逐一排除掉。
内核中的bug也是多种多样的。它们的产生有无数的原因,同时表象也变化多端。从隐藏在源代码中的错误到展现在目击者面前的bug,其发作往往是一系列连锁反应的事件才可能出发的。虽然内核调试有一定的困难,但是通过你的努力和理解,说不定你会喜欢上这样的挑战。
学习编写驱动程序要构建安装自己的内核(标准主线内核)。最重要的原因之一是:内核开发者已经建立了多项用于调试的功能。但是由于这些功能会造成额外的输出,并导致能下降,因此发行版厂商通常会禁止发行版内核中的调试功能。
为了实现内核调试,在内核配置上增加了几项:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
Kernel hacking --->
[*] Magic SysRq key
[*] Kernel debugging
[*] Debug slab memory allocations
[*] Spinlock and rw-lock debugging: basic checks
[*] Spinlock debugging: sleep -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
|
启用选项例如:
1
2
3
4
5
6
|
slab layer debugging(slab层调试选项)
high-memory debugging(高端内存调试选项)
I /O mapping debugging(I /O 映射调试选项)
spin-lock debugging(自旋锁调试选项)
stack-overflow checking(栈溢出检查选项)
sleep -inside-spinlock checking(自旋锁内睡眠选项)
|
从内核2.5开发,为了检查各类由原子操作引发的问题,内核提供了极佳的工具。
内核提供了一个原子操作计数器,它可以配置成,一旦在原子操作过程中,进城进入睡眠或者做了一些可能引起睡眠的操作,就打印警告信息并提供追踪线索。
所以,包括在使用锁的时候调用schedule(),正使用锁的时候以阻塞方式请求分配内存等,各种潜在的bug都能够被探测到。
下面这些选项可以最大限度地利用该特性:
1
2
3
4
|
CONFIG_PREEMPT = y
CONFIG_DEBUG_KERNEL = y
CONFIG_KLLSYMS = y
CONFIG_SPINLOCK_SLEEP = y
|
一些内核调用可以用来方便标记bug,提供断言并输出信息。最常用的两个是BUG()和BUG_ON()。
定义在
1
2
3
4
5
6
7
8
9
10
|
#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);
有些时候,只需要在终端上打印一下栈的回溯信息来帮助你调试。这时可以使用dump_stack()。这个函数只在终端上打印寄存器上下文和函数的跟踪线索。
1
2
3
4
|
if (!debug_check) {
printk(KERN_DEBUG “provide some information…/n”);
dump_stack();
}
|
内核提供的格式化打印函数。
健壮性是printk最容易被接受的一个特质,几乎在任何地方,任何时候内核都可以调用它(中断上下文、进程上下文、持有锁时、多处理器处理时等)。
在系统启动过程中,终端初始化之前,在某些地方是不能调用的。如果真的需要调试系统启动过程最开始的地方,有以下方法可以使用:
使用串口调试,将调试信息输出到其他终端设备。
使用early_printk(),该函数在系统启动初期就有打印能力。但它只支持部分硬件体系。
printk和printf一个主要的区别就是前者可以指定一个LOG等级。内核根据这个等级来判断是否在终端上打印消息。内核把比指定等级高的所有消息显示在终端。
可以使用下面的方式指定一个LOG级别:
printk(KERN_CRIT “Hello, world!\n”);
注意,第一个参数并不一个真正的参数,因为其中没有用于分隔级别(KERN_CRIT)和格式字符的逗号(,)。KERN_CRIT本身只是一个普通的字符串(事实上,它表示的是字符串 "<2>";表 1 列出了完整的日志级别清单)。作为预处理程序的一部分,C 会自动地使用一个名为 字符串串联 的功能将这两个字符串组合在一起。组合的结果是将日志级别和用户指定的格式字符串包含在一个字符串中。
内核使用这个指定LOG级别与当前终端LOG等级console_loglevel来决定是不是向终端打印。
下面是可使用的LOG等级:
1
2
3
4
5
6
7
8
9
|
#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 "
|
注意,如果调用者未将日志级别提供给 printk,那么系统就会使用默认值 KERN_WARNING "<4>"(表示只有KERN_WARNING 级别以上的日志消息会被记录)。由于默认值存在变化,所以在使用时最好指定LOG级别。有LOG级别的一个好处就是我们可以选择性的输出LOG。比如平时我们只需要打印KERN_WARNING级别以上的关键性LOG,但是调试的时候,我们可以选择打印KERN_DEBUG等以上的详细LOG。而这些都不需要我们修改代码,只需要通过命令修改默认日志输出级别:
1
2
3
4
5
6
7
8
|
mtj@ubuntu :~$ cat /proc/sys/kernel/printk
4 4 1 7
mtj@ubuntu :~$ cat /proc/sys/kernel/printk_delay
0
mtj@ubuntu :~$ cat /proc/sys/kernel/printk_ratelimit
5
mtj@ubuntu :~$ cat /proc/sys/kernel/printk_ratelimit_burst
10
|
第一项定义了 printk API 当前使用的日志级别。这些日志级别表示了控制台的日志级别、默认消息日志级别、最小控制台日志级别和默认控制台日志级别。printk_delay 值表示的是 printk 消息之间的延迟毫秒数(用于提高某些场景的可读性)。注意,这里它的值为 0,而它是不可以通过 /proc 设置的。printk_ratelimit 定义了消息之间允许的最小时间间隔(当前定义为每 5 秒内的某个内核消息数)。消息数量是由 printk_ratelimit_burst 定义的(当前定义为 10)。如果您拥有一个非正式内核而又使用有带宽限制的控制台设备(如通过串口), 那么这非常有用。注意,在内核中,速度限制是由调用者控制的,而不是在printk 中实现的。如果一个 printk 用户要求进行速度限制,那么该用户就需要调用printk_ratelimit 函数。
内核消息都被保存在一个LOG_BUF_LEN大小的环形队列中。
关于LOG_BUF_LEN定义:
1
|
#define __LOG_BUF_LEN (1 << CONFIG_LOG_BUF_SHIFT)
|
1
|
CONFIG_LOG_BUF_SHIFT=18
|
记录缓冲区操作:
① 消息被读出到用户空间时,此消息就会从环形队列中删除。
② 当消息缓冲区满时,如果再有printk()调用时,新消息将覆盖队列中的老消息。
③ 在读写环形队列时,同步问题很容易得到解决。
※ 这个纪录缓冲区之所以称为环形,是因为它的读写都是按照环形队列的方式进行操作的。
在标准的Linux系统上,用户空间的守护进程klogd从纪录缓冲区中获取内核消息,再通过syslogd守护进程把这些消息保存在系统日志文件中。klogd进程既可以从/proc/kmsg文件中,也可以通过syslog()系统调用读取这些消息。默认情况下,它选择读取/proc方式实现。klogd守护进程在消息缓冲区有新的消息之前,一直处于阻塞状态。一旦有新的内核消息,klogd被唤醒,读出内核消息并进行处理。默认情况下,处理例程就是把内核消息传给syslogd守护进程。syslogd守护进程一般把接收到的消息写入/var/log/messages文件中。不过,还是可以通过/etc/syslog.conf文件来进行配置,可以选择其他的输出文件。
dmesg 命令也可用于打印和控制内核环缓冲区。这个命令使用 klogctl 系统调用来读取内核环缓冲区,并将它转发到标准输出(stdout)。这个命令也可以用来清除内核环缓冲区(使用 -c 选项),设置控制台日志级别(-n 选项),以及定义用于读取内核日志消息的缓冲区大小(-s 选项)。注意,如果没有指定缓冲区大小,那么 dmesg 会使用 klogctl 的SYSLOG_ACTION_SIZE_BUFFER 操作确定缓冲区大小。
a) 虽然printk很健壮,但是看了源码你就知道,这个函数的效率很低:做字符拷贝时一次只拷贝一个字节,且去调用console输出可能还产生中断。所以如果你的驱动在功能调试完成以后做性能测试或者发布的时候千万记得尽量减少printk输出,做到仅在出错时输出少量信息。否则往console输出无用信息影响性能。
b) printk的临时缓存printk_buf只有1K,所有一次printk函数只能记录<1K的信息到log buffer,并且printk使用的“ringbuffer”.
动态调试是通过动态的开启和禁止某些内核代码来获取额外的内核信息。
首先内核选项CONFIG_DYNAMIC_DEBUG应该被设置。所有通过pr_debug()/dev_debug()打印的信息都可以动态的显示或不显示。
可以通过简单的查询语句来筛选需要显示的信息。
-源文件名
-函数名
-行号(包括指定范围的行号)
-模块名
-格式化字符串
将要打印信息的格式写入
1
|
nullarbor:~ # echo 'file svcsock.c line 1603 +p' >
|
参考:
1 内核日志及printk结构浅析 -- Tekkaman Ninja
2 内核日志:API 及实现
3 printk实现分析
4 dynamic-debug-howto.txt
MEMWATCH 由 Johan Lindh 编写,是一个开放源代码 C 语言内存错误检测工具,您可以自己下载它。只要在代码中添加一个头文件并在 gcc 语句中定义了 MEMWATCH 之后,您就可以跟踪程序中的内存泄漏和错误了。MEMWATCH 支持ANSIC,它提供结果日志纪录,能检测双重释放(double-free)、错误释放(erroneous free)、没有释放的内存(unfreedmemory)、溢出和下溢等等。
清单 1. 内存样本(test1.c)
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#include
#include
#include "memwatch.h"
int main( void )
{
char *ptr1;
char *ptr2;
ptr1 = malloc (512);
ptr2 = malloc (512);
ptr2 = ptr1;
free (ptr2);
free (ptr1);
}
|
清单 1 中的代码将分配两个 512 字节的内存块,然后指向第一个内存块的指针被设定为指向第二个内存块。结果,第二个内存块的地址丢失,从而产生了内存泄漏。
现在我们编译清单 1 的 memwatch.c。下面是一个 makefile 示例:
test1
1
2
|
gcc -DMEMWATCH -DMW_STDIO test1.c memwatch
c -o test1
|
当您运行 test1 程序后,它会生成一个关于泄漏的内存的报告。清单 2 展示了示例 memwatch.log 输出文件。
清单 2. test1 memwatch.log 文件
1
2
3
4
5
6
7
8
9
10
11
|
MEMWATCH 2.67 Copyright (C) 1992-1999 Johan Lindh
...
double - free : <4> test1.c(15), 0x80517b4 was freed from test1.c(14)
...
unfreed: <2> test1.c(11), 512 bytes at 0x80519e4
{FE FE FE FE FE FE FE FE FE FE FE FE ..............}
Memory usage statistics (global):
N)umber of allocations made: 2
L)argest memory usage : 1024
T)otal of all alloc() calls: 1024
U)nfreed bytes totals : 512
|
MEMWATCH 为您显示真正导致问题的行。如果您释放一个已经释放过的指针,它会告诉您。对于没有释放的内存也一样。日志结尾部分显示统计信息,包括泄漏了多少内存,使用了多少内存,以及总共分配了多少内存。
YAMD 软件包由 Nate Eldredge 编写,可以查找 C 和 C++ 中动态的、与内存分配有关的问题。在撰写本文时,YAMD 的最新版本为 0.32。请下载 yamd-0.32.tar.gz。执行 make 命令来构建程序;然后执行 make install 命令安装程序并设置工具。
一旦您下载了 YAMD 之后,请在 test1.c 上使用它。请删除 #include memwatch.h 并对 makefile 进行如下小小的修改:
使用 YAMD 的 test1
1
|
gcc -g test1.c -o test1
|
清单 3 展示了来自 test1 上的 YAMD 的输出。
清单 3. 使用 YAMD 的 test1 输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
YAMD version 0.32
Executable: /usr/src/test/yamd-0 .32 /test1
...
INFO: Normal allocation of this block
Address 0x40025e00, size 512
...
INFO: Normal allocation of this block
Address 0x40028e00, size 512
...
INFO: Normal deallocation of this block
Address 0x40025e00, size 512
...
ERROR: Multiple freeing At
free of pointer already freed
Address 0x40025e00, size 512
...
WARNING: Memory leak
Address 0x40028e00, size 512
WARNING: Total memory leaks:
1 unfreed allocations totaling 512 bytes
*** Finished at Tue ... 10:07:15 2002
Allocated a grand total of 1024 bytes 2 allocations
Average of 512 bytes per allocation
Max bytes allocated at one time : 1024
24 K alloced internally / 12 K mapped now / 8 K max
Virtual program size is 1416 K
End.
|
YAMD 显示我们已经释放了内存,而且存在内存泄漏。让我们在清单 4 中另一个样本程序上试试 YAMD。
清单 4. 内存代码(test2.c)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
#include
#include
int main( void )
{
char *ptr1;
char *ptr2;
char *chptr;
int i = 1;
ptr1 = malloc (512);
ptr2 = malloc (512);
chptr = ( char *) malloc (512);
for (i; i <= 512; i++) {
chptr[i] = 'S' ;
}
ptr2 = ptr1;
free (ptr2);
free (ptr1);
free (chptr);
}
|
1
|
. /run-yamd /usr/src/test/test2/test2
|
清单 5 显示了在样本程序 test2 上使用 YAMD 得到的输出。YAMD 告诉我们在 for 循环中有“越界(out-of-bounds)”的情况。
清单 5. 使用 YAMD 的 test2 输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
Running /usr/src/test/test2/test2
Temp output to /tmp/yamd-out .1243
*********
. /run-yamd : line 101: 1248 Segmentation fault (core dumped)
YAMD version 0.32
Starting run: /usr/src/test/test2/test2
Executable: /usr/src/test/test2/test2
Virtual program size is 1380 K
...
INFO: Normal allocation of this block
Address 0x40025e00, size 512
...
INFO: Normal allocation of this block
Address 0x40028e00, size 512
...
INFO: Normal allocation of this block
Address 0x4002be00, size 512
ERROR: Crash
...
Tried to write address 0x4002c000
Seems to be part of this block:
Address 0x4002be00, size 512
...
Address in question is at offset 512 (out of bounds)
Will dump core after checking heap.
Done.
|
MEMWATCH 和 YAMD 都是很有用的调试工具,它们的使用方法有所不同。对于 MEMWATCH,您需要添加包含文件memwatch.h 并打开两个编译时间标记。对于链接(link)语句,YAMD 只需要 -g 选项。
多数 Linux 分发版包含一个 Electric Fence 包,不过您也可以选择下载它。Electric Fence 是一个由 Bruce Perens 编写的malloc()调试库。它就在您分配内存后分配受保护的内存。如果存在 fencepost 错误(超过数组末尾运行),程序就会产生保护错误,并立即结束。通过结合 Electric Fence 和 gdb,您可以精确地跟踪到哪一行试图访问受保护内存。ElectricFence 的另一个功能就是能够检测内存泄漏。
strace 命令是一种强大的工具,它能够显示所有由用户空间程序发出的系统调用。strace 显示这些调用的参数并返回符号形式的值。strace 从内核接收信息,而且不需要以任何特殊的方式来构建内核。将跟踪信息发送到应用程序及内核开发者都很有用。在清单 6 中,分区的一种格式有错误,清单显示了 strace 的开头部分,内容是关于调出创建文件系统操作(mkfs )的。strace 确定哪个调用导致问题出现。
清单 6. mkfs 上 strace 的开头部分
1
2
3
4
5
6
7
8
9
10
11
12
13
|
execve( "/sbin/mkfs.jfs" , [ "mkfs.jfs" , "-f" , "/dev/test1" ], &
...
open ( "/dev/test1" , O_RDWR|O_LARGEFILE) = 4
stat64( "/dev/test1" , {st_mode=&, st_rdev=makedev(63, 255), ...}) = 0
ioctl(4, 0x40041271, 0xbfffe128) = -1 EINVAL (Invalid argument)
write(2, "mkfs.jfs: warning - cannot setb" ..., 98mkfs.jfs: warning -
cannot set blocksize on block device /dev/test1 : Invalid argument )
= 98
stat64( "/dev/test1" , {st_mode=&, st_rdev=makedev(63, 255), ...}) = 0
open ( "/dev/test1" , O_RDONLY|O_LARGEFILE) = 5
ioctl(5, 0x80041272, 0xbfffe124) = -1 EINVAL (Invalid argument)
write(2, "mkfs.jfs: can\'t determine device" ..., ..._exit(1)
= ?
|
清单 6 显示 ioctl 调用导致用来格式化分区的 mkfs 程序失败。 ioctl BLKGETSIZE64 失败。( BLKGET-SIZE64 在调用 ioctl的源代码中定义。) BLKGETSIZE64 ioctl 将被添加到 Linux 中所有的设备,而在这里,逻辑卷管理器还不支持它。因此,如果BLKGETSIZE64 ioctl 调用失败,mkfs 代码将改为调用较早的 ioctl 调用;这使得 mkfs 适用于逻辑卷管理器。
参考:
http://www.ibm.com/developerworks/cn/linux/sdk/l-debug/index.html#resources
OOPS(也称 Panic)消息包含系统错误的细节,如 CPU 寄存器的内容等。是内核告知用户有不幸发生的最常用的方式。
内核只能发布OOPS,这个过程包括向终端上输出错误消息,输出寄存器保存的信息,并输出可供跟踪的回溯线索。通常,发送完OOPS之后,内核会处于一种不稳定的状态。
OOPS的产生有很多可能原因,其中包括内存访问越界或非法的指令等。
※ 作为内核的开发者,必定将会经常处理OOPS。
※ OOPS中包含的重要信息,对所有体系结构的机器都是完全相同的:寄存器上下文和回溯线索(回溯线索显示了导致错误发生的函数调用链)。
在 Linux 中,调试系统崩溃的传统方法是分析在发生崩溃时发送到系统控制台的 Oops 消息。一旦您掌握了细节,就可以将消息发送到 ksymoops 实用程序,它将试图将代码转换为指令并将堆栈值映射到内核符号。
※ 如:回溯线索中的地址,会通过ksymoops转化成名称可见的函数名。
ksymoops需要几项内容:Oops 消息输出、来自正在运行的内核的 System.map 文件,还有 /proc/ksyms、vmlinux和/proc/modules。
关于如何使用 ksymoops,内核源代码 /usr/src/linux/Documentation/oops-tracing.txt 中或 ksymoops 手册页上有完整的说明可以参考。Ksymoops 反汇编代码部分,指出发生错误的指令,并显示一个跟踪部分表明代码如何被调用。
首先,将 Oops 消息保存在一个文件中以便通过 ksymoops 实用程序运行它。清单 7 显示了由安装 JFS 文件系统的 mount命令创建的 Oops 消息。
清单 7. ksymoops 处理后的 Oops 消息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
ksymoops 2.4.0 on i686 2.4.17. Options used
... 15:59:37 sfb1 kernel: Unable to handle kernel NULL pointer dereference at
virtual address 0000000
... 15:59:37 sfb1 kernel: c01588fc
... 15:59:37 sfb1 kernel: *pde = 0000000
... 15:59:37 sfb1 kernel: Oops: 0000
... 15:59:37 sfb1 kernel: CPU: 0
... 15:59:37 sfb1 kernel: EIP: 0010:[jfs_mount+60 /704 ]
... 15:59:37 sfb1 kernel: Call Trace: [jfs_read_super+287 /688 ]
[get_sb_bdev+563 /736 ] [do_kern_mount+189 /336 ] [do_add_mount+35 /208 ]
[do_page_fault+0 /1264 ]
... 15:59:37 sfb1 kernel: Call Trace: [
... 15:59:37 sfb1 kernel: [ ... 15:59:37 sfb1 kernel: Code: 8b 2d 00 00 00 00 55 ...
>>EIP; c01588fc ...
Trace; c0106cf3 Code; c01588fc 00000000 <_EIP>:
Code; c01588fc 0: 8b 2d 00 00 00 00 mov 0x0,%ebp <=====
Code; c0158902 6: 55 push %ebp
|
接下来,您要确定 jfs_mount 中的哪一行代码引起了这个问题。Oops 消息告诉我们问题是由位于偏移地址 3c 的指令引起的。做这件事的办法之一是对 jfs_mount.o 文件使用 objdump 实用程序,然后查看偏移地址 3c。Objdump 用来反汇编模块函数,看看您的 C 源代码会产生什么汇编指令。清单 8 显示了使用 objdump 后您将看到的内容,接着,我们查看jfs_mount 的 C 代码,可以看到空值是第 109 行引起的。偏移地址 3c 之所以很重要,是因为 Oops 消息将该处标识为引起问题的位置。
清单 8. jfs_mount 的汇编程序清单
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
109 printk( "%d\n" ,*ptr);
objdump jfs_mount.o
jfs_mount.o: file format elf32-i386
Disassembly of section .text:
00000000
0:55 push %ebp
...
2c: e8 cf 03 00 00 call 400
31: 89 c3 mov %eax,%ebx
33: 58 pop %eax
34: 85 db test %ebx,%ebx
36: 0f 85 55 02 00 00 jne 291
3c: 8b 2d 00 00 00 00 mov 0x0,%ebp << problem line above
42: 55 push %ebp
|
开发版2.5内核引入了kallsyms特性,它可以通过定义CONFIG_KALLSYMS编译选项启用。该选项可以载入内核镜像所对应的内存地址的符号名称(即函数名),所以内核可以打印解码之后的跟踪线索。相应,解码OOPS也不再需要System.map和ksymoops工具了。另外,
这样做,会使内核变大些,因为地址对应符号名称必须始终驻留在内核所在内存上。
1
2
3
4
5
6
|
#cat /proc/kallsyms
c0100240 T _stext
c0100240 t run_init_process
c0100240 T stext
c0100269 t init
…
|
3.1 Kdump 的基本概念
3.1.1 什么是 kexec ?
Kexec 是实现 kdump 机制的关键,它包括 2 个组成部分:一是内核空间的系统调用 kexec_load,负责在生产内核(production kernel 或 first kernel)启动时将捕获内核(capture kernel 或 sencond kernel)加载到指定地址。二是用户空间的工具 kexec-tools,他将捕获内核的地址传递给生产内核,从而在系统崩溃的时候能够找到捕获内核的地址并运行。没有 kexec 就没有 kdump。先有 kexec 实现了在一个内核中可以启动另一个内核,才让 kdump 有了用武之地。kexec 原来的目的是为了节省 kernel 开发人员重启系统的时间,谁能想到这个“偷懒”的技术却孕育了最成功的内存转存机制呢?
3.1.2 什么是 kdump ?
Kdump 的概念出现在 2005 左右,是迄今为止最可靠的内核转存机制,已经被主要的 linux™ 厂商选用。kdump是一种先进的基于 kexec 的内核崩溃转储机制。当系统崩溃时,kdump 使用 kexec 启动到第二个内核。第二个内核通常叫做捕获内核,以很小内存启动以捕获转储镜像。第一个内核保留了内存的一部分给第二内核启动用。由于 kdump 利用 kexec 启动捕获内核,绕过了 BIOS,所以第一个内核的内存得以保留。这是内核崩溃转储的本质。
kdump 需要两个不同目的的内核,生产内核和捕获内核。生产内核是捕获内核服务的对像。捕获内核会在生产内核崩溃时启动起来,与相应的 ramdisk 一起组建一个微环境,用以对生产内核下的内存进行收集和转存。
3.1.3 如何使用 kdump
构建系统和 dump-capture 内核,此操作有 2 种方式可选:
1)构建一个单独的自定义转储捕获内核以捕获内核转储;
2) 或者将系统内核本身作为转储捕获内核,这就不需要构建一个单独的转储捕获内核。
方法(2)只能用于可支持可重定位内核的体系结构上;目前 i386,x86_64,ppc64 和 ia64 体系结构支持可重定位内核。构建一个可重定位内核使得不需要构建第二个内核就可以捕获转储。但是可能有时想构建一个自定义转储捕获内核以满足特定要求。
3.1.4 如何访问捕获内存
在内核崩溃之前所有关于核心映像的必要信息都用 ELF 格式编码并存储在保留的内存区域中。ELF 头所在的物理地址被作为命令行参数(fcorehdr=)传递给新启动的转储内核。
在 i386 体系结构上,启动的时候需要使用物理内存开始的 640K,而不管操作系统内核转载在何处。因此,这个640K 的区域在重新启动第二个内核的时候由 kexec 备份。
在第二个内核中,“前一个系统的内存”可以通过两种方式访问:
1) 通过 /dev/oldmem 这个设备接口。
一个“捕捉”设备可以使用“raw”(裸的)方式 “读”这个设备文件并写出到文件。这是关于内存的 “裸”的数据转储,同时这些分析 / 捕捉工具应该足够“智能”从而可以知道从哪里可以得到正确的信息。ELF 文件头(通过命令行参数传递过来的 elfcorehdr)可能会有帮助。
2) 通过 /proc/vmcore。
这个方式是将转储输出为一个 ELF 格式的文件,并且可以使用一些文件拷贝命令(比如 cp,scp 等)将信息读出来。同时,gdb 可以在得到的转储文件上做一些调试(有限的)。这种方式保证了内存中的页面都以正确的途径被保存 ( 注意内存开始的 640K 被重新映射了 )。
3.1.5 kdump 的优势
1) 高可靠性
崩溃转储数据可从一个新启动内核的上下文中获取,而不是从已经崩溃内核的上下文。
2) 多版本支持
LKCD(Linux Kernel Crash Dump),netdump,diskdump 已被纳入 LDPs(Linux Documen-tation Project) 内核。SUSE 和 RedHat 都对 kdump 有技术支持。
3.2 Kdump 实现流程
图 1. RHEL6.2 执行流程
图 2. sles11 执行流程
3.3 配置 kdump
3.3.1 安装软件包和实用程序
Kdump 用到的各种工具都在 kexec-tools 中。kernel-debuginfo 则是用来分析 vmcore 文件。从 rhel5 开始,kexec-tools 已被默认安装在发行版。而 novell 也在 sles10 发行版中把 kdump 集成进来。所以如果使用的是rhel5 和 sles10 之后的发行版,那就省去了安装 kexec-tools 的步骤。而如果需要调试 kdump 生成的 vmcore文件,则需要手动安装 kernel-debuginfo 包。检查安装包操作:
3.3.2 参数相关设置 uli13lp1:/ # rpm -qa|grep kexec kexec-tools-2.0.0-53.43.10 uli13lp1:/ # rpm -qa 'kernel*debuginfo*' kernel-default-debuginfo-3.0.13-0.27.1 kernel-ppc64-debuginfo-3.0.13-0.27.1
系统内核设置选项和转储捕获内核配置选择在《使用 Crash 工具分析 Linux dump 文件》一文中已有说明,在此不再赘述。仅列出内核引导参数设置以及配置文件设置。
1) 修改内核引导参数,为启动捕获内核预留内存
通过下面的方法来配置 kdump 使用的内存大小。添加启动参数"crashkernel=Y@X",这里,Y 是为 kdump 捕捉内核保留的内存,X 是保留部分内存的开始位置。
对于 i386 和 x86_64, 编辑 /etc/grub.conf, 在内核行的最后添加"crashkernel=128M" 。
对于 ppc64,在 /etc/yaboot.conf 最后添加"crashkernel=128M"。
在 ia64, 编辑 /etc/elilo.conf,添加"crashkernel=256M"到内核行。
2) kdump 配置文件
kdump 的配置文件是 /etc/kdump.conf(RHEL6.2);/etc/sysconfig/kdump(SLES11 sp2)。每个文件头部都有选项说明,可以根据使用需求设置相应的选项。
3.3.3 启动 kdump 服务
在设置了预留内存后,需要重启机器,否则 kdump 是不可使用的。启动 kdump 服务:
Rhel6.2:
# chkconfig kdump on # service kdump status Kdump is operational # service kdump start
SLES11SP2:
# chkconfig boot.kdump on # service boot.kdump start
3.3.4 测试配置是否有效
可以通过 kexec 加载内核镜像,让系统准备好去捕获一个崩溃时产生的 vmcore。可以通过 sysrq 强制系统崩溃。
# echo c > /proc/sysrq-trigger
这造成内核崩溃,如配置有效,系统将重启进入 kdump 内核,当系统进程进入到启动 kdump 服务的点时,vmcore 将会拷贝到你在 kdump 配置文件中设置的位置。RHEL 的缺省目录是 : /var/crash;SLES 的缺省目录是 : /var/log/dump。然后系统重启进入到正常的内核。一旦回复到正常的内核,就可以在上述的目录下发现 vmcore 文件,即内存转储文件。可以使用之前安装的 kernel-debuginfo 中的 crash 工具来进行分析(crash 的更多详细用法将在本系列后面的文章中有介绍)。
# crash /usr/lib/debug/lib/modules/2.6.17-1.2621.el5/vmlinux /var/crash/2006-08-23-15:34/vmcore crash> bt
3.4 载入“转储捕获”内核
需要引导系统内核时,可使用如下步骤和命令载入“转储捕获”内核:
kexec -p\ --initrd= --args-linux \ --append="root= init 1 irqpoll"
装载转储捕捉内核的注意事项:
转储捕捉内核应当是一个 vmlinux 格式的映像(即是一个未压缩的 ELF 映像文件),而不能是 bzImage 格式;
默认情况下,ELF 文件头采用 ELF64 格式存储以支持那些拥有超过 4GB 内存的系统。但是可以指定“--elf32-core-headers”标志以强制使用 ELF32 格式的 ELF 文件头。这个标志是有必要注意的,一个重要的原因就是:当前版本的 GDB 不能在一个 32 位系统上打开一个使用 ELF64 格式的 vmcore 文件。ELF32 格式的文件头不能使用在一个“没有物理地址扩展”(non-PAE)的系统上(即:少于 4GB 内存的系统);
一个“irqpoll”的启动参数可以减低由于在“转储捕获内核”中使用了“共享中断”技术而导致出现驱动初始化失败这种情况发生的概率 ;
必须指定
3.5 后记
Kdump 是一个强大的、灵活的内核转储机制,能够在生产内核上下文中执行捕获内核是非常有价值的。本文仅介绍在 RHEL6.2 和 SLES11 中如何配置 kdump。望抛砖引玉,对阅读本文的读者有益。
参考:
1 kallsyms的分析
2 深入探索 Kdump
kgdb提供了一种使用 gdb调试 Linux 内核的机制。使用KGDB可以象调试普通的应用程序那样,在内核中进行设置断点、检查变量值、单步跟踪程序运行等操作。使用KGDB调试时需要两台机器,一台作为开发机(Development Machine),另一台作为目标机(Target Machine),两台机器之间通过串口或者以太网口相连。串口连接线是一根RS-232接口的电缆,在其内部两端的第2脚(TXD)与第3脚(RXD)交叉相连,第7脚(接地脚)直接相连。调试过程中,被调试的内核运行在目标机上,gdb调试器运行在开发机上。
目前,kgdb发布支持i386、x86_64、32-bit PPC、SPARC等几种体系结构的调试器。
安装kgdb调试环境需要为Linux内核应用kgdb补丁,补丁实现的gdb远程调试所需要的功能包括命令处理、陷阱处理及串口通讯3个主要的部分。kgdb补丁的主要作用是在Linux内核中添加了一个调试Stub。调试Stub是Linux内核中的一小段代码,提供了运行gdb的开发机和所调试内核之间的一个媒介。gdb和调试stub之间通过gdb串行协议进行通讯。gdb串行协议是一种基于消息的ASCII码协议,包含了各种调试命令。当设置断点时,kgdb负责在设置断点的指令前增加一条trap指令,当执行到断点时控制权就转移到调试stub中去。此时,调试stub的任务就是使用远程串行通信协议将当前环境传送给gdb,然后从gdb处接受命令。gdb命令告诉stub下一步该做什么,当stub收到继续执行的命令时,将恢复程序的运行环境,把对CPU的控制权重新交还给内核
下面我们将以Linux 2.6.7内核为例详细介绍kgdb调试环境的建立过程。
2.1 软硬件准备
以下软硬件配置取自笔者进行试验的系统配置情况:
kgdb补丁的版本遵循如下命名模式:Linux-A-kgdb-B,其中A表示Linux的内核版本号,B为kgdb的版本号。以试验使用的kgdb补丁为例,linux内核的版本为linux-2.6.7,补丁版本为kgdb-2.2。
物理连接好串口线后,使用以下命令来测试两台机器之间串口连接情况,stty命令可以对串口参数进行设置:
在development机上执行:
1
|
stty ispeed 115200 ospeed 115200 -F /dev/ttyS0
|
在target机上执行:
1
|
stty ispeed 115200 ospeed 115200 -F /dev/ttyS0
|
在developement机上执行:
1
|
echo hello > /dev/ttyS0
|
在target机上执行:
1
|
cat /dev/ttyS0
|
如果串口连接没问题的话在将在target机的屏幕上显示"hello"。
2.2 安装与配置
下面我们需要应用kgdb补丁到Linux内核,设置内核选项并编译内核。这方面的资料相对较少,笔者这里给出详细的介绍。下面的工作在开发机(developement)上进行,以上面介绍的试验环境为例,某些具体步骤在实际的环境中可能要做适当的改动:
I、内核的配置与编译
1
2
3
|
[root@lisl tmp] # tar -jxvf linux-2.6.7.tar.bz2
[root@lisl tmp] #tar -jxvf linux-2.6.7-kgdb-2.2.tar.tar
[root@lisl tmp] #cd inux-2.6.7
|
请参照目录补丁包中文件README给出的说明,执行对应体系结构的补丁程序。由于试验在i386体系结构上完成,所以只需要安装一下补丁:core-lite.patch、i386-lite.patch、8250.patch、eth.patch、core.patch、i386.patch。应用补丁文件时,请遵循kgdb软件包内series文件所指定的顺序,否则可能会带来预想不到的问题。eth.patch文件是选择以太网口作为调试的连接端口时需要运用的补丁。
应用补丁的命令如下所示:
1
|
[root@lisl tmp] #patch -p1 <../linux-2.6.7-kgdb-2.2/core-lite.patch
|
如果内核正确,那么应用补丁时应该不会出现任何问题(不会产生*.rej文件)。为Linux内核添加了补丁之后,需要进行内核的配置。内核的配置可以按照你的习惯选择配置Linux内核的任意一种方式。
1
|
[root@lisl tmp] #make menuconfig
|
在内核配置菜单的Kernel hacking选项中选择kgdb调试项,例如:
1
2
3
4
5
|
[*] KGDB: kernel debugging with remote gdb
Method for KGDB communication (KGDB: On generic serial port (8250)) --->
[*] KGDB: Thread analysis
[*] KGDB: Console messages through gdb
[root@lisl tmp] #make
|
编译内核之前请注意Linux目录下Makefile中的优化选项,默认的Linux内核的编译都以-O2的优化级别进行。在这个优化级别之下,编译器要对内核中的某些代码的执行顺序进行改动,所以在调试时会出现程序运行与代码顺序不一致的情况。可以把Makefile中的-O2选项改为-O,但不可去掉-O,否则编译会出问题。为了使编译后的内核带有调试信息,注意在编译内核的时候需要加上-g选项。
不过,当选择"Kernel debugging->Compile the kernel with debug info"选项后配置系统将自动打开调试选项。另外,选择"kernel debugging with remote gdb"后,配置系统将自动打开"Compile the kernel with debug info"选项。
内核编译完成后,使用scp命令进行将相关文件拷贝到target机上(当然也可以使用其它的网络工具,如rcp)。
1
2
|
[root@lisl tmp] #scp arch/i386/boot/bzImage [email protected]:/boot/vmlinuz-2.6.7-kgdb
[root@lisl tmp] #scp System.map [email protected]:/boot/System.map-2.6.7-kgdb
|
如果系统启动使所需要的某些设备驱动没有编译进内核的情况下,那么还需要执行如下操作:
1
2
|
[root@lisl tmp] #mkinitrd /boot/initrd-2.6.7-kgdb 2.6.7
[root@lisl tmp] #scp initrd-2.6.7-kgdb [email protected]:/boot/ initrd-2.6.7-kgdb
|
II、kgdb的启动
在将编译出的内核拷贝的到target机器之后,需要配置系统引导程序,加入内核的启动选项。以下是kgdb内核引导参数的说明:
如表中所述,在kgdb 2.0版本之后内核的引导参数已经与以前的版本有所不同。使用grub引导程序时,直接将kgdb参数作为内核vmlinuz的引导参数。下面给出引导器的配置示例。
1
2
3
|
title 2.6.7 kgdb
root (hd0,0)
kernel /boot/vmlinuz-2 .6.7-kgdb ro root= /dev/hda1 kgdbwait kgdb8250=1,115200
|
在使用lilo作为引导程序时,需要把kgdb参放在由append修饰的语句中。下面给出使用lilo作为引导器时的配置示例。
1
2
3
4
5
|
image= /boot/vmlinuz-2 .6.7-kgdb
label=kgdb
read -only
root= /dev/hda3
append= "gdb gdbttyS=1 gdbbaud=115200"
|
保存好以上配置后重新启动计算机,选择启动带调试信息的内核,内核将在短暂的运行后在创建init内核线程之前停下来,打印出以下信息,并等待开发机的连接。
Waiting for connection from remote gdb...
在开发机上执行:
1
2
3
4
|
gdb
file vmlinux
set remotebaud 115200
target remote /dev/ttyS0
|
其中vmlinux是指向源代码目录下编译出来的Linux内核文件的链接,它是没有经过压缩的内核文件,gdb程序从该文件中得到各种符号地址信息。
这样,就与目标机上的kgdb调试接口建立了联系。一旦建立联接之后,对Linux内的调试工作与对普通的运用程序的调试就没有什么区别了。任何时候都可以通过键入ctrl+c打断目标机的执行,进行具体的调试工作。
在kgdb 2.0之前的版本中,编译内核后在arch/i386/kernel目录下还会生成可执行文件gdbstart。将该文件拷贝到target机器的/boot目录下,此时无需更改内核的启动配置文件,直接使用命令:
1
|
[root@lisl boot] #gdbstart -s 115200 -t /dev/ttyS0
|
可以在KGDB内核引导启动完成后建立开发机与目标机之间的调试联系。
2.3 通过网络接口进行调试
kgdb也支持使用以太网接口作为调试器的连接端口。在对Linux内核应用补丁包时,需应用eth.patch补丁文件。配置内核时在Kernel hacking中选择kgdb调试项,配置kgdb调试端口为以太网接口,例如:
1
2
3
4
|
[*]KGDB: kernel debugging with remote gdb
Method for KGDB communication (KGDB: On ethernet) --->
( ) KGDB: On generic serial port (8250)
(X) KGDB: On ethernet
|
另外使用eth0网口作为调试端口时,grub.list的配置如下:
1
2
3
|
title 2.6.7 kgdb
root (hd0,0)
|
其他的过程与使用串口作为连接端口时的设置过程相同。
注意:尽管可以使用以太网口作为kgdb的调试端口,使用串口作为连接端口更加简单易行,kgdb项目组推荐使用串口作为调试端口。
2.4 模块的调试方法
内核可加载模块的调试具有其特殊性。由于内核模块中各段的地址是在模块加载进内核的时候才最终确定的,所以develop机的gdb无法得到各种符号地址信息。所以,使用kgdb调试模块所需要解决的一个问题是,需要通过某种方法获得可加载模块的最终加载地址信息,并把这些信息加入到gdb环境中。
I、在Linux 2.4内核中的内核模块调试方法
在Linux2.4.x内核中,可以使用insmod -m命令输出模块的加载信息,例如:
1
|
[root@lisl tmp] # insmod -m hello.ko >modaddr
|
查看模块加载信息文件modaddr如下:
1
2
3
4
5
6
7
|
.this 00000060 c88d8000 2**2
.text 00000035 c88d8060 2**2
.rodata 00000069 c88d80a0 2**5
……
.data 00000000 c88d833c 2**2
.bss 00000000 c88d833c 2**2
……
|
在这些信息中,我们关心的只有4个段的地址:.text、.rodata、.data、.bss。在development机上将以上地址信息加入到gdb中,这样就可以进行模块功能的测试了。
1
|
( gdb ) Add-symbol- file hello.o 0xc88d8060 -s .data 0xc88d80a0 -s .rodata 0xc88d80a0 -s .bss 0x c88d833c
|
这种方法也存在一定的不足,它不能调试模块初始化的代码,因为此时模块初始化代码已经执行过了。而如果不执行模块的加载又无法获得模块插入地址,更不可能在模块初始化之前设置断点了。对于这种调试要求可以采用以下替代方法。
在target机上用上述方法得到模块加载的地址信息,然后再用rmmod卸载模块。在development机上将得到的模块地址信息导入到gdb环境中,在内核代码的调用初始化代码之前设置断点。这样,在target机上再次插入模块时,代码将在执行模块初始化之前停下来,这样就可以使用gdb命令调试模块初始化代码了。
另外一种调试模块初始化函数的方法是:当插入内核模块时,内核模块机制将调用函数sys_init_module(kernel/modle.c)执行对内核模块的初始化,该函数将调用所插入模块的初始化函数。程序代码片断如下:
1
2
3
4
|
…… ……
if (mod->init != NULL)
ret = mod->init();
…… ……
|
在该语句上设置断点,也能在执行模块初始化之前停下来。
II、在Linux 2.6.x内核中的内核模块调试方法
Linux 2.6之后的内核中,由于module-init-tools工具的更改,insmod命令不再支持-m参数,只有采取其他的方法来获取模块加载到内核的地址。通过分析ELF文件格式,我们知道程序中各段的意义如下:
.text(代码段):用来存放可执行文件的操作指令,也就是说是它是可执行程序在内存种的镜像。
.data(数据段):数据段用来存放可执行文件中已初始化全局变量,也就是存放程序静态分配的变量和全局变量。
.bss(BSS段):BSS段包含了程序中未初始化全局变量,在内存中 bss段全部置零。
.rodata(只读段):该段保存着只读数据,在进程映象中构造不可写的段。
通过在模块初始化函数中放置一下代码,我们可以很容易地获得模块加载到内存中的地址。
1
2
|