Linux内核调试方法总结

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


一  调试前的准备

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

  • 有一个被确认的bug。

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

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

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

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

 


二  内核中的bug

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


三  内核调试配置选项

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

1  内核配置

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

?
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  调试原子操作

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

?
1
2
3
4
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>中:

 

?
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); 

2  dump_stack()

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

?
1
2
3
4
   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等级: 

?
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     "<d>"   /* Use the default kernel loglevel           */

注意,如果调用者未将日志级别提供给 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 函数。 

4  记录缓冲区

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

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

 

?
1
CONFIG_LOG_BUF_SHIFT=18

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

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


5  syslogd/klogd

在标准的Linux系统上,用户空间的守护进程klogd从纪录缓冲区中获取内核消息,再通过syslogd守护进程把这些消息保存在系统日志文件中。klogd进程既可以从/proc/kmsg文件中,也可以通过syslog()系统调用读取这些消息。默认情况下,它选择读取/proc方式实现。klogd守护进程在消息缓冲区有新的消息之前,一直处于阻塞状态。一旦有新的内核消息,klogd被唤醒,读出内核消息并进行处理。默认情况下,处理例程就是把内核消息传给syslogd守护进程。syslogd守护进程一般把接收到的消息写入/var/log/messages文件中。不过,还是可以通过/etc/syslog.conf文件来进行配置,可以选择其他的输出文件。

Linux内核调试方法总结
    




Linux内核调试方法


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”. 

8 内核printk和日志系统的总体结构Linux内核调试方法总结
    




Linux内核调试方法


9  动态调试

动态调试是通过动态的开启和禁止某些内核代码来获取额外的内核信息。 
首先内核选项CONFIG_DYNAMIC_DEBUG应该被设置。所有通过pr_debug()/dev_debug()打印的信息都可以动态的显示或不显示。 
可以通过简单的查询语句来筛选需要显示的信息。 

-源文件名

-函数名

-行号(包括指定范围的行号)

-模块名

-格式化字符串

将要打印信息的格式写入<debugfs>/dynamic_debug/control中。 

?
1
nullarbor:~ # echo 'file svcsock.c line 1603 +p' >      <debugfs>/dynamic_debug/control


参考: 
1   内核日志及printk结构浅析 -- Tekkaman Ninja 
2   内核日志:API 及实现 
3   printk实现分析 
4   dynamic-debug-howto.txt 

六  内存调试工具

1  MEMWATCH

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 <stdlib.h>
#include <stdio.h>
#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 为您显示真正导致问题的行。如果您释放一个已经释放过的指针,它会告诉您。对于没有释放的内存也一样。日志结尾部分显示统计信息,包括泄漏了多少内存,使用了多少内存,以及总共分配了多少内存。


2  YAMD

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 <stdlib.h>
#include <stdio.h>
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);
}
您可以使用下面的命令来启动 YAMD: 

 

?
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 选项。 

3  Electric Fence

多数 Linux 分发版包含一个 Electric Fence 包,不过您也可以选择下载它。Electric Fence 是一个由 Bruce Perens 编写的malloc()调试库。它就在您分配内存后分配受保护的内存。如果存在 fencepost 错误(超过数组末尾运行),程序就会产生保护错误,并立即结束。通过结合 Electric Fence 和 gdb,您可以精确地跟踪到哪一行试图访问受保护内存。ElectricFence 的另一个功能就是能够检测内存泄漏。 

七  strace

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

OOPS(也称 Panic)消息包含系统错误的细节,如 CPU 寄存器的内容等。是内核告知用户有不幸发生的最常用的方式。 
内核只能发布OOPS,这个过程包括向终端上输出错误消息,输出寄存器保存的信息,并输出可供跟踪的回溯线索。通常,发送完OOPS之后,内核会处于一种不稳定的状态。 
OOPS的产生有很多可能原因,其中包括内存访问越界或非法的指令等。 

※ 作为内核的开发者,必定将会经常处理OOPS。

※ OOPS中包含的重要信息,对所有体系结构的机器都是完全相同的:寄存器上下文和回溯线索(回溯线索显示了导致错误发生的函数调用链)。



1  ksymoops

在 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: [<c0155d4f>]...
... 15:59:37 sfb1 kernel: [<c0106e04 ...
... 15:59:37 sfb1 kernel: Code: 8b 2d 00 00 00 00 55 ...
>>EIP; c01588fc <jfs_mount+3c/2c0> <=====
...
Trace; c0106cf3 <system_call+33/40>
Code; c01588fc <jfs_mount+3c/2c0>
00000000 <_EIP>:
Code; c01588fc <jfs_mount+3c/2c0>  <=====
  0: 8b 2d 00 00 00 00 mov 0x0,%ebp    <=====
Code; c0158902 <jfs_mount+42/2c0>
  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 <jfs_mount>:
  0:55 push %ebp
 ...
 2c: e8 cf 03 00 00   call   400 <chkSuper>
 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 <jfs_mount+0x291>
 3c: 8b 2d 00 00 00 00 mov 0x0,%ebp << problem line above
 42: 55 push %ebp

 


2  kallsyms

开发版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  Kdump

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 实现流程

Linux内核调试方法总结
    




Linux内核调试方法 

                     图 1. RHEL6.2 执行流程

Linux内核调试方法总结
    




Linux内核调试方法 

                                      图 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 <dump-capture-kernel> \ 
           --initrd=<initrd-for-dump-capture-kernel> --args-linux \ 
           --append="root=<root-dev> init 1 irqpoll"

 

装载转储捕捉内核的注意事项:

  • 转储捕捉内核应当是一个 vmlinux 格式的映像(即是一个未压缩的 ELF 映像文件),而不能是 bzImage 格式;

  • 默认情况下,ELF 文件头采用 ELF64 格式存储以支持那些拥有超过 4GB 内存的系统。但是可以指定“--elf32-core-headers”标志以强制使用 ELF32 格式的 ELF 文件头。这个标志是有必要注意的,一个重要的原因就是:当前版本的 GDB 不能在一个 32 位系统上打开一个使用 ELF64 格式的 vmcore 文件。ELF32 格式的文件头不能使用在一个“没有物理地址扩展”(non-PAE)的系统上(即:少于 4GB 内存的系统);

  • 一个“irqpoll”的启动参数可以减低由于在“转储捕获内核”中使用了“共享中断”技术而导致出现驱动初始化失败这种情况发生的概率 ;

  • 必须指定 <root-dev>,指定的格式是和要使用根设备的名字。具体可以查看 mount 命令的输出;“init 1”这个命令将启动“转储捕捉内核”到一个没有网络支持的单用户模式。如果你希望有网络支持,那么使用“init 3”。

 

3.5  后记

Kdump 是一个强大的、灵活的内核转储机制,能够在生产内核上下文中执行捕获内核是非常有价值的。本文仅介绍在 RHEL6.2 和 SLES11 中如何配置 kdump。望抛砖引玉,对阅读本文的读者有益。


参考:

 kallsyms的分析 
2   深入探索 Kdump


九  KGDB

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等几种体系结构的调试器。 

1  kgdb的调试原理

安装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内核调试方法总结
    




Linux内核调试方法 

2  Kgdb的安装与设置

下面我们将以Linux 2.6.7内核为例详细介绍kgdb调试环境的建立过程。 


2.1  软硬件准备

以下软硬件配置取自笔者进行试验的系统配置情况:Linux内核调试方法总结
    




Linux内核调试方法 
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 root@192.168.6.13:/boot/vmlinuz-2.6.7-kgdb
[root@lisl tmp]#scp System.map root@192.168.6.13:/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 root@192.168.6.13:/boot/ initrd-2.6.7-kgdb

 


II、kgdb的启动 
在将编译出的内核拷贝的到target机器之后,需要配置系统引导程序,加入内核的启动选项。以下是kgdb内核引导参数的说明: 
Linux内核调试方法总结
    




Linux内核调试方法 
如表中所述,在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)
kernel /boot/vmlinuz-2.6.7-kgdb ro root=/dev/hda1 kgdbwait kgdboe=@192.168.5.13/,@192.168. 6.13/

 

其他的过程与使用串口作为连接端口时的设置过程相同。 
注意:尽管可以使用以太网口作为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
3
4
5
6
7
8
9
10
11
……
int bss_var;
static int hello_init(void)
{
printk(KERN_ALERT "Text location .text(Code Segment):%p\n",hello_init);
static int data_var=0;
printk(KERN_ALERT "Data Location .data(Data Segment):%p\n",&data_var);
printk(KERN_ALERT "BSS Location: .bss(BSS Segment):%p\n",&bss_var);
……
}
Module_init(hello_init);

这里,通过在模块的初始化函数中添加一段简单的程序,使模块在加载时打印出在内核中的加载地址。.rodata段的地址可以通过执行命令readelf -e hello.ko,取得.rodata在文件中的偏移量并加上段的align值得出。 
为了使读者能够更好地进行模块的调试,kgdb项目还发布了一些脚本程序能够自动探测模块的插入并自动更新gdb中模块的符号信息。这些脚本程序的工作原理与前面解释的工作过程相似,更多的信息请阅读参考资料[4]。 

2.5  硬件断点 
kgdb提供对硬件调试寄存器的支持。在kgdb中可以设置三种硬件断点:执行断点(Execution Breakpoint)、写断点(Write Breakpoint)、访问断点(Access Breakpoint)但不支持I/O访问的断点。 目前,kgdb对硬件断点的支持是通过宏来实现的,最多可以设置4个硬件断点,这些宏的用法如下: 
Linux内核调试方法总结
    




Linux内核调试方法 
在有些情况下,硬件断点的使用对于内核的调试是非常方便的。 

3  在VMware中搭建调试环境

kgdb调试环境需要使用两台微机分别充当development机和target机,使用VMware后我们只使用一台计算机就可以顺利完成kgdb调试环境的搭建。以windows下的环境为例,创建两台虚拟机,一台作为开发机,一台作为目标机。 

3.1  虚拟机之间的串口连接 
虚拟机中的串口连接可以采用两种方法。一种是指定虚拟机的串口连接到实际的COM上,例如开发机连接到COM1,目标机连接到COM2,然后把两个串口通过串口线相连接。另一种更为简便的方法是:在较高一些版本的VMware中都支持把串口映射到命名管道,把两个虚拟机的串口映射到同一个命名管道。例如,在两个虚拟机中都选定同一个命名管道 \\.\pipe\com_1,指定target机的COM口为server端,并选择"The other end is a virtual machine"属性;指定development机的COM口端为client端,同样指定COM口的"The other end is a virtual machine"属性。对于IO mode属性,在target上选中"Yield CPU on poll"复选择框,development机不选。这样,可以无需附加任何硬件,利用虚拟机就可以搭建kgdb调试环境。 即降低了使用kgdb进行调试的硬件要求,也简化了建立调试环境的过程。 
Linux内核调试方法总结
    




Linux内核调试方法 


3.2  VMware的使用技巧

VMware虚拟机是比较占用资源的,尤其是象上面那样在Windows中使用两台虚拟机。因此,最好为系统配备512M以上的内存,每台虚拟机至少分配128M的内存。这样的硬件要求,对目前主流配置的PC而言并不是过高的要求。出于系统性能的考虑,在VMware中尽量使用字符界面进行调试工作。同时,Linux系统默认情况下开启了sshd服务,建议使用SecureCRT登陆到Linux进行操作,这样可以有较好的用户使用界面。 

3.3  在Linux下的虚拟机中使用kgdb 
对于在Linux下面使用VMware虚拟机的情况,笔者没有做过实际的探索。从原理上而言,只需要在Linux下只要创建一台虚拟机作为target机,开发机的工作可以在实际的Linux环境中进行,搭建调试环境的过程与上面所述的过程类似。由于只需要创建一台虚拟机,所以使用Linux下的虚拟机搭建kgdb调试环境对系统性能的要求较低。(vmware已经推出了Linux下的版本)还可以在development机上配合使用一些其他的调试工具,例如功能更强大的cgdb、图形界面的DDD调试器等,以方便内核的调试工作。 
Linux内核调试方法总结
    




Linux内核调试方法 


4  kgdb的一些特点和不足

使用kgdb作为内核调试环境最大的不足在于对kgdb硬件环境的要求较高,必须使用两台计算机分别作为target和development机。尽管使用虚拟机的方法可以只用一台PC即能搭建调试环境,但是对系统其他方面的性能也提出了一定的要求,同时也增加了搭建调试环境时复杂程度。另外,kgdb内核的编译、配置也比较复杂,需要一定的技巧,笔者当时做的时候也是费了很多周折。当调试过程结束后时,还需要重新制作所要发布的内核。使用kgdb并不能进行全程调试,也就是说kgdb并不能用于调试系统一开始的初始化引导过程。 
不过,kgdb是一个不错的内核调试工具,使用它可以进行对内核的全面调试,甚至可以调试内核的中断处理程序。如果在一些图形化的开发工具的帮助下,对内核的调试将更方便。 

参考: 
透过虚拟化技术体验kgdb 
Linux 系统内核的调试 
Debugging The Linux Kernel Using Gdb 


十  使用SkyEye构建Linux内核调试环境

SkyEye是一个开源软件项目(OPenSource Software),SkyEye项目的目标是在通用的Linux和Windows平台上模拟常见的嵌入式计算机系统。SkyEye实现了一个指令级的硬件模拟平台,可以模拟多种嵌入式开发板,支持多种CPU指令集。SkyEye 的核心是 GNU 的 gdb 项目,它把gdb和 ARM Simulator很好地结合在了一起。加入ARMulator 的功能之后,它就可以来仿真嵌入式开发板,在它上面不仅可以调试硬件驱动,还可以调试操作系统。Skyeye项目目前已经在嵌入式系统开发领域得到了很大的推广。 

1  SkyEye的安装和μcLinux内核编译

1.1  SkyEye的安装 
SkyEye的安装不是本文要介绍的重点,目前已经有大量的资料对此进行了介绍。有关SkyEye的安装与使用的内容请查阅参考资料[11]。由于skyeye面目主要用于嵌入式系统领域,所以在skyeye上经常使用的是μcLinux系统,当然使用Linux作为skyeye上运行的系统也是可以的。由于介绍μcLinux 2.6在skyeye上编译的相关资料并不多,所以下面进行详细介绍。 

1.2  μcLinux 2.6.x的编译 
要在SkyEye中调试操作系统内核,首先必须使被调试内核能在SkyEye所模拟的开发板上正确运行。因此,正确编译待调试操作系统内核并配置SkyEye是进行内核调试的第一步。下面我们以SkyEye模拟基于Atmel AT91X40的开发板,并运行μcLinux 2.6为例介绍SkyEye的具体调试方法。 

I、安装交叉编译环境 
先安装交叉编译器。尽管在一些资料中说明使用工具链arm-elf-tools-20040427.sh ,但是由于arm-elf-xxx与arm-linux-xxx对宏及链接处理的不同,经验证明使用arm-elf-xxx工具链在链接vmlinux的最后阶段将会出错。所以这里我们使用的交叉编译工具链是:arm-uclinux-tools-base-gcc3.4.0-20040713.sh,关于该交叉编译工具链的下载地址请参见[6]。注意以下步骤最好用root用户来执行。 

?
1
2
[root@lisl tmp]#chmod +x  arm-uclinux-tools-base-gcc3.4.0-20040713.sh
[root@lisl tmp]#./arm-uclinux-tools-base-gcc3.4.0-20040713.sh

 

安装交叉编译工具链之后,请确保工具链安装路径存在于系统PATH变量中。 

II、制作μcLinux内核 
得到μcLinux发布包的一个最容易的方法是直接访问uClinux.org站点[7]。该站点发布的内核版本可能不是最新的,但你能找到一个最新的μcLinux补丁以及找一个对应的Linux内核版本来制作一个最新的μcLinux内核。这里,将使用这种方法来制作最新的μcLinux内核。目前(笔者记录编写此文章时),所能得到的发布包的最新版本是uClinux-dist.20041215.tar.gz。 
下载uClinux-dist.20041215.tar.gz,文件的下载地址请参见[7]。 
下载linux-2.6.9-hsc0.patch.gz,文件的下载地址请参见[8]。 
下载linux-2.6.9.tar.bz2,文件的下载地址请参见[9]。 
现在我们得到了整个的linux-2.6.9源代码,以及所需的内核补丁。请准备一个有2GB空间的目录里来完成以下制作μcLinux内核的过程。 

?
1
2
3
[root@lisl tmp]# tar -jxvf uClinux-dist-20041215.tar.bz2
[root@lisl uClinux-dist]# tar -jxvf  linux-2.6.9.tar.bz2
[root@lisl uClinux-dist]# gzip -dc linux-2.6.9-hsc0.patch.gz | patch -p0

 

或者使用:

?
1
2
[root@lisl uClinux-dist]# gunzip linux-2.6.9-hsc0.patch.gz 
[root@lisl uClinux-dist]patch -p0 < linux-2.6.9-hsc0.patch

 

执行以上过程后,将在linux-2.6.9/arch目录下生成一个补丁目录-armnommu。删除原来μcLinux目录里的linux-2.6.x(即那个linux-2.6.9-uc0),并将我们打好补丁的Linux内核目录更名为linux-2.6.x。 

?
1
2
[root@lisl uClinux-dist]# rm -rf linux-2.6.x/
[root@lisl uClinux-dist]# mv linux-2.6.9 linux-2.6.x

 


III、配置和编译μcLinux内核 
因为只是出于调试μcLinux内核的目的,这里没有生成uClibc库文件及romfs.img文件。在发布μcLinux时,已经预置了某些常用嵌入式开发板的配置文件,因此这里直接使用这些配置文件,过程如下: 

?
1
2
[root@lisl uClinux-dist]# cd linux-2.6.x
[root@lisl linux-2.6.x]#make ARCH=armnommu CROSS_COMPILE=arm-uclinux- atmel_deconfig

 

atmel_deconfig文件是μcLinux发布时提供的一个配置文件,存放于目录linux-2.6.x /arch/armnommu/configs/中。 

?
1
[root@lisl linux-2.6.x]#make ARCH=armnommu CROSS_COMPILE=arm-uclinux- oldconfig

 

下面编译配置好的内核: 

?
1
[root@lisl linux-2.6.x]# make ARCH=armnommu CROSS_COMPILE=arm-uclinux- v=1

 



一般情况下,编译将顺利结束并在Linux-2.6.x/目录下生成未经压缩的μcLinux内核文件vmlinux。需要注意的是为了调试μcLinux内核,需要打开内核编译的调试选项-g,使编译后的内核带有调试信息。打开编译选项的方法可以选择:
"Kernel debugging->Compile the kernel with debug info"后将自动打开调试选项。也可以直接修改linux-2.6.x目录下的Makefile文件,为其打开调试开关。方法如下:。 

?
1
CFLAGS  += -g

 

最容易出现的问题是找不到arm-uclinux-gcc命令的错误,主要原因是PATH变量中没有 包含arm-uclinux-gcc命令所在目录。在arm-linux-gcc的缺省安装情况下,它的安装目录是/root/bin/arm-linux-tool/,使用以下命令将路径加到PATH环境变量中。 

?
1
Export PATH=$PATH:/root/bin/arm-linux-tool/bin

 


IV、根文件系统的制作 
Linux内核在启动的时的最后操作之一是加载根文件系统。根文件系统中存放了嵌入式 系统使用的所有应用程序、库文件及其他一些需要用到的服务。出于文章篇幅的考虑,这里不打算介绍根文件系统的制作方法,读者可以查阅一些其他的相关资料。值得注意的是,由配置文件skyeye.conf指定了装载到内核中的根文件系统。 

2  使用SkyEye调试

编译完μcLinux内核后,就可以在SkyEye中调试该ELF执行文件格式的内核了。前面已经说过利用SkyEye调试内核与使用gdb调试运用程序的方法相同。 
需要提醒读者的是,SkyEye的配置文件-skyeye.conf记录了模拟的硬件配置和模拟执行行为。该配置文件是SkyEye系统中一个及其重要的文件,很多错误和异常情况的发生都和该文件有关。在安装配置SkyEye出错时,请首先检查该配置文件然后再进行其他的工作。此时,所有的准备工作已经完成,就可以进行内核的调试工作了。 

3  使用SkyEye调试内核的特点和不足

在SkyEye中可以进行对Linux系统内核的全程调试。由于SkyEye目前主要支持基于ARM内核的CPU,因此一般而言需要使用交叉编译工具编译待调试的Linux系统内核。另外,制作SkyEye中使用的内核编译、配置过程比较复杂、繁琐。不过,当调试过程结束后无需重新制作所要发布的内核。 
SkyEye只是对系统硬件进行了一定程度上的模拟,所以在SkyEye与真实硬件环境相比较而言还是有一定的差距,这对一些与硬件紧密相关的调试可能会有一定的影响,例如驱动程序的调试。不过对于大部分软件的调试,SkyEye已经提供了精度足够的模拟了。 
SkyEye的下一个目标是和eclipse结合,有了图形界面,能为调试和查看源码提供一些方便。 

参考: 
Linux 系统内核的调试 

十一  KDB

Linux 内核调试器(KDB)允许您调试 Linux 内核。这个恰如其名的工具实质上是内核代码的补丁,它允许高手访问内核内存和数据结构。KDB 的主要优点之一就是它不需要用另一台机器进行调试:您可以调试正在运行的内核。 
设置一台用于 KDB 的机器需要花费一些工作,因为需要给内核打补丁并进行重新编译。KDB 的用户应当熟悉 Linux 内核的编译(在一定程度上还要熟悉内核内部机理)。 
在本文中,我们将从有关下载 KDB 补丁、打补丁、(重新)编译内核以及启动 KDB 方面的信息着手。然后我们将了解 KDB 命令并研究一些较常用的命令。最后,我们将研究一下有关设置和显示选项方面的一些详细信息。 

1  入门

KDB 项目是由 Silicon Graphics 维护的,您需要从它的 FTP 站点下载与内核版本有关的补丁。(在编写本文时)可用的最新 KDB 版本是 4.2。您将需要下载并应用两个补丁。一个是“公共的”补丁,包含了对通用内核代码的更改,另一个是特定于体系结构的补丁。补丁可作为 bz2 文件获取。例如,在运行 2.4.20 内核的 x86 机器上,您会需要 kdb-v4.2-2.4.20-common-1.bz2 和 kdb-v4.2-2.4.20-i386-1.bz2。 
这里所提供的所有示例都是针对 i386 体系结构和 2.4.20 内核的。您将需要根据您的机器和内核版本进行适当的更改。您还需要拥有 root 许可权以执行这些操作。 
将文件复制到 /usr/src/linux 目录中并从用 bzip2 压缩的文件解压缩补丁文件:

?
1
2
#bzip2 -d kdb-v4.2-2.4.20-common-1.bz2
#bzip2 -d kdb-v4.2-2.4.20-i386-1.bz2

 

您将获得 kdb-v4.2-2.4.20-common-1 和 kdb-v4.2-2.4-i386-1 文件。 
现在,应用这些补丁:

?
1
2
#patch -p1 <kdb-v4.2-2.4.20-common-1
#patch -p1 <kdb-v4.2-2.4.20-i386-1

 

这些补丁应该干净利落地加以应用。查找任何以 .rej 结尾的文件。这个扩展名表明这些是失败的补丁。如果内核树没问题,那么补丁的应用就不会有任何问题。 
接下来,需要构建内核以支持 KDB。第一步是设置 CONFIG_KDB 选项。使用您喜欢的配置机制(xconfig 和 menuconfig 等)来完成这一步。转到结尾处的“Kernel hacking”部分并选择“Built-in Kernel Debugger support”选项。 
您还可以根据自己的偏好选择其它两个选项。选择“Compile the kernel with frame pointers”选项(如果有的话)则设置CONFIG_FRAME_POINTER 标志。这将产生更好的堆栈回溯,因为帧指针寄存器被用作帧指针而不是通用寄存器。您还可以选择“KDB off by default”选项。这将设置 CONFIG_KDB_OFF 标志,并且在缺省情况下将关闭 KDB。我们将在后面一节中对此进行详细介绍。 
保存配置,然后退出。重新编译内核。建议在构建内核之前执行“make clean”。用常用方式安装内核并引导它。 

2  初始化并设置环境变量

您可以定义将在 KDB 初始化期间执行的 KDB 命令。需要在纯文本文件 kdb_cmds 中定义这些命令,该文件位于 Linux 源代码树(当然是在打了补丁之后)的 KDB 目录中。该文件还可以用来定义设置显示和打印选项的环境变量。文件开头的注释提供了编辑文件方面的帮助。使用这个文件的缺点是,在您更改了文件之后需要重新构建并重新安装内核。 

3  激活 KDB

如果编译期间没有选中 CONFIG_KDB_OFF ,那么在缺省情况下 KDB 是活动的。否则,您需要显式地激活它 - 通过在引导期间将kdb=on 标志传递给内核或者通过在挂装了 /proc 之后执行该工作:

?
1
#echo "1" >/proc/sys/kernel/kdb

 

倒过来执行上述步骤则会取消激活 KDB。也就是说,如果缺省情况下 KDB 是打开的,那么将 kdb=off 标志传递给内核或者执行下面这个操作将会取消激活 KDB:

?
1
#echo "0" >/proc/sys/kernel/kdb

 

在引导期间还可以将另一个标志传递给内核。 kdb=early 标志将导致在引导过程的初始阶段就把控制权传递给 KDB。如果您需要在引导过程初始阶段进行调试,那么这将有所帮助。 
调用 KDB 的方式有很多。如果 KDB 处于打开状态,那么只要内核中有紧急情况就自动调用它。按下键盘上的 PAUSE 键将手工调用 KDB。调用 KDB 的另一种方式是通过串行控制台。当然,要做到这一点,需要设置串行控制台并且需要一个从串行控制台进行读取的程序。按键序列 Ctrl-A 将从串行控制台调用 KDB。 

4  KDB 命令

KDB 是一个功能非常强大的工具,它允许进行几个操作,比如内存和寄存器修改、应用断点和堆栈跟踪。根据这些,可以将 KDB 命令分成几个类别。下面是有关每一类中最常用命令的详细信息。 

4.1  内存显示和修改 
这一类别中最常用的命令是 md 、 mdr 、 mm 和 mmW 。 
md 命令以一个地址/符号和行计数为参数,显示从该地址开始的 line-count 行的内存。如果没有指定 line-count ,那么就使用环境变量所指定的缺省值。如果没有指定地址,那么 md 就从上一次打印的地址继续。地址打印在开头,字符转换打印在结尾。 
mdr 命令带有地址/符号以及字节计数,显示从指定的地址开始的 byte-count 字节数的初始内存内容。它本质上和 md 一样,但是它不显示起始地址并且不在结尾显示字符转换。 mdr 命令较少使用。 
mm 命令修改内存内容。它以地址/符号和新内容作为参数,用 new-contents 替换地址处的内容。 
mmW 命令更改从地址开始的 W 个字节。请注意, mm 更改一个机器字。 
示例 

显示从 0xc000000 开始的 15 行内存:

?
1
[0]kdb> md 0xc000000 15

 

将内存位置为 0xc000000 上的内容更改为 0x10:

?
1
[0]kdb> mm 0xc000000 0x10

 


4.2  寄存器显示和修改 
这一类别中的命令有 rd 、 rm 和 ef 。 
rd 命令(不带任何参数)显示处理器寄存器的内容。它可以有选择地带三个参数。如果传递了 c 参数,则 rd 显示处理器的控制寄存器;如果带有 d 参数,那么它就显示调试寄存器;如果带有 u 参数,则显示上一次进入内核的当前任务的寄存器组。 
rm 命令修改寄存器的内容。它以寄存器名称和 new-contents 作为参数,用 new-contents 修改寄存器。寄存器名称与特定的体系结构有关。目前,不能修改控制寄存器。 
ef 命令以一个地址作为参数,它显示指定地址处的异常帧。 
示例 
显示通用寄存器组:

?
1
2
[0]kdb> rd
[0]kdb> rm %ebx 0x25

 


4.3  断点 
常用的断点命令有 bp 、 bc 、 bd 、 be 和 bl 。 
bp 命令以一个地址/符号作为参数,它在地址处应用断点。当遇到该断点时则停止执行并将控制权交予 KDB。该命令有几个有用的变体。 bpa 命令对 SMP 系统中的所有处理器应用断点。 bph 命令强制在支持硬件寄存器的系统上使用它。 bpha 命令类似于 bpa 命令,差别在于它强制使用硬件寄存器。 
bd 命令禁用特殊断点。它接收断点号作为参数。该命令不是从断点表中除去断点,而只是禁用它。断点号从 0 开始,根据可用性顺序分配给断点。 
be 命令启用断点。该命令的参数也是断点号。 
bl 命令列出当前的断点集。它包含了启用的和禁用的断点。 
bc 命令从断点表中除去断点。它以具体的断点号或 * 作为参数,在后一种情况下它将除去所有断点。 

示例 
对函数 sys_write() 设置断点:

?
1
[0]kdb> bp sys_write

 

列出断点表中的所有断点:

?
1
[0]kdb> bl

 

清除断点号 1:

?
1
[0]kdb> bc 1

 


4.4  堆栈跟踪 
主要的堆栈跟踪命令有 bt 、 btp 、 btc 和 bta 。 
bt 命令设法提供有关当前线程的堆栈的信息。它可以有选择地将堆栈帧地址作为参数。如果没有提供地址,那么它采用当前寄存器来回溯堆栈。否则,它假定所提供的地址是有效的堆栈帧起始地址并设法进行回溯。如果内核编译期间设置了CONFIG_FRAME_POINTER 选项,那么就用帧指针寄存器来维护堆栈,从而就可以正确地执行堆栈回溯。如果没有设置CONFIG_FRAME_POINTER ,那么 bt 命令可能会产生错误的结果。 
btp 命令将进程标识作为参数,并对这个特定进程进行堆栈回溯。 
btc 命令对每个活动 CPU 上正在运行的进程执行堆栈回溯。它从第一个活动 CPU 开始执行 bt ,然后切换到下一个活动 CPU,以此类推。 
bta 命令对处于某种特定状态的所有进程执行回溯。若不带任何参数,它就对所有进程执行回溯。可以有选择地将各种参数传递给该命令。将根据参数处理处于特定状态的进程。选项以及相应的状态如下: 

  • D:不可中断状态

  • R:正运行

  • S:可中断休眠

  • T:已跟踪或已停止

  • Z:僵死

  • U:不可运行

这类命令中的每一个都会打印出一大堆信息。示例 

跟踪当前活动线程的堆栈:

?
1
[0]kdb> bt

 

跟踪标识为 575 的进程的堆栈:

?
1
[0]kdb> btp 575

 


4.5  其它命令 
下面是在内核调试过程中非常有用的其它几个 KDB 命令。 
id 命令以一个地址/符号作为参数,它对从该地址开始的指令进行反汇编。环境变量 IDCOUNT 确定要显示多少行输出。 
ss 命令单步执行指令然后将控制返回给 KDB。该指令的一个变体是 ssb ,它执行从当前指令指针地址开始的指令(在屏幕上打印指令),直到它遇到将引起分支转移的指令为止。分支转移指令的典型示例有 call 、 return 和 jump 。 
go 命令让系统继续正常执行。一直执行到遇到断点为止(如果已应用了一个断点的话)。 
reboot 命令立刻重新引导系统。它并没有彻底关闭系统,因此结果是不可预测的。 
ll 命令以地址、偏移量和另一个 KDB 命令作为参数。它对链表中的每个元素反复执行作为参数的这个命令。所执行的命令以列表中当前元素的地址作为参数。 
示例 
反汇编从例程 schedule 开始的指令。所显示的行数取决于环境变量 IDCOUNT :

?
1
[0]kdb> id schedule

 

执行指令直到它遇到分支转移条件(在本例中为指令 jne )为止:

?
1
2
3
4
5
[0]kdb> ssb
0xc0105355  default_idle+0x25:  cli
0xc0105356  default_idle+0x26:  mov  0x14(%edx),%eax
0xc0105359  default_idle+0x29:  test %eax, %eax
0xc010535b  default_idle+0x2b:  jne  0xc0105361 default_idle+0x31

 


5  技巧和诀窍

调试一个问题涉及到:使用调试器(或任何其它工具)找到问题的根源以及使用源代码来跟踪导致问题的根源。单单使用源代码来确定问题是极其困难的,只有老练的内核黑客才有可能做得到。相反,大多数的新手往往要过多地依靠调试器来修正错误。这种方法可能会产生不正确的问题解决方案。我们担心的是这种方法只会修正表面症状而不能解决真正的问题。此类错误的典型示例是添加错误处理代码以处理 NULL 指针或错误的引用,却没有查出无效引用的真正原因。 
结合研究代码和使用调试工具这两种方法是识别和修正问题的最佳方案。 
调试器的主要用途是找到错误的位置、确认症状(在某些情况下还有起因)、确定变量的值,以及确定程序是如何出现这种情况的(即,建立调用堆栈)。有经验的黑客会知道对于某种特定的问题应使用哪一个调试器,并且能迅速地根据调试获取必要的信息,然后继续分析代码以识别起因。 
因此,这里为您介绍了一些技巧,以便您能使用 KDB 快速地取得上述结果。当然,要记住,调试的速度和精确度来自经验、实践和良好的系统知识(硬件和内核内部机理等)。 

5.1  技巧 #1 
在 KDB 中,在提示处输入地址将返回与之最为匹配的符号。这在堆栈分析以及确定全局数据的地址/值和函数地址方面极其有用。同样,输入符号名则返回其虚拟地址。 
示例 

表明函数 sys_read 从地址 0xc013db4c 开始:

?
1
2
[0]kdb> 0xc013db4c
0xc013db4c = 0xc013db4c (sys_read)

 

同样,表明 sys_write 位于地址 0xc013dcc8:

?
1
2
[0]kdb> sys_write
sys_write = 0xc013dcc8 (sys_write)

 

这些有助于在分析堆栈时找到全局数据和函数地址。 

5.2  技巧 #2 
在编译带 KDB 的内核时,只要 CONFIG_FRAME_POINTER 选项出现就使用该选项。为此,需要在配置内核时选择“Kernel hacking”部分下面的“Compile the kernel with frame pointers”选项。这确保了帧指针寄存器将被用作帧指针,从而产生正确的回溯。实际上,您可以手工转储帧指针寄存器的内容并跟踪整个堆栈。例如,在 i386 机器上,%ebp 寄存器可以用来回溯整个堆栈。 
例如,在函数 rmqueue() 上执行第一个指令后,堆栈看上去类似于下面这样:

?
1
2
3
4
5
6
7
8
9
10
11
12
[0]kdb> md %ebp
0xc74c9f38 c74c9f60  c0136c40 000001f0 00000000
0xc74c9f48 08053328 c0425238 c04253a8 00000000
0xc74c9f58 000001f0  00000246 c74c9f6c c0136a25
0xc74c9f68 c74c8000  c74c9f74  c0136d6d c74c9fbc
0xc74c9f78 c014fe45  c74c8000  00000000 08053328
[0]kdb> 0xc0136c40
0xc0136c40 = 0xc0136c40 (__alloc_pages +0x44)
[0]kdb> 0xc0136a25
0xc0136a25 = 0xc0136a25 (_alloc_pages +0x19)
[0]kdb> 0xc0136d6d
0xc0136d6d = 0xc0136d6d (__get_free_pages +0xd)

 

我们可以看到 rmqueue() 被 __alloc_pages 调用,后者接下来又被 _alloc_pages 调用,以此类推。 
每一帧的第一个双字(double word)指向下一帧,这后面紧跟着调用函数的地址。因此,跟踪堆栈就变成一件轻松的工作了。 

5.3  技巧 #3 
go 命令可以有选择地以一个地址作为参数。如果您想在某个特定地址处继续执行,则可以提供该地址作为参数。另一个办法是使用rm 命令修改指令指针寄存器,然后只要输入 go 。如果您想跳过似乎会引起问题的某个特定指令或一组指令,这就会很有用。但是,请注意,该指令使用不慎会造成严重的问题,系统可能会严重崩溃。 

5.4  技巧 #4 
您可以利用一个名为 defcmd 的有用命令来定义自己的命令集。例如,每当遇到断点时,您可能希望能同时检查某个特殊变量、检查某些寄存器的内容并转储堆栈。通常,您必须要输入一系列命令,以便能同时执行所有这些工作。 defcmd 允许您定义自己的命令,该命令可以包含一个或多个预定义的 KDB 命令。然后只需要用一个命令就可以完成所有这三项工作。其语法如下:

?
1
2
3
[0]kdb> defcmd name "usage" "help"
[0]kdb> [defcmd] type the commands here
[0]kdb> [defcmd] endefcmd

 

例如,可以定义一个(简单的)新命令 hari ,它显示从地址 0xc000000 开始的一行内存、显示寄存器的内容并转储堆栈:

?
1
2
3
4
5
[0]kdb> defcmd hari "" "no arguments needed"
[0]kdb> [defcmd] md 0xc000000 1
[0]kdb> [defcmd] rd
[0]kdb> [defcmd] md %ebp 1
[0]kdb> [defcmd] endefcmd

该命令的输出会是:

?
1
2
3
4
5
6
7
8
9
10
[0]kdb> hari
[hari]kdb> md 0xc000000 1
0xc000000 00000001 f000e816 f000e2c3 f000e816
[hari]kdb> rd
eax = 0x00000000 ebx = 0xc0105330 ecx = 0xc0466000 edx = 0xc0466000
....
...
[hari]kdb> md %ebp 1
0xc0467fbc c0467fd0 c01053d2 00000002 000a0200
[0]kdb>

 


5.5  技巧 #5 
可以使用 bph 和 bpha 命令(假如体系结构支持使用硬件寄存器)来应用读写断点。这意味着每当从某个特定地址读取数据或将数据写入该地址时,我们都可以对此进行控制。当调试数据/内存毁坏问题时这可能会极其方便,在这种情况中您可以用它来识别毁坏的代码/进程。 
示例 
每当将四个字节写入地址 0xc0204060 时就进入内核调试器:

?
1
[0]kdb> bph 0xc0204060 dataw 4

 

在读取从 0xc000000 开始的至少两个字节的数据时进入内核调试器:

?
1
[0]kdb> bph 0xc000000 datar 2

 


6  结束语

对于执行内核调试,KDB 是一个方便的且功能强大的工具。它提供了各种选项,并且使我们能够分析内存内容和数据结构。最妙的是,它不需要用另一台机器来执行调试。 

参考: 
Linux 内核调试器内幕 KDB入门指南 

十二  Kprobes

Kprobes 是 Linux 中的一个简单的轻量级装置,让您可以将断点插入到正在运行的内核之中。 Kprobes 提供了一个强行进入任何内核例程并从中断处理器无干扰地收集信息的接口。使用 Kprobes 可以 轻松地收集处理器寄存器和全局数据结构等调试信息。开发者甚至可以使用 Kprobes 来修改 寄存器值和全局数据结构的值。 
为完成这一任务,Kprobes 向运行的内核中给定地址写入断点指令,插入一个探测器。 执行被探测的指令会导致断点错误。Kprobes 钩住(hook in)断点处理器并收集调试信息。Kprobes 甚至可以单步执行被探测的指令。 

1  安装

要安装 Kprobes,需要从 Kprobes 主页下载最新的补丁。 打包的文件名称类似于 kprobes-2.6.8-rc1.tar.gz。解开补丁并将其安装到 Linux 内核: 

?
1
2
3
$tar -xvzf kprobes-2.6.8-rc1.tar.gz 
$cd /usr/src/linux-2.6.8-rc1 
$patch -p1 < ../kprobes-2.6.8-rc1-base.patch

Kprobes 利用了 SysRq 键,这个 DOS 时代的产物在 Linux 中有了新的用武之地。您可以在 Scroll Lock键左边找到 SysRq 键;它通常标识为 Print Screen。要为 Kprobes 启用 SysRq 键,需要安装 kprobes-2.6.8-rc1-sysrq.patch 补丁: 

?
1
$patch -p1 < ../kprobes-2.6.8-rc1-sysrq.patch

使用 make xconfig/ make menuconfig/ make oldconfig 配置内核,并 启用 CONFIG_KPROBES 和 CONFIG_MAGIC_SYSRQ标记。 编译并引导到新内核。您现在就已经准备就绪,可以插入 printk 并通过编写简单的 Kprobes 模块来动态而且无干扰地 收集调试信息。 

2  编写 Kprobes 模块

对于每一个探测器,您都要分配一个结构体 struct kprobe kp; (参考 include/linux/kprobes.h 以获得关于此数据结构的详细信息)。 

清单 9. 定义 pre、post 和 fault 处理器

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* pre_handler: this is called just before the probed instruction is
 * executed.
 */
int handler_pre(struct kprobe *p, struct pt_regs *regs) {
printk("pre_handler: p->addr=0x%p, eflags=0x%lx\n",p->addr,
regs->eflags);
return 0;
}
/* post_handler: this is called after the probed instruction is executed
 * (provided no exception is generated).
 */
void handler_post(struct kprobe *p, struct pt_regs *regs, unsigned long flags) {
printk("post_handler: p->addr=0x%p, eflags=0x%lx \n", p->addr,
regs->eflags);
}
/* fault_handler: this is called if an exception is generated for any
 * instruction within the fault-handler, or when Kprobes
 * single-steps the probed instruction.
 */
int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr) {
printk("fault_handler:p->addr=0x%p, eflags=0x%lx\n", p->addr,
regs->eflags);
return 0;
}

 


2.1  获得内核例程的地址 
在注册过程中,您还需要指定插入探测器的内核例程的地址。使用这些方法中的任意一个来获得内核例程 的地址: 

  1. 从 System.map 文件直接得到地址。

  2. 例如,要得到 do_fork 的地址,可以在命令行执行 $grep do_fork /usr/src/linux/System.map 。

  3. 使用 nm 命令。

  4. $nm vmlinuz |grep do_fork

  5. 从 /proc/kallsyms 文件获得地址。

  6. $cat /proc/kallsyms |grep do_fork

  7. 使用 kallsyms_lookup_name() 例程。

  8. 这个例程是在 kernel/kallsyms.c 文件中定义的,要使用它,必须启用 CONFIG_KALLSYMS 编译内核。kallsyms_lookup_name() 接受一个字符串格式内核例程名, 返回那个内核例程的地址。例如:kallsyms_lookup_name("do_fork");

然后在 init_moudle 中注册您的探测器: 

清单 10. 注册一个探测器

?
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
28
29
30
31
/* specify pre_handler address
 */
kp.pre_handler=handler_pre;
/* specify post_handler address
 */
kp.post_handler=handler_post;
/* specify fault_handler address
 */
kp.fault_handler=handler_fault;
/* specify the address/offset where you want to insert probe.
 * You can get the address using one of the methods described above.
 */
kp.addr = (kprobe_opcode_t *) kallsyms_lookup_name("do_fork");
/* check if the kallsyms_lookup_name() returned the correct value.
 */
if (kp.add == NULL) {
printk("kallsyms_lookup_name could not find address
for the specified symbol name\n");
return 1;
}
/* or specify address directly.
 * $grep "do_fork" /usr/src/linux/System.map
 * or
 * $cat /proc/kallsyms |grep do_fork
 * or
 * $nm vmlinuz |grep do_fork
 */
kp.addr = (kprobe_opcode_t *) 0xc01441d0;
/* All set to register with Kprobes
 */
       register_kprobe(&kp);

一旦注册了探测器,运行任何 shell 命令都会导致一个对 do_fork 的调用,您将可以在控制台上或者运行 dmesg 命令来查看您的 printk。做完后要记得注销探测器: 
unregister_kprobe(&kp); 
下面的输出显示了 kprobe 的地址以及 eflags 寄存器的内容: 

$tail -5 /var/log/messages 
Jun 14 18:21:18 llm05 kernel: pre_handler: p->addr=0xc01441d0, eflags=0x202 
Jun 14 18:21:18 llm05 kernel: post_handler: p->addr=0xc01441d0, eflags=0x196 

2.2  获得偏移量 
您可以在例程的开头或者函数中的任意偏移位置插入 printk(偏移量必须在指令范围之内)。 下面的代码示例展示了如何来计算偏移量。首先,从对象文件中反汇编机器指令,并将它们 保存为一个文件: 

?
1
$objdump -D /usr/src/linux/kernel/fork.o > fork.dis

其结果是: 

清单 11. 反汇编的 fork

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
000022b0 <do_fork>:
   22b0:       55                      push   %ebp
   22b1:       89 e5                   mov    %esp,%ebp
   22b3:       57                      push   %edi
   22b4:       89 c7                   mov    %eax,%edi
   22b6:       56                      push   %esi
   22b7:       89 d6                   mov    %edx,%esi
   22b9:       53                      push   %ebx
   22ba:       83 ec 38                sub    $0x38,%esp
   22bd:       c7 45 d0 00 00 00 00    movl   $0x0,0xffffffd0(%ebp)
   22c4:       89 cb                   mov    %ecx,%ebx
   22c6:       89 44 24 04             mov    %eax,0x4(%esp)
   22ca:       c7 04 24 0a 00 00 00    movl   $0xa,(%esp)
   22d1:       e8 fc ff ff ff          call   22d2 <do_fork+0x22>
   22d6:       b8 00 e0 ff ff          mov    $0xffffe000,%eax
   22db:       21 e0                   and    %esp,%eax
   22dd:       8b 00                   mov    (%eax),%eax

要在偏移位置 0x22c4 插入探测器,先要得到与例程的开始处相对的偏移量 0x22c4 - 0x22b0 = 0x14 ,然后将这个偏移量添加到 do_fork 的地址 0xc01441d0 + 0x14 。(运行 $cat /proc/kallsyms | grep do_fork 命令以获得 do_fork 的地址。) 
您还可以将 do_fork 的相对偏移量 0x22c4 - 0x22b0 = 0x14 添加到 kallsyms_lookup_name("do_fork"); 的输入,即:0x14 + kallsyms_lookup_name("do_fork"); 

2.3  转储内核数据结构 
现在,让我们使用修改过的用来转储数据结构的 Kprobe post_handler 来转储运行在系统上的所有作业的一些组成部分: 

清单 12. 用来转储数据结构的修改过的 Kprope post_handler

?
1
2
3
4
5
6
7
8
9
10
11
12
void handler_post(struct kprobe *p, struct pt_regs *regs, unsigned long flags) {
struct task_struct *task;
read_lock(&tasklist_lock);
for_each_process(task) {
printk("pid =%x task-info_ptr=%lx\n", task->pid,
task->thread_info);
printk("thread-info element status=%lx,flags=%lx, cpu=%lx\n",
task->thread_info->status, task->thread_info->flags,
task->thread_info->cpu);
}
read_unlock(&tasklist_lock);
}

这个模块应该插入到 do_fork 的偏移位置。 

清单 13. pid 1508 和 1509 的结构体 thread_info 的输出

?
1
2
3
4
5
$tail -10 /var/log/messages
Jun 22 18:14:25 llm05 kernel: thread-info element status=0,flags=0, cpu=1
Jun 22 18:14:25 llm05 kernel: pid =5e4 task-info_ptr=f5948000
Jun 22 18:14:25 llm05 kernel: thread-info element status=0,flags=8, cpu=0
Jun 22 18:14:25 llm05 kernel: pid =5e5 task-info_ptr=f5eca000


2.4  启用奇妙的 SysRq 键 
为了支持 SysRq 键,我们已经进行了编译。这样来启用它: 

?
1
$echo 1 > /proc/sys/kernel/sysrq

现在,您可以使用 Alt+SysRq+W 在控制台上或者到 /var/log/messages 中去查看所有插入的内核探测器。 

清单 14. /var/log/messages 显示出在 do_fork 插入了一个 Kprobe

?
1
2
3
Jun 23 10:24:48 linux-udp4749545uds kernel: SysRq : Show kprobes
Jun 23 10:24:48 linux-udp4749545uds kernel:
Jun 23 10:24:48 linux-udp4749545uds kernel: [<c011ea60>] do_fork+0x0/0x1de


3  使用 Kprobes 更好地进行调试

由于探测器事件处理器是作为系统断点中断处理器的扩展来运行,所以它们很少或者根本不依赖于系统 工具 —— 这样可以被植入到大部分不友好的环境中(从中断时间和任务时间到禁用的上下文间切换和支持 SMP 的代码路径)—— 都不会对系统性能带来负面影响。 
使用 Kprobes 的好处有很多。不需要重新编译和重新引导内核就可以插入 printk。为了进行调试可以记录 处理器寄存器的日志,甚至进行修改 —— 不会干扰系统。类似地,同样可以无干扰地记录 Linux 内核数据结构的日志,甚至 进行修改。您甚至可以使用 Kprobes 调试 SMP 系统上的竞态条件 —— 避免了您自己重新编译和重新引导的所有 麻烦。您将发现内核调试比以往更为快速和简单。 

参考: 
使用 Kprobes 调试内核 

本文参考: 
1  Linux内核设计与实现   P243 第十八章 调试 
3   linux内核调试指南

 

 

kdb:只能在汇编代码级进行调试;

  优点是不需要两台机器进行调试。

  gdb:在调试模块时缺少一些至关重要的功能,它可用来查看内核的运行情况,包括反汇编内核函数。

  kgdb:能很方便的在源码级对内核进行调试,缺点是kgdb只能进行远程调试,它需要一根串口线及两台机器来调试内核(也可以是在同一台主机上用vmware软件运行两个操作系统来调试)

printk() 是调试内核代码时最常用的一种技术。在内核代码中的特定位置加入printk() 调试调用,可以直接把所关心的信息打打印到屏幕上,从而可以观察程序的执行路径和所关心的变量、指针等信息。 Linux 内核调试器(Linux kernel debugger,kdb)是 Linux 内核的补丁,它提供了一种在系统能运行时对内核内存和数据结构进行检查的办法。Oops、KDB在文章掌握 Linux 调试技术有详细介绍,大家可以参考。 Kprobes 提供了一个强行进入任何内核例程,并从中断处理器无干扰地收集信息的接口。使用 Kprobes 可以轻松地收集处理器寄存器和全局数据结构等调试信息,而无需对Linux内核频繁编译和启动,具体使用方法,请参考使用 Kprobes 调试内核。

/proc文件系统

在 /proc 文件系统中,对虚拟文件的读写操作是一种与内核通信的手段,要查看内核回环缓冲区中的消息,可以使用 dmesg 工具(或者通过 /proc 本身使用 cat /proc/kmsg 命令)。清单 6 给出了 dmesg 显示的最后几条消息。

清单 6. 查看来自 LKM 的内核输出

[root@plato]# dmesg | tail -5
cs: IO port probe 0xa00-0xaff: clean.
eth0: Link is down
eth0: Link is up, running at 100Mbit half-duplex
my_module_init called.  Module is now loaded.
my_module_cleanup called.  Module is now unloaded.


可以在内核输出中看到这个模块的消息。现在让我们暂时离开这个简单的例子,来看几个可以用来开发有用 LKM 的内核 API。

调试工具

  使用调试器来一步步地跟踪代码,查看变量和计算机寄存器的值。在内核中使用交互式调试器是一个很复杂的问题。内核在它自己的地址空间中运行。许多用户空间下的调试器所提供的常用功能很难用于内核之中,比如断点和单步调试等。

目录

[隐藏]

内核bug跟踪

oops消息分析

(1)oops消息产生机制

oops(也称 panic),称程序运行崩溃,程序崩溃后会产生oops消息。应用程序或内核线程的崩溃都会产生oops消息,通常发生oops时,系统不会发生死机,而在终端或日志中打印oops信息。

当使用NULL指针或不正确的指针值时,通常会引发一个 oops 消息,这是因为当引用一个非法指针时,页面映射机制无法将虚拟地址映像到物理地址,处理器就会向操作系统发出一个"页面失效"的信号。内核无法"换页"到并不存在的地址上,系统就会产生一个"oops"。

oops 显示发生错误时处理器的状态,包括 CPU 寄存器的内容、页描述符表的位置,以及其一些难理解的信息。这些消息由失效处理函数(arch/*/kernel/traps.c)中的printk 语句产生。较为重要的信息就是指令指针(EIP),即出错指令的地址。

由于很难从十六进制数值中看出含义,可使用符号解析工具klogd。klogd 守护进程能在 oops 消息到达记录文件之前对它们解码。klogd在缺省情况下运行并进行符号解码。

通常Oops文本由klogd从内核缓冲区里读取并传给syslogd,由syslogd写到syslog文件中,该文件典型为/var/log/messages(依赖于/etc/syslog.conf)。如果klogd崩溃了,用户可"dmesg > file"从内核缓冲区中读取数据并保存下来。还可用"cat /proc/kmsg > file"读取数据,此时,需要用户中止传输,因为kmsg是一个"永不结束的文件"。

当保护错误发生时,klogd守护进程自动把内核日志信息中的重要地址翻译成它们相应的符号。klogd执行静态地址翻译和动态地址翻译。静态地址翻译使用System.map文件将符号地址翻译为符号。klogd守护进程在初始化时必须能找到system.map文件。

动态地址翻译通常对内核模块中的符号进行翻译。内核模块的内存从内核动态内存池里分配,内核模块中符号的位置在内核装载后才最终确定。

Linux内核提供了调用,允许程序决定装载哪些模块和它们在内存中位置。通过这些系统调用,klogd守护进程生成一张符号表用于调试发生在可装载模块中的保护错误。内核模块的装载或者卸载都会自动向klogd发送信号,klogd可将内核模块符号的地址动态翻译为符号字符串。

(2)产生oops的样例代码

使用空指针和缓冲区溢出是产生oops的两个最常见原因。下面两个函数faulty_write和faulty_read是一个内核模块中的写和读函数,分别演示了这两种情况。当内核调用这两个函数时,会产生oops消息。

函数faulty_write删除一个NULL指针的引用,由于0不是一个有效的指针值,内核将打印oops信息,并接着,杀死调用些函数的进程。
ssize_t faulty_write (struct file *filp, const char _ _user *buf, size_t count, loff_t *pos)
{
    /* make a simple fault by dereferencing a NULL pointer */
    *(int *)0 = 0;
    return 0;
}

函数faulty_write产生oops信息列出如下(注意 EIP 行和 stack 跟踪记录中已经解码的符号):
Unable to handle kernel NULL pointer dereference at virtual address \
   00000000 

printing eip: c48370c3 *pde = 00000000 Oops: 0002 CPU: 0 EIP: 0010:[faulty:faulty_write+3/576] EFLAGS: 00010286 eax: ffffffea ebx: c2c55ae0 ecx: c48370c0 edx: c2c55b00 esi: 0804d038 edi: 0804d038 ebp: c2337f8c esp: c2337f8c ds: 0018 es: 0018 ss: 0018 Process cat (pid: 23413, stackpage=c2337000) Stack: 00000001 c01356e6 c2c55ae0 0804d038 00000001 c2c55b00 c2336000 \

          00000001 
     0804d038 bffffbd4 00000000 00000000 bffffbd4 c010b860 00000001 \ 
          0804d038 
     00000001 00000001 0804d038 bffffbd4 00000004 0000002b 0000002b \ 
          00000004 

Call Trace: [sys_write+214/256] [system_call+52/56]

Code: c7 05 00 00 00 00 00 00 00 00 31 c0 89 ec 5d c3 8d b6 00 00

上述oops消息中,字符串 3/576 表示处理器正处于函数的第3个字节上,函数整体长度为 576 个字节。 函数faulty_read拷贝一个字符串到本地变量,由于字符串比目的地数组长造成缓冲区溢出。当函数返回时,缓冲区溢出导致产生oops信息。因为返回指令引起指令指针找不到运行地址,这种错误很难发现和跟踪。
ssize_t faulty_read(struct file *filp, char _ _user *buf, size_t count, loff_t *pos)
{
    int ret;
    char stack_buf[4];
    /* Let's try a buffer overflow */
    memset(stack_buf, 0xff, 20);
    if (count > 4)
        count = 4;
    /* copy 4 bytes to the user */
    ret = copy_to_user(buf, stack_buf, count);
    if (!ret)
        return count;
    return ret;
}

函数faulty_read产生oops信息列出如下:
EIP: 0010:[<00000000>]

Unable to handle kernel paging request at virtual address ffffffff printing eip: ffffffff Oops: 0000 [#5] SMP CPU: 0 EIP: 0060:[] Not tainted EFLAGS: 00010296 (2.6.6) EIP is at 0xffffffff eax: 0000000c ebx: ffffffff ecx: 00000000 edx: bfffda7c esi: cf434f00 edi: ffffffff ebp: 00002000 esp: c27fff78 ds: 007b es: 007b ss: 0068 Process head (pid: 2331, threadinfo=c27fe000 task=c3226150) Stack: ffffffff bfffda70 00002000 cf434f20 00000001 00000286 cf434f00 fffffff7 bfffda70 c27fe000 c0150612 cf434f00 bfffda70 00002000 cf434f20 00000000 00000003 00002000 c0103f8f 00000003 bfffda70 00002000 00002000 bfffda70 Call Trace: [] sys_read+0x42/0x70 [] syscall_call+0x7/0xb

Code: Bad EIP value.

在上述oops消息中,由于缓冲区溢出,仅能看到函数调用栈的一部分,看不见函数名vfs_read和faulty_read,并且代码(Code)处仅输出"bad EIP value.",列在栈上开始处的地址"ffffffff"表示内核栈已崩溃。

(3)oops信息分析

面对产生的oops信息,首先应查找源程序发生oops的位置,通过查看指令指令寄存器EIP的值,可以找到位置,如:EIP: 0010:[faulty:faulty_write+3/576]。

再查找函数调用栈(call stack)可以得到更多的信息。从函数调用栈可辨别出局部变量、全局变量和函数参数。例如:在函数faulty_read的oops信息的函数调用栈中,栈顶为ffffffff,栈顶值应为一个小于ffffffff的值,为此值,说明再找不回调用函数地址,说明有可能因缓冲区溢出等原因造成指针错误。

在x86构架上,用户空间的栈从0xc0000000以下开始,递归值bfffda70可能是用户空间的栈地址。实际上它就是传递给read系统调用的缓冲区地址,系统调用read进入内核时,将用户空间缓冲区的数据拷贝到内核空间缓冲区。

如果oops信息显示触发oops的地址为0xa5a5a5a5,则说明很可能是因为没有初始化动态内存引起的。

另外,如果想看到函数调用栈的符号,编译内核时,请打开CONFIG_KALLSYMS选项。

klogd 提供了许多信息来帮助分析。为了使 klogd 正确地工作,必须在 /boot 中提供符号表文件 System.map。如果符号表与当前内核不匹配,klogd 就会拒绝解析符号。

有时内核错误会将系统完全挂起。例如代码进入一个死循环,系统不会再响应任何动作。这时可通过在一些关键点上插入 schedule 调用可以防止死循环。

系统崩溃重启动

由于内核运行错误,在某些极端情况下,内核会运行崩溃,内核崩溃时会导致死机。为了解决此问题,内核引入了快速装载和重启动新内核机制。内核通过kdump在崩溃时触发启动新内核,存储旧内存映像以便于调试,让系统在新内核上运行 ,从而避免了死机,增强了系统的稳定性。

(1)工具kexec介绍

kexec是一套系统调用,允许用户从当前正执行的内核装载另一个内核。用户可用shell命令"yum install kexec-tools"安装kexec工具包,安装后,就可以使用kexec命令。

工具kexec直接启动进入一个新内核,它通过系统调用使用户能够从当前内核装载并启动进入另一个内核。在当前内核中,kexec执行BootLoader的功能。在标准系统启动和kexec启动之间的主要区别是:在kexec启动期间,依赖于硬件构架的固件或BIOS不会被执行来进行硬件初始化。这将大大降低重启动的时间。

为了让内核的kexec功能起作用,内核编译配置是应确认先择了"CONFIG_KEXEC=y",在配置后生成的.config文件中应可看到此条目。

工具kexec的使用分为两步,首先,用kexec将调试的内核装载进内存,接着,用kexec启动装载的内核。

装载内核的语法列出如下:

kexec -l kernel-image --append=command-line-options --initrd=initrd-image

上述命令中,参数kernel-image为装载内核的映射文件,该命令不支持压缩的内核映像文件bzImage,应使用非压缩的内核映射文件vmlinux;参数initrd-image为启动时使用initrd映射文件;参数command-line-options为命令行选项,应来自当前内核的命令行选项,可从文件"/proc/cmdline"中提取,该文件的内容列出如下:

^-^$ cat /proc/cmdline

ro root=/dev/VolGroup00/LogVol00 rhgb quiet

例如:用户想启动的内核映射为/boot/vmlinux,initrd为/boot/initrd,则kexec加载命令列出如下:

Kexec –l /boot/vmlinux –append=/dev/VolGroup00/LogVol00 initrd=/boot/initrd

还可以加上选项-p或--load-panic,表示装载新内核在系统内核崩溃使用。

在内核装载后,用下述命令启动装载的内核,并进行新的内核中运行:

kexec -e

当kexec将当前内核迁移到新内核上运行时,kexec拷贝新内核到预保留内存块,该保留位置如图1所示, 原系统内核给kexec装载内核预保留一块内存(在图中的阴影部分),用于装载新内核,其他内存区域在未装载新内核时,由原系统内核使用。

Linux内核调试方法总结
    




Linux内核调试方法

图1 kexec装载的内核所在预保留位置示意图

在x86构架的机器上,系统启动时需要使用第一个640KB物理内存,用于内核装载,kexec在重启动进入转储捕捉的内核之前备份此区域。相似地,PPC64构架的机器在启动里需要使用第一个32KB物理内核,并需要支持64K页,kexec备份第一个64KB内存。

(2)kdump介绍

kdump是基于kexec的崩溃转储机制(kexec-based Crash Dumping),无论内核内核需要转储时,如:系统崩溃时,kdump使用kexec快速启动进入转储捕捉的内核。在这里,原运行的内核称为系统内核或原内核,新装载运行的内核称为转储捕捉的内核或装载内核或新内核。

在重启动过程中,原内核的内存映像被保存下来,并且转储捕捉的内核(新装载的内核)可以访问转储的映像。用户可以使用命令cp和scp将内存映射拷贝到一个本地硬盘上的转储文件或通过网络拷贝到远程计算机上。

当前仅x86, x86_64, ppc64和ia64构架支持kdump和kexec。

当系统内核启动时,它保留小部分内存给转储(dump)捕捉的内核,确保了来自系统内核正进行的直接内存访问(Direct Memory Access:DMA)不会破坏转储捕捉的内核。命令kexec –p装载新内核到这个保留的内存。

在崩溃前,所有系统内核的核心映像编码为ELF格式,并存储在内核的保留区域。ELF头的开始物理地址通过参数elfcorehdr=boot传递到转储捕捉的内核。

通过使用转储捕捉的内核,用户可以下面两种方式访问内存映像或旧内存:

(1)通过/dev/oldmem设备接口,捕捉工具程序能读取设备文件并以原始流的格式写出内存,它是一个内存原始流的转储。分析和捕捉工具必须足够智能以判断查找正确信息的位置。

(2)通过/proc/vmcore,能以ELF格式文件输出转储信息,用户可以用GDB(GNU Debugger)和崩溃调试工具等分析工具调试转储文件。

(3)建立快速重启动机制和安装工具

1)安装工具kexec-tools

可以下载源代码编译安装工具kexec-tools。由于工具kexec-tools还依赖于一些其他的库,因此,最好的方法是使用命令"yum install kexec-tools"从网上下载安装并自动解决依赖关系。

2)编译系统和转储捕捉的内核

可编译独立的转储捕捉内核用于捕捉内核的转储,还可以使用原系统内核作为转储捕捉内核,在这种情况下,不需要再编译独立的转储捕捉内核,但仅支持重定位内核的构架才可以用作转储捕捉的内核,如:构架i386和ia64支持重定位内核。

对于系统和转储捕捉内核来说,为了打开kdump支持,内核需要设置一些特殊的配置选项,下面分别对系统内核和转储捕捉内核的配置选项进行说明:

系统内核的配置选项说明如下:

  • 在菜单条目"Processor type and features."中打开选项"kexec system call",使内核编译安装kexe系统调用。配置文件.config生成语句"CONFIG_KEXEC=y"。
  • 在菜单条目"Filesystem"->"Pseudo filesystems."中打开选项"sysfs file system support",使内核编译安装文件系统sysfs.配置文件.config生成语句"CONFIG_SYSFS=y"。
  • 在菜单条目"Kernel hacking."中打开选项"Compile the kernel with debug info ",使内核编译安装后支持调试信息输出,产生调试符号用于分析转储文件。配置文件.config生成语句"CONFIG_DEBUG_INFO=Y"。

转储捕捉内核配置选项(不依赖于处理器构架)说明如下:

  • 在菜单条目"Processor type and features"中打开选项"kernel crash dumps",配置文件.config生成语句" CONFIG_CRASH_DUMP=y"。
  • 在菜单条目"Filesystems"->"Pseudo filesystems"中打开选项"/proc/vmcore support",配置文件.config生成语句"CONFIG_PROC_VMCORE=y"。

转储捕捉内核配置选项(依赖于处理器构架i386和x86_64)说明如下:

  • 在处理器构架i386上,在菜单条目"Processor type and features"中打开高端内存支持,配置文件.config生成语句"CONFIG_HIGHMEM64G=y"或"CONFIG_HIGHMEM4G"。
  • 在处理器构架i386和x86_64上,在菜单条目"rocessor type and features"中关闭对称多处理器支持,配置文件.config生成语句"CONFIG_SMP=n"。如果配置文件中的设置为"CONFIG_SMP=y",则可在装载转储捕捉内核的内核命令行上指定"maxcpus=1"。
  • 如果想构建和使用可重定位内核,在菜单条目"rocessor type and featuresIf"中打开选项"Build a relocatable kernel",配置文件.config生成语句"CONFIG_RELOCATABLE=y"。
  • 在菜单"Processor type and features"下的条目"Physical address where the kernel is loaded"设置合适的值用于内核装载的物理地址。它仅在打开了"kernel crash dumps"时出现。合适的值依赖于内核是否可重定位。

如果设置了值"CONFIG_PHYSICAL_START=0x100000",则表示使用可重定位内核。它将编译内核在物理地址1MB处,内核是可重定位的,因此,内核可从任何物理地址运行。Kexec BootLoader将装载内核到用于转储捕捉内核的内核保留区域。

否则,将使用启动参数"crashkernel=Y@X"指定第二个内核保留内核区域的开始地址,其中,Y表示内存区域的大小,X表示保留给转储捕捉内核的内存区域的开始地址,通过X为16MB (0x1000000),因此用户可设置"CONFIG_PHYSICAL_START=0x1000000"。

在配置完内核后,编译和安装内核及内核模块。

3)扩展的crashkernel语法

在系统内核的启动命令行选项中,通常语法"crashkernel=size[@offset]"对于大多数据配置已够用了,但有时候保留的内存依赖于系统RAM。此时可通过扩展的crashkernel命令行对内存进行 限制避免从机器上移去一部分内核后造成系统不可启动。扩展的crashkernel语法列出如下:

crashkernel=<range1>:<size1>[,<range2>:<size2>,...][@offset]

其中,range=start-[end]。

例如:crashkernel=512M-2G:64M,2G-:128M,含义为:如果内存小于512M,不设置保留内存,如果内存为512M到2G之间,设置保留内存区域为64M,如果内存大于128M,设置保留内存区域为128M。

4)启动进入系统内核

必要时更新BootLoader。然后用参数"crashkernel=Y@X"启动系统内核,如:crashkernel=64M@16M,表示告诉系统内核保留从物理地址0x01000000 (16MB)开始的64MB大小给转储捕捉内核使用。通常x86和x86_64平台设置"crashkernel=64M@16M",ppc64平台设置"crashkernel=128M@32M"。

5)装载转储捕捉内核

在启动进入系统内核后,需要装载转储捕捉内核。根据处理器构架和映射文件的类型(可否重定位),可以选择装载不压缩的vmlinux或压缩的bzImage/vmlinuz内核映像。选择方法说明如下:

对于i386和x86_64平台:

  • 如果内核不是可重定位的,使用vmlinux。
  • 如果内核是可重定位的,使用bzImage/vmlinuz。

对于ppc64平台:

  • 使用vmlinux。

对于ia64平台:

  • 使用vmlinux或vmlinuz.gz。
如果用户使用不压缩的vmlinux映像,那么使用下面的命令装载转储捕捉内核:
kexec -p <dump-capture-kernel-vmlinux-image> \
   --initrd=<initrd-for-dump-capture-kernel> --args-linux \
   --append="root=<root-dev> <arch-specific-options>"

如果用户使用压缩的bzImage/vmlinuz映像,那么使用下面的命令装载转储捕捉内核:
kexec -p <dump-capture-kernel-bzImage>\
  --initrd=<initrd-for-dump-capture-kernel> \
   --append="root=<root-dev> <arch-specific-options>"

注意:参数--args-linux在ia64平台中不用指定。

下面是在装载转储捕捉内核时使用的构架特定命令行选项:

  • 对于i386, x86_64和ia64平台,选项为"1 irqpoll maxcpus=1 reset_devices"。
  • 对于ppc64平台,选项为"1 maxcpus=1 noirqdistrib reset_devices"。

在装载转储捕捉内核时需要注意的事项说明如下:

  • 缺省设置下,ELF头以ELF64格式存储,以支持多于4GB内核的系统,在i386上,kexec自动检查物理RAM尺寸是否超过4GB限制,如果没有超过,使用ELF32。因此,在非PAE系统上ELF头总是使用ELF32格式。
  • 选项--elf32-core-headers可用于强制产生ELF32头,这是必要的,因为在32位系统上,GDB当前不能打开带有ELF64头的vmcore文件。
  • 在转储捕捉内核中,启动参数irqpoll减少了由于共享中断引起的驱动程序初始化失败。
  • 用户必须以命令mount输出的根设备名的格式指定<root-dev>。
  • 启动参数"1"将转储捕捉内核启动进入不支持网络的单用户模式。如果用户想使用网络,需要设置为3。
  • 通常不必让转储捕捉内核以SMP方式运行。因此,通常编译一个单CPU转储捕捉内核或装载转储捕捉内核时指定选项"maxcpus=1"。

6)内核崩溃时触发内核启动

在装载转储捕捉内核后,如果系统发生崩溃(Kernel Panic),系统将重启动进入转储捕捉内核。重启动的触发点在函数die(), die_nmi()和sysrq处理例程(按ALT-SysRq-c组合键)。

下面条件将执行一个崩溃触发点:

  • 如果检测到硬件锁住,并且配置了"NMI watchdog",系统将调用函数die_nmi()启动进入转储捕捉内核。
  • 如果调用了函数die(),并且该线程的pid为0或1,或者在中断上下文中调用die(),或者设置了panic_on_oops并调用了die(),系统将启动进入转储捕捉内核。
  • 在powerpc系统,当一个软复位产生时,所有的CPU调用die(),并且系统将启动进入转储捕捉内核。
  • 为了测试目的,用户可以使用"ALT-SysRq-c","echo c > /proc/sysrq-trigger"触发一个崩溃,或者写一个内核模块强制内核崩溃。

7)写出转储文件

在转储捕捉内核启动后,可用下面的命令写出转储文件:

cp /proc/vmcore <dump-file>

用户还可以将转储内存作为设备/dev/oldmem以线性原始流视图进行访问,使用下面的命令创建该设备:

mknod /dev/oldmem c 1 12

使用命令dd拷贝转储内存的特定部分,拷贝整个内存的命令列出如下:

dd if=/dev/oldmem of=oldmem.001

8)转储文件分析

在分析转储映像之前,用户应重启动进入一个稳定的内核。用户可以用GDB对拷贝出的转储进行有限分析。编译vmlinux时应加上-g选项,才能生成调试用的符号,然后,用下面的命令调试vmlinux:

gdb vmlinux <dump-file>

SysRq魔术组合键打印内核信息

SysRq"魔术组合键"是一组按键,由键盘上的"Alt+SysRq+[CommandKey]"三个键组成,其中CommandKey为可选的按键。SysRq魔术组合键根据组合键的不同,可提供控制内核或打印内核信息的功能。SysRq魔术组合键的功能说明如表1所示。

表1 SysRq组合键的功能说明
键名 功能说明
b 在没有同步或卸载硬盘的情况下立即启动。
c 为了获取崩溃转储执行kexe重启动。
d 显示被持的所有锁。
e 发送信号SIGTERM给所有进程,除了init外。
f 将调用oom_kill杀死内存热进程。
g 在平台ppc和sh上被kgdb使用。
h 显示帮助信息。
i 发送信号SIGKILL给所有的进程,除了init外。
k 安全访问密钥(Secure Access Key,SAK)杀死在当前虚拟终端上的所有程序。
m 转储当前的内存信息到控制台。
n 用于设置实时任务为可调整nice的。
o 将关闭系统(如果配置为支持)。
p 打印当前寄存器和标识到控制台。
q 将转储所有正运行定时器的列表。
r 关闭键盘Raw模式并设置为XLATE模式。
s 尝试同步所有挂接的文件系统。
t 将转储当前的任务列表和它们的信息到控制台。
u 尝试以仅读的方式重挂接所有已挂接的文件系统。
v 转储Voyager SMP处理器信息到控制台。
w 转储的所有非可中断(已阻塞)状态的任务。
x 在平台ppc/powerpc上被xmon(X监视器)接口使用。
0~9 设备控制台日志级别,控制将打印到控制台的内核信息。例如:0仅打印紧急信息,如:PANIC和OOPS信息。

默认SysRq组合键是关闭的。可用下面的命令打开此功能:

# echo 1 > /proc/sys/kernel/sysrq

关闭此功能的命令列出如下:

# echo 0 > /proc/sys/kernel/sysrq

如果想让此功能总是起作用,可在/etc/sysctl.conf文件中设置kernel.sysrq值为1。 系统重新启动以后,此功能将会自动打开。

打开SysRq组合键功能后,有终端访问权限的用户就可以自用它打印内核信息了。

注意:SysRq组合键在X windows上是无法使用的。必须先要切换到文本虚拟终端下。如果在图形界面,可以按Ctrl+Alt+F1切换到虚拟终端。在串口终端上,需要先在终端上发送Break信号,然后在5秒内输入sysrq组合键。如果用户有root权限,可把commandkey字符写入到/proc/sysrq-trigger文件,触发一个内核信息打印,打印的信息存放在/var/log/messages中。下面是一个命令样例:
^-^$ echo 't' > sysrq-trigger
^-^vim /var/log/messages
Oct 29 17:51:43 njllinux kernel: SysRq : Show State
Oct 29 17:51:43 njllinux kernel:  task                        PC stack   pid father
Oct 29 17:51:43 njllinux kernel: init          S ffffffff812b76a0     0     1      0
Oct 29 17:51:43 njllinux kernel: ffff81013fa97998 0000000000000082 0000000000000000 ffff81013fa9795c
Oct 29 17:51:43 njllinux kernel: 000000003fa97978 ffffffff81583700 ffffffff81583700 ffff81013fa98000
Oct 29 17:51:43 njllinux kernel: ffffffff813cc5b0 ffff81013fa98350 000000003c352a50 ffff81013fa98350
Oct 29 17:51:43 njllinux kernel: Call Trace:
Oct 29 17:51:43 njllinux kernel: 000300000004 ffff8101333cb090
Oct 29 17:51:43 njllinux kernel: Call Trace:
Oct 29 17:51:43 njllinux kernel: [<ffffffff81040c2e>] sys_pause+0x19/0x22
Oct 29 17:51:43 njllinux kernel: [<ffffffff8100c291>] tracesys+0xd0/0xd5
Oct 29 17:51:43 njllinux kernel:
Oct 29 17:51:43 njllinux kernel: lighttpd      S ffffffff812b76a0     0  3365      1
Oct 29 17:51:43 njllinux kernel: ffff810132d49b18 0000000000000082 0000000000000000 ffff810132d49adc
Oct 29 17:51:43 njllinux kernel: ffff81013fb2d148 ffffffff81583700 ffffffff81583700 ffff8101354896a0
Oct 29 17:51:43 njllinux kernel: ffffffff813cc5b0 ffff8101354899f0 0000000032d49ac8 ffff8101354899f0
Oct 29 17:51:43 njllinux kernel: Call Trace:
Oct 29 17:51:43 njllinux kernel: [<ffffffff81040722>] ? __mod_timer+0xbb/0xcd
Oct 29 17:51:43 njllinux kernel: [<ffffffff8129b2ee>] schedule_timeout+0x8d/0xb4
Oct 29 17:51:43 njllinux kernel: [<ffffffff81040100>] ? process_timeout+0x0/0xb
Oct 29 17:51:43 njllinux kernel: [<ffffffff8129b2e9>] ? schedule_timeout+0x88/0xb4
Oct 29 17:51:43 njllinux kernel: [<ffffffff810b9498>] do_sys_poll+0x2a8/0x370
……

命令strace

命令strace 显示程序调用的所有系统调用。使用 strace 工具,用户可以清楚地看到这些调用过程及其使用的参数,了解它们与操作系统之间的底层交互。当系统调用失败时,错误的符号值(如 ENOMEM)和对应的字符串(如Out of memory)都能被显示出来。

starce 的另一个用处是解决和动态库相关的问题。当对一个可执行文件运行ldd时,它会告诉你程序使用的动态库和找到动态库的位置

strace命令行选项说明如表1。常用的选项为-t, -T, -e, -o等。

表1 命令strace的命令行选项说明
选项 说明
-c 统计每个系统调用执行的时间、次数和出错的次数等。
-d 输出一些strace自身的调试信息到标准输出。
-f 跟踪当前进程由系统调用fork产生的子进程。
-ff 如果使用选项-o filename,则将跟踪结果输出到相应的filename.pid中,pid是各进程的进程号。
-F 尝试跟踪vfork调用.在-f时,vfork不被跟踪。
-h 输出简要的帮助信息。
-i 在系统调用的时候打印指令指针。
-q 禁止输出关于粘附和脱离的信息,发生在输出重定向到文件且直接而不是粘附运行命令时。
-r 依赖于每个系统调用的入口打印相对时间戳。
-t 在输出中的每一行前加上时间信息。
-tt 在输出中的每一行前加上时间信息,包括毫秒。
-ttt 毫秒级输出,以秒表示时间。
-T 显示系统调用所花费的时间。
-v 输出所有的系统调用的信息。一些关于环境变量,状态,输入输出等调用由于使用频繁,默认不输出。
-V 输出strace的版本信息。
-x 以十六进制形式输出非ASCII标准字符串。
-xx 所有字符串以十六进制形式输出。
-a column 以特定的列数对齐返回值,缺省值为40。
-e expr 指定一个表达式,用来控制如何跟踪.格式如下:
[qualifier=][!]value1[,value2]...
qualifier只能是 trace,abbrev,verbose,raw,signal,read,write其中之一。value是用来限定的符号或数字。默认的qualifier是 trace。感叹号是否定符号。
-eopen 等价于 -e trace=open,表示只跟踪open调用。而-etrace!=open表示跟踪除了open以外的其他调用。
-e trace=set 只跟踪指定的系统调用。例如:-e trace=open,close,rean,write表示只跟踪这四个系统调用。默认的为set=all。
-e trace=file 只跟踪文件名作为参数的系统调用,一般为文件操作。
-e trace=process 只跟踪有关进程控制的系统调用。
-e trace=network 只跟踪与网络有关的所有系统调用。
-e strace=signal 跟踪所有与系统信号有关的系统调用。
-e trace=ipc 跟踪所有与进程间通信有关的系统调用。
-o filename 将strace的输出写入文件filename。
-p pid 跟踪指定的进程pid。
-s strsize 指定最大字符串打印长度,默认值为32。
-u username 以username的UID和GID执行命令。
例如:命令strace pwd的输出部分列出如下:
execve("/bin/pwd", ["pwd"], [/* 39 vars */]) = 0
uname({sys="Linux", node="sammy", ...}) = 0
brk(0)                                  = 0x804c000
old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x4001...
	fstat64(3, {st_mode=S_IFREG|0644, st_size=115031, ...}) = 0
old_mmap(NULL, 115031, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40017000
close(3)                                = 0
open("/lib/tls/libc.so.6", O_RDONLY)    = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\360U\1"..., 1024) = 1024
fstat64(3, {st_mode=S_IFREG|0755, st_size=1547996, ...}) = 0

用函数printk打印内核信息

Linux内核用函数printk打印调试信息,该函数的用法与C库打印函数printf格式类似,但在内核使用。用户可在内核代码中的某位置加入函数printk,直接把所关心的信息打打印到屏幕上或日志文件中。

函数printk根据日志级别(loglevel)对调试信息进行分类。日志级别用宏定义,展开为一个字符串,在编译时由预处理器将它和消息文本拼接成一个字符串,因此函数printk中的日志级别和格式字符串间不能有逗号。

下面两个 printk 的例子,一个是调试信息,一个是临界信息:
printk(KERN_DEBUG "Here I am: %s:%i\n", _ _FILE_ _, _ _LINE_ _); 
printk(KERN_CRIT "I'm trashed; giving up on %p\n", ptr);

样例:在用户空间或内核中开启及关闭打印调试消息 用户还可以在内核或用户空间应用程序定义统一的函数打印调试信息,可在Makefile文件中打开或关闭调试函数。定义方法列出如下:
/*debug_on_off.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

在文件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" 
else 
 DEBFLAGS = -O2 
endif 
 
CFLAGS += $(DEBFLAGS)

更改makefile中的DEBUG值,需要调试信息时,DEBUG = y,不需要时,DEBUG赋其它值。再用make编译即可。

内核探测kprobe

kprobe(内核探测,kernel probe)是一个动态地收集调试和性能信息的工具,如:收集寄存器和全局数据结构等调试信息,无需对Linux内核频繁编译和启动。用户可以在任何内核代码地址进行陷阱,指定调试断点触发时的处理例程。工作机制是:用户指定一个探测点,并把用户定义的处理函数关联到该探测点,当内核执行到该探测点时,相应的关联函数被执行,然后继续执行正常的代码路径。

kprobe允许用户编写内核模块添加调试信息到内核。当在远程机器上调试有bug的程序而日志/var/log/messages不能看出错误时,kprobe显得非常有用。用户可以编译一个内核模块,并将内核模块插入到调试的内核中,就可以输出所需要的调试信息了。

内核探测分为kprobe, jprobe和kretprobe(也称return probe,返回探测)三种。kprobe可插入内核中任何指令处;jprobe插入内核函数入口,方便于访问函数的参数;return probe用于探测指定函数的返回值。

内核模块的初始化函数init安装(或注册)了多个探测函数,内核模块的退出函数exit将注销它们。注册函数(如:register_kprobe())指定了探测器插入的地方、探测点触发的处理例程。

(1)配置支持kprobe的内核

配置内核时确信在.config文件中设置了CONFIG_KPROBES、CONFIG_MODULES、CONFIG_MODULE_UNLOAD、CONFIG_KALLSYMS_ALL和CONFIG_DEBUG_INFO。

配置了CONFIG_KALLSYMS_ALL,kprobe可用函数kallsyms_lookup_name从地址解析代码。配置了CONFIG_DEBUG_INFO后,可以用命令"objdump -d -l vmlinux"查看源到对象的代码映射。

调试文件系统debugfs含有kprobe的调试接口,可以查看注册的kprobe列表,还可以关闭/打开kprobe。

查看系统注册probe的方法列出如下:

#cat /debug/kprobes/list
c015d71a  k  vfs_read+0x0
c011a316  j  do_fork+0x0
c03dedc5  r  tcp_v4_rcv+0x0

第一列表示探测点插入的内核地址,第二列表示内核探测的类型,k表示kprobe,r表示kretprobe,j表示jprobe,第三列指定探测点的"符号+偏移"。如果被探测的函数属于一个模块,模块名也被指定。

打开和关闭kprobe的方法列出如下:

#echo ‘1’ /debug/kprobes/enabled
#echo ‘0’ /debug/kprobes/enabled

(2)kprobe样例

Linux内核源代码在目录samples/kpobges下提供了各种kprobe类型的探测处理例程编写样例,分别对应文件kprobe_example.c、jprobe_example.c和kretprobe_example.c,用户稍加修改就可以变成自己的内核探测模块。下面仅说明kprobe类型的探测例程。

样例kprobe_example是kprobe类型的探测例程内核模块,显示了在函数do_fork被调用时如何使用kprobe转储栈和选择的寄存器。当内核函数do_fork被调用创建一个新进程时,在控制台和/var/log/messages中将显示函数printk打印的跟踪数据。样例kprobe_example列出如下(在samples/kprobe_example.c中):

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>
 
/* 对于每个探测,用户需要分配一个kprobe对象*/
static struct kprobe kp = {
	.symbol_name	= "do_fork",
};
 
/* 在被探测指令执行前,将调用预处理例程 pre_handler,用户需要定义该例程的操作*/
static int handler_pre(struct kprobe *p, struct pt_regs *regs)
{
#ifdef CONFIG_X86
	printk(KERN_INFO "pre_handler: p->addr = 0x%p, ip = %lx,"
			" flags = 0x%lx\n",
		p->addr, regs->ip, regs->flags);  /*打印地址、指令和标识*/
#endif
#ifdef CONFIG_PPC
	printk(KERN_INFO "pre_handler: p->addr = 0x%p, nip = 0x%lx,"
			" msr = 0x%lx\n",
		p->addr, regs->nip, regs->msr);
#endif
 
	/* 在这里可以调用内核接口函数dump_stack打印出栈的内容*/
	return 0;
}
 
/* 在被探测指令执行后,kprobe调用后处理例程post_handler */
static void handler_post(struct kprobe *p, struct pt_regs *regs,
				unsigned long flags)
{
#ifdef CONFIG_X86
	printk(KERN_INFO "post_handler: p->addr = 0x%p, flags = 0x%lx\n",
		p->addr, regs->flags);
#endif
#ifdef CONFIG_PPC
	printk(KERN_INFO "post_handler: p->addr = 0x%p, msr = 0x%lx\n",
		p->addr, regs->msr);
#endif
}
 
/*在pre-handler或post-handler中的任何指令或者kprobe单步执行的被探测指令产生了例外时,会调用fault_handler*/
static int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr)
{
	printk(KERN_INFO "fault_handler: p->addr = 0x%p, trap #%dn",
		p->addr, trapnr);
	/* 不处理错误时应该返回*/
	return 0;
}
 
/*初始化内核模块*/
static int __init kprobe_init(void)
{
	int ret;
	kp.pre_handler = handler_pre;
	kp.post_handler = handler_post;
	kp.fault_handler = handler_fault;
 
	ret = register_kprobe(&kp);  /*注册kprobe*/
	if (ret < 0) {
		printk(KERN_INFO "register_kprobe failed, returned %d\n", ret);
		return ret;
	}
	printk(KERN_INFO "Planted kprobe at %p\n", kp.addr);
	return 0;
}
 
static void __exit kprobe_exit(void)
{
	unregister_kprobe(&kp);
	printk(KERN_INFO "kprobe at %p unregistered\n", kp.addr);
}
 
module_init(kprobe_init)
module_exit(kprobe_exit)
MODULE_LICENSE("GPL");

Systemtap调试

(1)Systemtap原理

Systemtap是一个基于kprobe调试内核的开源软件。调试者只需要写一些脚本,通过Systemtap提供的命令行接口对正在运行的内核进行诊断调试,不需要修改或插入调试代码、重新编译内核、安装内核和重启动等工作,使内核调试变得简单容易。Systemtap调试过程与在gdb调试器中用断点命令行调试类似。

Systemtap用类似于awk语言的脚本语言编写调试脚本,该脚本命名事件并给这些事件指定处理例程。只要指定的事件发生,Linux内核将运行对应的处理例程。

有几种类型的事件,如:进入或退出一个函数,一个定时器超时或整个systemtap会话开始或停止。处理例程是一系列脚本语言语句指定事件发生时所做的工作,包括从事件上下文提取数据,存储它们进入内部变量或打印结果。

Systemtap的运行过程如图2所示,用户调试时用Systemtap编写调试脚本,Systemtap的翻译模块(translator)将脚本经语法分析(parse)、功能处理(elaborate)和翻译后生成C语言调试程序,然后,运行C编译器编译(build)创建调试内核模块。再接着将该内核模块装载入内核,通过kprobe机制,内核的hook激活所有的探测事件。当任何处理器上有这些事件发生时,对应的处理例程被触发工作,kprobe机制在内核获取的调试数据通过文件系统relayfs传回Systemtap,输出调试数据probe.out。在调试结束时,会话停止,内核断开hook连接,并卸载内核模块。整个操作过程由单个命令行程序strap驱动控制。

Linux内核调试方法总结
    




Linux内核调试方法

图2 Systemtap运行过程

(2)stap程序

stap程序是Systemtap工具的前端,它接受用systemtap脚本语言编写的探测指令,翻译这些指令到C语言代码,编译C代码产生并装载内核模块到正运行的Linux内核,执行请求的跟踪或探测函数。用户可在一个命名文件中提供脚本或从命令行中提供调试语句。

命令stap的用法列出如下:

stap [ OPTIONS ] FILENAME [ ARGUMENTS ]

stap [ OPTIONS ] - [ ARGUMENTS ]

stap [ OPTIONS ] -e SCRIPT [ ARGUMENTS ]

stap [ OPTIONS ] -l PROBE [ ARGUMENTS ]

选项[ OPTIONS ]说明如下:

-h 显示帮助信息。

-V 显示版本信息。

-k 在所有操作完成后,保留临时目录。对于检查产生的C代码或重使用编译的内核对象来说,这是有用的。

-u 非优化编译模式。.

-w 关闭警告信息。

-b 让内核到用户数据传输使用bulk模式。

-t 收集时间信息:探测执行的次数、每个探测花费的平均时间量。

-sNUM 内核到用户数据传输使用NUM MB 的缓冲区。当多个处理器工作在bulk模式时,这是单个处理器的缓冲区大小。

-p NUM Systemtap在通过NUM个步骤后停止。步骤数为1-5: parse, elaborate, translate, compile, run。

-I DIR 添加tapset库(用于翻译C代码的函数集)搜索目录。

-D NAME=VALUE 添加C语言宏定义给内核模块Makefile,用于重写有限的参数。

-R DIR 在给定的目录查找Systemtap运行源代码。

-r RELEASE 为给定的内核发布版本RELEASE而不是当前运行内核编译内核模块。

-m MODULE 给编译产生的内核模块命名MODULE,替代缺省下的随机命名。产生的内核模块被拷贝到当前目录。

-o FILE 发送标准输出到命名文件FILE。在bulk模式,每个CPU的文件名将用"FILE_CPU序号"表示。

-c CMD 开始探测,运行CMD,当CMD完成时退出。

-x PID 设置target()????到PID。这允许脚本作为指定进程的过滤器。

-l PROBE 代替运行一个探测脚本,它仅列出所有匹配给定模式PROBE可用的探测点,模式PROBE可用通配符。

--kmap[=FILE] 指定符号文件,缺省文件为/boot/System.map-VER-SION。探测的函数的地址和名字需要通过内核或内核模块的符号表解析。如果内核编译时没有调试信息或者探测是在没有调试信息的汇编语言言论的,这将是有用的。

--ignore-vmlinux 忽略vmlinux文件。

(3)Systemtap脚本语法

Systemtap脚本语法类似于C语言,它使用了三种数据类型:整数(integers)、字符串(strings)和关联数组(associative Arrays)。它有与C语言一样的控制结构。Systemtap脚本语法详细内容请参考《Systemtap tutorial》。

Systemtap脚本由探测点(probe)和探测输出函数组成。每个Systemtap脚本至少定义一个探测点。函数是探测点的处理例程。

#!/usr/bin/env stap    #Systemtap脚本的标志
#
# 显示在最后5秒内调用最后10个系统调用
display the top 10 syscalls called in last 5 seconds
#
global syscalls          #定义全局变量
function print_top () {  #定义函数
        cnt=0            #局部变量
        log ("SYSCALL\t\t\t\tCOUNT")      #打印表头标题“SYSCALL COUNT”
        foreach ([name] in syscalls-) {   #查询每个系统调用的计数值
                printf("%-20s\t\t%5d\n",name, syscalls[name])  #按格式打印
                if (cnt++ == 10)
                        break
        }
        printf("--------------------------------------\n")
        delete syscalls                 #删除全局变量
}
probe syscall.* {       #在系统调用探测点
        syscalls[probefunc()]++      #系统调用计数
}
probe timer.ms(5000) {
        print_top ()       #调用函数
}

kdb内核调试器

Kdb(Kernel Debug)是SGI公司开发的遵循GPL的内建Linux内核调试工具。标准的Linux内核不包括kdb,需要从ftp://oss.sgi.com/www/projects/kdb/download/ix86 下载对应标准版本内核的kdb补丁,对标准内核打补丁,然后,编译打过补丁的内核代码。目前kdb支持包括x86(IA32)、IA64和MIPS在内的体系结构。

Kdb调试器是Linux内核的一部分,提供了检查内存和数据结构的方法。通过附加命令,它可以格式化显示给定地址或ID的基本系统数据结构。kdb当前的命令集可以完全控制内核的操作,包括单步运行一个处理器、在指定的指令执行处理暂停、在访问或修改指定虚拟内存的位置暂停、在输入-输出地址空间对一个寄存器访问处暂停、通过进程ID跟踪任务、指令反汇编等。

安装kdb

标准内核不包含kdb,因此,用户需要先下载kdb补丁,如:kdb-v4.4-2.6.24-x86-2.bz2,接着,应打补丁、配置、编译和安装内核。

(1)打补丁

下载和解压缩补丁,将补丁打进标准内核中。方法如下:

$ upzip kdb-v4.4-2.6.24-x86-2.bz2
$cp kdb-v4.4-2.6.24-x86-2  linux-2.6.24/
$ cd linux-2.6.24
$ patch -p1 < kdb-v4.4-2.6.24-x86-2.bz
$ make xconfig

(2)配置新内核

运行make xconfig,在配置界面上选择CONFIG_KDB选项,为了更好地调试,建议用户从配置界面上选择CONFIG_FRAME_POINTER选项,尽管该选项使用了格外的寄存器并产生稍慢一些的内核。

(3)编译与安装新内核

按下面步骤重新编译和安装新内核:

#make   
#make install  
#make modules_install

使用kdb调试命令

运行支持kdb的内核后,在控制台上按下 Pause(或 Break)键将启动调试。当内核发生 oop或到达某个断点时,也会启动 kdb。kdb提示符如下所示:

Entering kdb (current=0xc03b0000,pid 0)on processor 0 due to Keyboard Entry

[0]kdb>

在kdb提示符下,用户可以输入kdb命令,详细的kdb命令使用说明请参考man kdb文档,一些常见的命令说明如表1.

表1 常见kdb命令说明

命令 命令说明
' 命令可以用于显示所有kdb命令。
bp 设置或显示一个断点。
bph 设置一个硬件断点。
bc 清除一个断点。
bl 列出所有当前断点。
bt 显示当前进程的堆栈跟踪情况。
go 退出调试器并重启内核运行。
Id 反汇编指令。
md 显示指定地址内容。
mds 以符号形式显示内存。
mm 修改内存。
reboot 立即重启机器。
rd 显示寄存器内容。
ss 单步执行(一次一条指令)。
ssb 单步执行CPU直到到达一分支。

下面以调试scull驱动程序为例简单说明kdb的使用方法:

假定scull驱动程序内核模块已装载入内核,先在驱动程序的函数scull_read 中设置一个断点,方法如下:

[1]kdb> bp scull_read

Instruction(i) BP #0 at 0xc8833514 (scull_read) is enabled on cpu 1

[1]kdb> go

  命令bp在函数scull_read 开始处设置了一个断点,接着,命令go退出调试器,重启内核运行。内核下一次进入函数scull_read 时暂停运行。产生如下的状态:

Entering kdb (0xc3108000) on processor 0 due to Breakpoint @ 0xc8833515 
Instruction(i) breakpoint #0 at 0xc8833514 
scull_read+0x1:   movl   %esp,%ebp 
[0]kdb>

  kdb当前scull_read断点位置。可用命令bt查看堆栈跟踪记录,检查函数调用层次树,方法如下:

[0]kdb> bt 
   EBP       EIP         Function(args) 
0xc3109c5c 0xc8833515  scull_read+0x1 
0xc3109fbc 0xfc458b10  scull_read+0x33c255fc( 0x3, 0x803ad78, 0x1000, 
0x1000, 0x804ad78) 
0xbffffc88 0xc010bec0  system_call 
[0]kdb>

再可用命令mds显示指定内存的数据,如:查询 scull_devices 指针的值方法如下:

[0]kdb> mds scull_devices 1

c8836104: c4c125c0 ....

上面命令查看指针scull_devices所指位置的一个双字(4个字节)数据,表示设备结构数组的起始地址为c4c125c0。再用mds查看设备结构的数据,方法如下:

[0]kdb> mds c4c125c0

c4c125c0: c3785000 ....

c4c125c4: 00000000 ....

c4c125c8: 00000fa0 ....

c4c125cc: 000003e8 ....

c4c125d0: 0000009a ....

c4c125d4: 00000000 ....

c4c125d8: 00000000 ....

c4c125dc: 00000001 ....

  上面8行分别对应结构Scull_Dev的8个成员。再与数据结构Scull_Dev的定义相对照,可知这8个数据的含义。

还可以使用命令mm修改数据。例如:将结构Scull_Dev的某一成员值设置为0x50,方法如下:

[0]kdb> mm c4c125d0 0x50

0xc4c125d0 = 0x50

kgdb

kgdb调试原理

调试器GNU gdb主要用于调试用户级程序,通过串口线或网络将两台计算机以主机/目标机(host machine/target machine)方式连接时,gdb还可用于调试linux内核。这种方式需要给内核打进包含kgdb驱动程序在内的补丁。

kgdb是Linux内核的源代码级调试器,与gdb配合使用可以调试Linux内核。在Linux内核的kgdb配合下,内核开发者可以用类似于调试应用程序的方式通过gdb调试内核,可以方便以使用gdb的命令在内核代码放置断点、单步调试内核和观察内核变量值等。

kgdb进行源码级内核调试的原理图如图1所示。在两台计算中机,一台用作开发计算机,称为主机或开发机;一台用作测试计算机,称为目标机或测试机。两台计算机可通过串行线或以太网进行通信。内核在测试机上调试,gdb在开发机上运行,gdb通过串行线用null modem与调试的内核通信。两台计算机也可以使用一台计算机上的两个虚拟机进行替代。

Linux内核调试方法总结
    




Linux内核调试方法

图1 kgdb进行源码级内核调试原理图

目前,kgdb支持i386, x86_64, ppc, arm, mips和ia64等处理器构架,开发机和测试机可用串行线或以太网进行连接通信。

kgdb补丁将下面的内容加入到内核代码中:

●gdb stub - gdb stub("树桩")是调试器的核心,它处理来自开发机上gdb的请求。当测试机运行了帯有kgdb的内核时,gdb stub控制测试机中所有的处理器。

●对出错处理例程的修改- 当一个不期望的错误发生时,内核将控制传递给kgdb调试器。不含有kgdb的内核在出现不可预测错误时会崩溃(panic),通过对出错处理的修改,kgdb允许开发者分析不可预测的出错。

●串行通信-该部件通过内核的串行驱动程序,为内核中的stub提供接口,负责在串行连接线上发送和接收数据,还负责处理开发机上gdb发送的处理控制断点请求。

建立kdbg联机调试的方法

下面说明建立kdbg联机调试的步骤:

(1)软件建立和应用kgdb补丁

1)下载Linux内核源代码:linux-2.6.15.5.tar.bz2。

2)下载与内核版本对应的kgdb补丁:linux-2.6.15.5-kgdb-2.4.tar.bz2。

3)解压缩软件包,方法如下:

cd ${BASE_DIR}

tar -jxvf linux-2.6.15.5.tar.bz2

cd ${BASE_DIR}/linux-2.6.15.5

tar -jxvf linux-2.6.15.5-kgdb-2.4.tar.bz2

在${BASE_DIR}/linux-2.6.15.5目录中,给Linux内核打kgdb补丁,方法如下:

patch -p1 < ${BASE_DIR}/linux-2.6.15.5-kgdb-2.4/core-lite.patch

patch -p1 < ${BASE_DIR}/linux-2.6.15.5-kgdb-2.4/i386.patch

(2)在开发机上编译内核

1)在${BASE_DIR}/linux-2.6.15.5/Makefile,设置EXTRAVERSION = -kgdb。

2)运行命令make xconfig或make oldconfig,出现如图2所示的内核配置界面。在配置界面中,为目标机硬件选择合适的选项;在"Kernel hacking"条目下,选择kgdb的选项。

Linux内核调试方法总结
    




Linux内核调试方法

图2 在Linux内核配置界面中与kgdb相关的配置选项

3)运行make bzImage编译内核。

4)将编译的内核从开发机上传送到目标机上,拷贝内核映像${BASE_DIR}/linux-2.6.15.5/arch/i386/boot/bzImage到目标机/boot/vmlinuz-2.6.15.5-kgdb。然后,再拷贝映射文件${BASE_DIR}/linux-2.6.15.5/System.map到目标机/boot/System.map-2.6.15.5-kgdb。再如下建立符号链接:

ln -s /boot/vmlinuz-2.6.15.5-kgdb /boot/vmlinuz

ln -s /boot/System.map-2.6.15.5-kgdb /boot/System.map

5)在目标机上编辑文件/boot/grub/grub.conf, 在该文件加入含有kgdb的内核条目,方法如下:

title Linux-2.6.15.5-kgdb

root (hd0,0)

kernel /boot/vmlinuz-2.6.15.5-kgdb ro root=/dev/hda1 kgdbwait

(3)在开发机上开始调试会话

1)在启动目标机后,它将等待开发机连接,显示下面的消息:

Waiting for connection from remote gdb...

2)用命令cd ${BASE_DIR}/linux-2.6.15.5进入目录linux-2.6.15.5目录。

3)用root用户登录设置调试会话波特率,方法如下:

<root#> gdb ./vmlinux

(gdb) set remotebaud 115200

(gdb) target remote /dev/ttyS0

Remote debugging using /dev/ttyS0

breakpoint () at kernel/kgdb.c:1212

1212 atomic_set(&kgdb_setting_breakpoint, 0);

warning: shared library handler failed to enable breakpoint

(gdb)

4)在开发机上输入调试命令

此时,gdb已连接到目标机上的内核,目标机上的内核正等待接收命令进行测试。输入命令(gdb) c(表示继续运行)时,目标机系统正常启动,在配置内核时,如果开发机选择了通过gdb输出控制台消息,则控制台log消息会从gdb上显示。

由gdb连接到测试内核,如果测试内核发生内核崩溃(kernel panic),它将首先将控制权转移给gdb,以让gdb分析崩溃原因。

(4)使用kgdb以太网接口

kgdb还可能通过以太网接口调试内核,用以太网接口建立连接的步骤说明如下:

1)添加下面的行到grub条目中:

kgdboe=@10.0.0.6/,@10.0.0.3/ (that's kgdboe=@LOCAL-IP/,@REMOTE-IP/)
# Sample grub.conf which will by default boot the kgdb enabled kernel
title Linux-2.6.15.5-kgdb(eth)
root (hd0,0)
kernel /boot/vmlinuz-2.6.15.5-kgdb ro root=/dev/hda1 kgdboe=@10.0.0.6/,@10.0.0.3/
console=ttyS0,115200

2)接着在gdb中用下面的命令开始调试会话:

(gdb) ./vmlinux

(gdb) target remote udp:HOSTNAME:6443

调试内核模块

内核可加载模块的调试具有其特殊性,内核模块中各段的地址在模块加载进内核后才最终确定的,开发机的gdb无法得到各种符号地址信息。因此,用户需要使用特殊版本的gdb,可以检测到内核模块的装载和卸载。另外,还需要将内核模块的符号装载到gdb中,这样,gdb才能解析到符号。

(1)准备检测内核模块装载和卸载代码的gdb派生版本

此步骤在开发机上完成。

安装在开发机上的gdb应含有内核模块调试特征,用户需要安装含有检测内核模块装载和卸载代码的gdb派生版本,该版本gdb派生于标准的gdb。用户可以从网址http://kgdb.linsyssoft.com/downloads.htm下载gdb-6.4-kgdb-2.4.tar.bz2,然后,编译安装生成gdb派生版本。或者下载gdbmod-2.4.bz2,解压缩后得到可执行的gdb派生版本。

在测试机上不需要特别的安装,内核模块可以出现在根文件系统或一个ramdisk中。

(2)装载内核模块符号到gdb中

此步骤在开发机上完成。

在开发机上,用户应装载内核模块的符号到gdb,让gdb调试时可以解析到二进制代码对应的符号。

首先,内核模块编译时应打开调试信息。然后,用下面方法设置内核、调试接口和定位内核模块位置:

#cd /usr/src/linux 2.6.13 
#gdbmod 2.4 vmlinux 
(gdb) target remote /dev/ttyS0
Remote debugging using /dev/ttyS0
breakpoint () at gdbstub.c:1153
1153 }
(gdb)set solib search path /usr/linux 2.6.13/drivers/net

一旦kgdb通知一个内核模块装载时,gdb必须能定位模块文件。因此,用户需要用命令"set solib-search-path"设置内核模块文件所在的路径。

(3)插入内核模块到内核

插入内核模块到内核,方法 如下:

# insmod mymodule.ko

到此,已装载了内核模块符号,内核模块可以像正常的内核代码一样调试了。

样例:调试内核模块test

1)编写内核模块

首先,在开发机上编写简单的内核模块test,代码如下:

void test_func()
{
    printk("test_func\n");
    printk("aaaaaaaaaaa\n");
}
 
int test_init()
{
    printk("test_init_module\n");
 
    return 0;
}
 
void test_exit()
{
    printk("test_cleanup_module\n");
}
 
module_init(test_init);
module_exit(test_exit);

2)编译安装内核模块

接着,编译内核模块,并将内核模块拷贝到测试机上。方法如下:

#cd /root/mymodule
#gcc -D__KERNEL__ -DMODULE -I/usr/src/linux-2.6.15/kernel/include -O -Wall -g -c -o test.ko test.c
#scp test.ko root@192.168.1.130:/root

3)开始调试

装载内核符号到gdb中,设置内核模块所在路径,方法如下:

# gdbmod vmlinux
 (gdb) set solib-search-path /root/mymodule

执行命令rmt,进入测试机调试,方法如下:

(gdb) rmt
breakpoint () at kgdbstub.c:1005
1005                    atomic_set(&kgdb_setting_breakpoint, 0);

在内核模块初始化处设置断点。查内核源码可知,内核模块初始化函数init在module.c文件函数sys_init_module函数中的mod->init处调用,对应行号为2168(根据不同版本的内核,行号可能不同)。设置断点方法如下:

(gdb) b module.c:2168
Breakpoint 1 at 0xc011cd83: file module.c, line 2168.

让测试机上的内核继续运行,方法如下:

(gdb) c
Continuing.
[New Thread 1352]
[Switching to Thread 1352]

测试机用命令"insmod test.ko"执行插入内核模块操作时,开发机会在断点处被暂停,暂停时显示如下

Breakpoint 1, sys_init_module (name_user=0xc03401bc "\001",
mod_user=0x80904d8) at module.c:2168
2168             ret = mod->init();

用step命令进入内核模块test的函数init,方法如下:

(gdb) step
test_init () at test.c:12
12              printk("test_init_module\n");
(gdb) n
15      }
(gdb)

对内核模块的非init函数调试时,由于测试机上已插入模块,模块的符号也已加载,只需要直接需要调试的代码处设置断点。如:在函数test_func处设置断点的方法如下:

(gdb)bt test_func

调试内核

用gdb调试内核类型于调试应用程序进程,kgdb支持gdb的执行控制命令、栈跟踪和线程分析等。但kgdb不支持watchpoint,kgdb通过gdb宏来执行watchpoint。

调试内核的命令说明如下:

(1)停止内核执行

用户在gdb终端中按下Ctrl + C键,gdb将发送停止消息给kgdb stub,kgdb stub控制内核的运行,并与gdb通信。

(2)继续内核运行

gdb命令"(gdb) c"告诉kgdb stub继续内核运行,直到遇到一个断点,或者gdb执行Ctrl + C,或其他原因,内核运行才停顿下来。

(3)断点

gdb断点(Breakpoints)用于在一个函数或代码行处暂停内核运行,设置断点命令如:"(gdb) b module.c:2168"。

(4)进入代码

使用命令"(gdb) step"进入一个函数或在暂停后执行下一个程序行;使用命令"(gdb) next"跳过一个函数执行下一个程序行或暂停后执行下一个程序;

(5)栈跟踪(Stack Trace)

使用命令"(gdb) bt"或"(gdb) backtrace"显示程序栈,它显示了调用函数的层次列表,表明了函数的调用函数。该命令还打印调解函数的参数值。

例如,运行命令backtrace的样例列出如下:

(gdb) backtrace
#0 breakpoint () at gdbstub.c:1160
#1 0xc0188b6c in gdb_interrupt (irq=3, dev_id=0x0, regs=0xc02c9f9c) at gdbserial.c:143
#2 0xc0108809 in handle_IRQ_event (irq=3, regs=0xc02c9f9c, action=0xc12fd200)3 0xc0108a0d in do_IRQ (regs={ebx = 1072672288, ecx = 0, edx = 1070825472, esi = 1070825472, edi = 1072672288, ebp = 1070817328, eax = 0, xds = 1072693224, xes = 1072693224, orig_eax = 253, eip = 1072672241, xcs = 16, eflags = 582, esp = 1070817308, xss = 1072672126}) at irq.c:621
#4 0xc0106e04 in ret_from_intr () at af_packet.c:1878
#5 0xc0105282 in cpu_idle () at process.c:135
#6 0xc02ca91f in start_kernel () at init/main.c:599
#7 0xc01001cf in L6 () at af_packet.c:1878
Cannot access memory at address 0x8e000

除非栈帧数作为命令backtrace的参数外,gdb仅在栈跟踪走出了可访问地址空间时才停止打印栈的信息。上面例子中,函数调用层次次序从上到下为:ret_from_intr, do_IRQ, handle_IRQ_event, gdb_interrupt。

放置一个断点在函数ext2_readlink,并访问一个符号链接,以便运行到该断点。 设置断点方法如下:

(gdb) br ext2_readlink
Breakpoint 2 at 0xc0158a05: file symlink.c, line 25.
(gdb) c
Continuing.

在测试机上运行命令"ls -l /boot/vmlinuz"显示一个符号链接。在测试机上,内核会运行到上述断点处,并暂停。然后,将断点信息传回开发机。在开发机上,用户可以查看栈或运行其他调试命令。例如:运行栈跟踪命令,显示的结果列出如下:

Breakpoint 2, ext2_readlink (dentry=0xc763c6c0, buffer=0xbfffed84 "\214\005",buflen=4096) at symlink.c:25 25 char *s = (char *)dentry >d_inode >u.ext2_i.i_data; 
(gdb) bt
#0 ext2_readlink (dentry=0xc763c6c0, buffer=0xbfffed84 "\214\005",buflen=4096) at symlink.c:25
#1 0xc013b027 in sys_readlink (path=0xbfffff77 "/boot/vmlinuz", buf=0xbfffed84 "\214\005", bufsiz=4096) at stat.c:262
#2 0xc0106d83 in system_call () at af_packet.c:1878
#3 0x804aec8 in ?? () at af_packet.c:1878
#4 0x8049697 in ?? () at af_packet.c:1878
#5 0x400349cb in ?? () at af_packet.c:1878

上述栈跟踪中,gdb打印一些无效的栈帧(#3~#5),这是因为gdb不知道在哪里停止栈跟踪,可以忽略这些无效的栈帧。

系统调用readlink在函数system_call进入内核,该函数显示在af_packet.c中,这是不对的,因为对于汇编语言文件的函数,gdb不能指出正常的代码行。但gdb可以正确处理在C语言文件中内联汇编代码。更多的调用层次是:sys_readlink和ext2_readlink。

在调试完后,用户可用"删除"命令和"继续"命令删除断点,并继续内核的运行,方法如下:

(gdb) delete
Delete all breakpoints? (y or n) y
(gdb) c
Continuing.

(6)内联函数

使用gdb栈跟踪命令通常足够找出一个函数的调用层次关系。但当其中一个栈帧在扩展的内联函数中,或者从一个内联函数访问另一个内联函数时,栈跟踪命令是不够用的,栈跟踪仅显示在内联函数中的源代码文件名和语句的行号,通过查看外面的函数,这可能知道调用的内联函数,但如果调用了两次内联函数,它就不知道是哪个内联函数了。

下面的处理流程可用来找出内联函数信息:

在栈跟踪中,gdb还与函数名一起显示代码地址,在调用了一个内联函数的语句中,gdb显示了这些代码调用和被调用的地址。脚本disasfun.sh可用来反汇编,源代码从vmlinux文件引用一个内核函数。文件vmlinux含有内核函数的绝对地址,因此,在汇编代码中看见的地址是在内存中的地址。

下面是一个样例。

配置内核时,kgdb应打开线程分析(CONFIG_KGDB_THREAD),gdb应连接到目标内核。

用"Ctrl+C"中断内核,放置一个断点在函数__down处,并继续运行,方法如下:

Program received signal SIGTRAP, Trace/breakpoint trap.
breakpoint () at gdbstub.c:1160
1160    }
(gdb) break __down
Breakpoint 1 at 0xc0105a43: file semaphore.c, line 62.
(gdb) c
Continuing.

为了让程序运行到断点处,在目标机上运行"man lilo"。程序会运行到断点,gdb会进入命令行模式。输入栈跟踪命令,显示如下:

Breakpoint 1, __down (sem=0xc7393f90) at semaphore.c:62
62              add_wait_queue_exclusive(&sem->wait, &wait);
(gdb) backtrace
#0  __down (sem=0xc7393f90) at semaphore.c:62
#1  0xc0105c70 in __down_failed () at af_packet.c:1878
#2  0xc011433b in do_fork (clone_flags=16657, stack_start=3221199556,
     regs=0xc7393fc4, stack_size=0)
     at /mnt/work/build/old-pc/linux-2.4.6-kgdb/include/asm/semaphore.h:120
#3  0xc010594b in sys_vfork (regs={ebx = 1074823660, ecx = 1074180970,
      edx = 1074823660, esi = -1073767732, edi = 134744856, ebp = -1073767712,
      eax = 190, xds = 43, xes = 43, orig_eax = 190, eip = 1074437320,
      xcs = 35, eflags = 518, esp = -1073767740, xss = 43}) at process.c:719

在函数sys_vfork中行号显示为719,这与文件process.c中的行号一致,查看该文件可得到确认,方法如下:

(gdb) list process.c:719
714      * do not have enough call-clobbered registers to hold all
715      * the information you need.
716      */
717     asmlinkage int sys_vfork(struct pt_regs regs)
718     {
719             return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, &regs, 0);
720     }
721
722     /*
723      * sys_execve() executes a new program.

就像gdb显示的一样,函数sys_vfork调用函数do_fork,再看栈跟踪显示的第2帧,gdb显示它在文件semaphore.h中的行号是120,显示的行号虽然没有用但是正确的。查看该文件可得到确认,方法如下:

(gdb) list semaphore.h:118
113      */
114     static inline void down(struct semaphore * sem)
115     {
116     #if WAITQUEUE_DEBUG
117             CHECK_MAGIC(sem->__magic);
118     #endif
119
120             __asm__ __volatile__(                                              <-----
121                     "# atomic down operation\n\t"
122                     LOCK "decl %0\n\t"     /* --sem->count */

上述代码中,在箭头所指示语句处,得到的信息仅是它在do_fork的一个扩展的内联函数down中。gdb还打印了在do_fork中从下一个被调用的函数开始代码的绝对地址:0xc011433b。这里我们用脚本disasfun找出该地址所对应的代码行。命令"disasfun vmlinux do_fork"输出的部分结果显示如下:

if ((clone_flags & CLONE_VFORK) && (retval > 0))
c011431d:       8b 7d 08                mov    0x8(%ebp),%edi
c0114320:       f7 c7 00 40 00 00       test   $0x4000,%edi
c0114326:       74 13                   je     c011433b <do_fork+0x707>
c0114328:       83 7d d4 00             cmpl   $0x0,0xffffffd4(%ebp)
c011432c:       7e 0d                   jle    c011433b <do_fork+0x707>
#if WAITQUEUE_DEBUG
CHECK_MAGIC(sem->__magic);
#endif
        __asm__ __volatile__(
c011432e:       8b 4d d0                mov    0xffffffd0(%ebp),%ecx
c0114331:       f0 ff 4d ec             lock decl 0xffffffec(%ebp)
c0114335:       0f 88 68 95 13 00       js     c024d8a3 <stext_lock+0x7bf>
                down(&sem);
        return retval;
c011433b:       8b 45 d4                mov    
0xffffffd4(%ebp),%eax              <-----
c011433e:       e9 8d 00 00 00          jmp    c01143d0 <do_fork+0x79c>
Looking at the code in fork.c  we know where above code is:
fork_out:
if ((clone_flags & CLONE_VFORK) && (retval > 0))
    down(&sem)

(7)线程分析

gdb具有分析应用程序线程的特征,它提供了应用程序创建的线程的列表,它允许开发者查看其中任何一个线程。gdb的特征可用来与kgdb一起查看内核线程。gdb能提供内核中所有线程的列表。开发者可指定一个线程进行分析。像backtrace,info regi这样的gdb命令接着可以显示指定线程上下文的信息。

应用程序创建的所有线程分享同一地址空间,相似地,所有内核线程共享内核地址空间。每个内核线程的用户地址空间可能不同,因此,gdb线程可较好地分析内核代码和驻留在内核空间的数据结构。

gdb info给出了关于gdb线程分析方面更多的信息。下面列出一个内核线程分析的样例:

gdb命令"info threads"给出了内核线程的列表,显示如下:

(gdb) info thr
  21 thread 516  schedule_timeout (timeout=2147483647) at sched.c:411
  20 thread 515  schedule_timeout (timeout=2147483647) at sched.c:411
  19 thread 514  schedule_timeout (timeout=2147483647) at sched.c:411
  18 thread 513  schedule_timeout (timeout=2147483647) at sched.c:411
  17 thread 512  schedule_timeout (timeout=2147483647) at sched.c:411
  16 thread 511  schedule_timeout (timeout=2147483647) at sched.c:411
  15 thread 438  schedule_timeout (timeout=2147483647) at sched.c:411
  14 thread 420  schedule_timeout (timeout=-1013981316) at sched.c:439
  13 thread 406  schedule_timeout (timeout=-1013629060) at sched.c:439
  12 thread 392  do_syslog (type=2, buf=0x804dc20 "run/utmp", len=4095)
    at printk.c:182
  11 thread 383  schedule_timeout (timeout=2147483647) at sched.c:411
  10 thread 328  schedule_timeout (timeout=2147483647) at sched.c:411
  9 thread 270  schedule_timeout (timeout=-1011908724) at sched.c:439
  8 thread 8  interruptible_sleep_on (q=0xc02c8848) at sched.c:814
  7 thread 6  schedule_timeout (timeout=-1055490112) at sched.c:439
  6 thread 5  interruptible_sleep_on (q=0xc02b74b4) at sched.c:814
  5 thread 4  kswapd (unused=0x0) at vmscan.c:736
  4 thread 3  ksoftirqd (__bind_cpu=0x0) at softirq.c:387
  3 thread 2  context_thread (startup=0xc02e93c8) at context.c:101
  2 thread 1  schedule_timeout (timeout=-1055703292) at sched.c:439
* 1 thread 0  breakpoint () at gdbstub.c:1159
(gdb)

如上所显示,gdb为每个线程设定在gdb中唯一的id,当gdb内部引用一个线程时,可以使用这个id。例如:线程7(PID 7)具有gdb id 8。为了分析内核线程8 ,我们指定线程9给gdb。gdb接着切换到该线程中,准备做更多的分析。

下面是分析线程的命令显示:

(gdb) thr 9
[Switching to thread 9 (thread 270)]
#0  schedule_timeout (timeout=-1011908724) at sched.c:439
439             del_timer_sync(&timer);
(gdb) bt
#0  schedule_timeout (timeout=-1011908724) at sched.c:439
#1  0xc0113f36 in interruptible_sleep_on_timeout (q=0xc11601f0, timeout=134)
    at sched.c:824
#2  0xc019e77c in rtl8139_thread (data=0xc1160000) at 8139too.c:1559
#3  0xc010564b in kernel_thread (fn=0x70617773, arg=0x6361635f,
    flags=1767859560) at process.c:491
#4  0x19 in uhci_hcd_cleanup () at uhci.c:3052
#5  0x313330 in ?? () at af_packet.c:1891
Cannot access memory at address 0x31494350
(gdb) info regi
eax            0xc38fdf7c       -1013981316
ecx            0x86     134
edx            0xc0339f9c       -1070358628
ebx            0x40f13  266003
esp             0xc3af7f74       0xc3af7f74
ebp            0xc3af7fa0       0xc3af7fa0
esi            0xc3af7f8c       -1011908724
edi            0xc3af7fbc       -1011908676
eip            0xc011346d       0xc011346d
eflags         0x86     134
cs             0x10     16
ss             0x18     24
ds             0x18     24
es             0x18     24
fs             0xffff   65535
gs             0xffff   65535
fctrl          0x0      0
fstat          0x0      0
ftag           0x0      0
fiseg          0x0      0
fioff          0x0      0
foseg          0x0      0
fooff          0x0      0
---Type <return> to continue, or q <return> to quit---
fop            0x0      0
(gdb) thr 7
[Switching to thread 7 (thread 6)]
#0  schedule_timeout (timeout=-1055490112) at sched.c:439
439             del_timer_sync(&timer);
(gdb) bt
#0  schedule_timeout (timeout=-1055490112) at sched.c:439
#1  0xc0137ef2 in kupdate (startup=0xc02e9408) at buffer.c:2826
#2  0xc010564b in kernel_thread (fn=0xc3843a64, arg=0xc3843a68,
    flags=3280222828) at process.c:491
#3  0xc3843a60 in ?? ()
Cannot access memory at address 0x1f4
(gdb)

用户可从http://sourceware.org/gdb/download/下载进程信息宏ps和psname,ps宏提供了运行在内核的线程的名字和ID。运行结果显示如下:

(gdb) ps
0 swapper
1 init
2 keventd
3 ksoftirqd_C
4 kswapd
5 bdflush
6 kupdated
8 khubd
270 eth0
328 portmap
383 syslogd
392 klogd
406 atd
420 crond
438 inetd
511 mingetty
512 mingetty
513 mingetty
514 mingetty
515 mingetty
516 mingetty
(gdb) 
The psname macro can be used to get name of a thread when it's id is known.
(gdb) psname 8
8 khubd
(gdb) psname 7
(gdb)
 
(7)Watchpoints

(7)Watchpoints

kgdb stub使用x86处理器的调试特征支持硬件断点,这些断点不需要代码修改。它们使用调试寄存器。x86体系中的ia32处理器有4个硬件断点可用。每个硬件断点可以是下面三个类型之一:

执行断点 当代码在断点地址执行时,触发执行断点。由于硬件断点有限,建议通过gdb break命令使用软件断点,除非可避免修改代码。

写断点 当系统对在断点地址的内存位置进行写操作时,触发一个写断点。写断点可以放置可变长度的数据。写断点的长度指示为观察的数据类型长度,1表示为字节数据,2表示为2字节数据,3表示为4字节数据。

访问断点 当系统读或写断点地址的内存时,触发一个访问断点。访问断点也有可变长度数据类型。

ia-32处理器不支持IO断点。

因为gdb stub目前不使用gdb用于硬件断点的协议,因此,它通过gdb宏访问硬件断点。硬件断点的gdb宏说明如下:

1)hwebrk – 放置一个执行断点。

用法:hwebrk breakpointno address

2)hwwbrk – 放置一个写断点。

用法:hwwbrk breakpointno length address

3)hwabrk – 放置一个访问断点。

用法:hwabrk breakpointno length address

4)hwrmbrk – 删除一个断点

用法:hwrmbrk breakpointno

5)exinfo – 告诉是否有一个软件或硬件断点发生。如果硬件断点发生,打印硬件断点的序号。

这些命令要求的参数说明如下:

breakpointno – 0~3

length - 1~3

address - 16进制内存位置(没有0x),如:c015e9bc

使用UML调试Linux内核

用户模式Linux(User Mode Linux,UML)不同于其他Linux虚拟化项目,UML尽量将它自己作为一个普通的程序。UML与其他虚拟化系统相比,优点说明如下:

良好的速度

UML编译成本地机器的代码,像主机上的其他已编译应用程序一样运行。它比在软件上应用整个硬件构架的虚拟机快得多。另一方面,UML不需要考虑依赖于特定CPU的虚拟化系统的硬件特异性。

获益于Liunx更新

每次Linux的改进,UML自动得到这些功能,虚拟化系统并不一定能从更新中获益。

弹性编码

内核需要与硬件或虚拟硬件交互,但UML可将交互看作其他方式。例如:可以将这些交互转换成共享的库,其他程序可以在使用时连接该库。它还可作为其他应用程序的子shell启动,能任何其他程序的stin/stdout使用。

可移植性

UML将来可以移植到x86 Windows, PowerPC Linux, x86 BSD或其他系统上运行。

从Linux2.6.9版本起,用户模式Linux(User mode Linux,UML)已随Linux内核源代码一起发布,它存放于arch/um目录下。编译好UML的内核之后,可直接用gdb运行编译好的内核并进行调试。

UML原理

用户模式Linux(User mode Linux,UML)将Linux内核的一部分作为用户空间的进程运行,称为客户机内核。UML运行在基于Linux系统调用接口所实现的虚拟机。UML运行的方式如图1所示。UML像其他应用程序一样与一个"真实"的Linux内核(称为"主机内核")交互。应用程序还可运行在UML中,就像运行在一个正常的Linux内核下。

 

Linux内核调试方法总结
    




Linux内核调试方法

图1 UML在Linux系统中运行的位置

使用UML的优点列出如下:

如果UML崩溃,主机内核还将运行完好。

可以用非root用户运行UML。

可以像正常进程一样调试UML。

在不中断任何操作下与内核进行交互。

用UML作为测试新应用程序的"沙箱",用于测试可能有伤害的程序。

可以用UML安全地开发内核。

可以同时运行不同的发布版本。

由于UML基于以Linux系统调用接口实现的虚拟机,UML无法访问主机的硬件设备。因此,UML不适合于调试与硬件相关的驱动程序。

编译UML模式客户机Linux内核

(1)获取源代码

http://www.kernel.org/下载linux-2.6.24.tar.bz2,解压缩源代码,方法如下:

host% bunzip2 linux-2.6.24.tar.bz2

host% tar xf linux-2.6.24.tar

host% cd linux-2.6.24

(2)配置UML模式内核

如果使用缺省配置,那么,方法如下:

host% make defconfig ARCH=um

如果运行配置界面,方法如下:

host% make menuconfig ARCH=um

如果不使用缺省配置defconfig,那么,内核编译将使用主机的配置文件,该配置文件在主机/boot目录下。对于UML模式内核来说,这是不对的,它将编译产生缺乏重要的驱动程序和不能启动的UML。

以编译UML时,每个make命令应加上选项"ARCH=um",或者设置环境变量"export ARCH=um"。

当再次配置时,可以先运行下面的命令清除所有原来编译产生的影响:

host% make mrproper

host% make mrproper ARCH=um

内核提供了配置选项用于内核调试,这些选项大部分在配置界面的kernel hacking菜单项中。一般需要选取CONFIG_DEBUG_INFO选项,以使编译的内核包含调试信息。

(3)编译UML模式内核

编译内核的方法如下:

host% make ARCH=um

当编译完成时,系统将产生名为"linux"的UML二进制。查看方法如下:

host% ls -l linux

-rwxrwxr-x 2 jdike jdike 18941274 Apr 7 15:18 linux

由于UML加入了调试符号,UML模式内核变得很大,删除这些符号将会大大缩小内核的大小,变为与标准内核接近的UML二进制。

现在,用户可以启动新的UML模式内核了。

(4)UML的工具

使用UML和管理UML的工具说明如下:

UMLd – 用于创建UML实例、管理实例启动/关闭的后台程序。

umlmgr –用于管理正运行的UML实例的前台工具程序。

UML Builder – 编译根文件系统映像(用于UML模式操作系统安装)。

uml switch2 用于后台传输的用户空间虚拟切换。

VNUML – 基于XML的语言,定义和启动基于UML的虚拟网络场景。

UMLazi – 配置和运行基于虚拟机的UML的管理工具。

vmon – 运行和监管多个UML虚拟机的轻量级工具,用Python 书写。

umvs – umvs是用C++和Bash脚本写的工具,用于管理UML实例。该应用程序的目的是简化UML的配置和管理。它使用了模板,使得编写不同的UML配置更容易。

MLN - MLN (My Linux Network) 是一个perl程序,用于从配置文件创建UML系统的完整网络,使得虚拟网络的配置和管理更容易。MLN基于它的描述和简单的编程语言编译和配置文件系统模板,并用一种组织方式存储它们。它还产生每个虚拟主机的启动和停止脚本,在一个网络内启动和停止单个虚拟机。MLN可以一次使用几个独立的网络、项目,甚至还可以将它们连接在一起。

Marionnet – 一个完全的虚拟网络实验,基于UML,带有用户友好的图形界面。

运行UML

(1)启动UML

为了运行UML实例,用户需要运行Linux操作系统主机和带有自己文件系统的UML客户机。用户可以从http://uml.nagafix.co.uk/下载UML(如:kernel)和客户机文件系统(如:root_fs),运行UML实例的方法如下:

$ ./kernel ubda= root_fs mem=128M

上述命令中,参数mem指定虚拟机的内存大小;参数ubda表示根文件系统root_fs作为虚拟机第一个块设备,虚拟机用/dev/udba表示虚拟机的第一个块设备,与Linux主机系统的第一个物理块设备/dev/sda类似。

用户还可以自己创建虚拟块设备,例如:建立交换分区并在UML上使用它的方法如下:

$ dd if=/dev/zero of=swap bs=1M count=128

$ ./kernel ubda= root_fs ubdb=swap mem=128M

上述命令,创建了128M的交换分区,作为第二个块设备ubdb,接着,启动UML模式内核,用ubdb作为它的交换分区。

(2)登录

预打包的文件系统有一个带有"root"密码的root帐户,还有一个带有"user"密码的user帐户。用户登录后可以进入虚拟机。预打包的文件系统已安装了各种命令和实用程序,用户还可容易地添加工具或程序。

还有一些其他登录方法,说明如下:

在虚拟终端上登录

每个已配置(设备存在于/dev,并且/etc/inittab在上面运行了一个getty)的虚拟终端有它自己的xterm。.

通过串行线登录

在启动输出中,找到类似下面的一行:

serial line 0 assigned pty /dev/ptyp1

粘贴用户喜爱的终端程序到相应的tty,如:minicom,方法如下:

host% minicom -o -p /dev/ttyp1

通过网络登录

如果网络正运行,用户可用telnet连接到虚拟机。

虚拟机运行后,用户可像一般Linux一样运行各种shell命令和应用程序。

建立串行线和控制台

可以粘附UML串行线和控制台到多个类型的主机I/O通道,通过命令行指定,用户可以粘附它们到主机ptys, ttys, 文件描述子和端口。常用连接方法说明如下:

让UML控制台出现在不用的主机控制台上。

将两个虚拟机连接在一起,一个粘到pty,另一个粘附到相应的tty。

创建可从网络访问的虚拟,粘附虚拟机的控制台到主机的一个端口。

(1)指定设备

用选项"con"或"ssl"(分别代表控制台和串行线)指定设备。例如:如果用户想用3号控制台或10号串行线交互,命令行选项分别为"con3"和"ssl10"。

例如:指定pty给每个串行线的样例选项列出如下:

ssl=pty ssl0=tty:/dev/tty0 ssl1=tty:/dev/tty1

(2)指定通道

可以粘附UML设备到多个不同类型的通道,每个类型有不同的指定方法,分别说明如下:

伪终端为:device=pty,pts终端为:device=pts

UML分配空闲的主机伪终端给。用户可以通过粘附终端程序到相应的tty访问伪终端,方法如下:

screen /dev/pts/n

screen /dev/ttyxx

minicom -o -p /dev/ttyxx #minicom似乎不能处理pts设备

kermit #启动它,打开设备,然后连接设备

终端为:device=tty:tty设备文件

UML将粘附设备到指定的tty,例如:一个样例选项列出如下:

con1=tty:/dev/tty3

上面语句将粘附UML的控制台1到主机的/dev/tty3。如果用户指定的tty是tty/pty对的slave端,则相应的pty必须已打开。

xterms为:device=xterm

UML将运行一个xterm,并且将设备粘附到xterm。

端口为:device=port:端口号

上述选项将粘附UML设备到指定的主机端口。例如:粘附控制台1到主机的端口9000,方法如下:

con1=port:9000

粘附所有串行线到主机端口,方法如下:

ssl=port:9000

用户可以通过telnet到该端口来访问这些设备,每个激活的telnet会话得到不同的设备,如果有比粘附到端口的UML设备多的telnet连接到一个端口,格外的telnet会话将阻塞正存在的telnet断线,或直到其他设备变为激活(如:通过在/etc/inittab中设置激活)。

已存在的文件描述子:device=文件描述子

如果用户在UML命令行中建立了一个文件描述子,他可以粘附UML设备到文件描述子。这最常用于在指定所有其他控制台后将主控制台放回到stdin和stdout上。方法如下:

con0=fd:0,fd:1 con=pts

null设备:device=null

与"none"选项相比,上述选项允许打开设备,但读将阻塞,并且写将成功,但数据会被丢掉。

无设备:device=none

上述选项将引起设备消失。如果你正使用devfs,设备将不出现在/dev下。如果设备出现,尝试打开它将返回错误-ENODEV。

用户还可以指定不同的输入和输出通道给一个设备,最常用的用途是重粘附主控制到stdin和stdout。例如:一个样例选项列出如下:

ssl3=tty:/dev/tty2,xterm

上述诗句将引起在主机/dev/tty3上的串行线3接受输入,显示输出在xterm上。

如果用户决定将主控制台从stdin/stdout移开,初始的启动输出将出现在用户正运行UML所在的终端。然而,一旦控制台驱动程序已初始化,启动及随后的输出将出现在控制台0所在的地方。

建立网络

UML实例可以用网络访问主机、本地网络上的其他机器和网络的其他部分。新的辅助程序uml_net进行主机建立时需要root权限。

当前UML虚拟机有5种传输类型用于与其他主机交换包,分别是:ethertap,TUN/TAP, Multicast,交换机后台(switch daemon),slip,slirp和pcap。

TUN/TAP, ethertap, slip和slirp传输允许UML实例与主机交换包。它们可定向到主机或主机可扮作路由器提供对其他物理或虚拟机的访问。

pcap传输是一个综合的仅读接口,用libpcap二进制从主机上的接口收集包并过滤包。这对于构建预配置的交通监管器或sniffer来说,是有用的。

后台和多播传输提供了完全虚拟的网络络其他虚拟机器。该网络完全从物理网络断开,除非某一个虚拟机扮作网关。

如何选择这些主机传输类型 '用户可根据用途进行选择,选择方法说明如下:

ethertap – 如果用户想对主机网络进行访问,并且运行以2.2以前版本时,使用它。

TUN/TAP – 如果用户想访问主机网络,可使用它。TUN/TAP 运行在2.4以后的版本,比ethertap 更有效率。TUN/TAP 传输还能使用预置的设备,避免对uml_net辅助程序进行setuid操作。

Multicast – 如果用户期望建立一个纯虚拟网络,并且仅想建立UML,就使用它。

交换机后台 – 如果用户想建立一个纯虚拟网络,并且不介意为了得到较好执行效率而建立后台,就使用它。

slip – 没有特殊理由不要运行slip后端,除非ethertap和TUN/TAP不可用。

slirp – 如果用户在主机上没有root权限对建立网络进行访问,或者如果用户不想分配对UML分配IP时,使用它。

pcap – 对实际的网络连接没有太多用途,但用于主机上监管网络交通很有用。

(1)网络建立通用步骤

首先,用户必须已在UML中打开虚拟网络。如果运行下载的预编译的内核,则已打开虚拟网络。如果用户自己编译内核,则在配置界面上的"Network device support"菜单中,打开"Network device support"和三种传输选项。

下一步是提供网络设备给虚拟机,通过在内核命令行中进行描述,格式如下:

eth <n> = <transport> , <transport args>

例如:一个虚拟以太网设备可以如下粘附到一个主机ethertap上:

eth0=ethertap,tap0,fe:fd:0:0:0:1,192.168.0.254

上述语句在虚拟机内部建立eth0,粘附它自己到主机/dev/tap0,指定一个以太网地址,并指定给主机tap0接口一个IP地址。

一旦用户决定如何建立设备后,就可以启动UML、登录、配置设备的UML侧,并设置对外界的路由。此后,UML就可以与网络任何其他机器(物理或虚拟的)通信。

(2)用户空间后台

http://www.user-mode-linux.org/cvs/tools/下载工具uml_net和uml_switch,编译并安装。uml_switch是在UML系统之间管理虚拟网络的后台,而不用连接到主机系统的网络。uml_switch将在UNIX域的socket上监听连接,并在连接到UNIX域的客户端之间转发包。

(3)指定以太网地址

TUN/TAP, ethertap和daemon接口允许用户给虚拟以太网设备指定硬件地址。但通常不需要指定硬件地址。如果命令行没有指定硬件地址,它将提供地址为fe:fd:nn:nn:nn:nn,其中,nn.nn.nn.nn是设备IP地址。这种方法通常足够保证有唯一的硬件地址。

(4)UML接口建立

一旦用命令行描述网络设备,用户在启动UML和登录后,第一件事应是建立接口,方法如下:

UML# ifconfig ethn ip-address up

此时,用户应可以ping通主机。为了能查看网络,用户设置缺省的路由为到达主机,方法如下:

UML# route add default gw host ip

例如:主机IP为192.168.0.4,设置路由方法如下:

UML# route add default gw 192.168.0.4

注意:如果UML不能与物理以太网上其他主机通信,可能是因为网络路由自动建立,可以运行"route –n"查看路由,结果类似如下:

Destination Gateway Genmask Flags Metric Ref Use Iface

192.168.0.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0

掩码不是255.255.255.255,因此,就使用到用户主机的路由替换它,方法如下:

UML# route del -net 192.168.0.0 dev eth0 netmask 255.255.255.0

UML# route add -host 192.168.0.4 dev eth0

添加缺省的路由到主机,将允许UML与用户以太网上任何机器交换包。

(5)多播

在多个UML之间建立一个虚拟网络的最简单方法是使用多播传输。用户的系统必须在内核中打开多播(multicast),并且在主机上必须有一个多播能力的网络设备。通常它是eth0。

为了使用多播,运行两个UML,命令行带有"eth0=mcast"选项。登录后,用户在每个虚拟机上用不同的IP地址配置以太网设备,方法如下:

UML1# ifconfig eth0 192.168.0.254

UML2# ifconfig eth0 192.168.0.253

这两个虚拟机应能相互通信。

传输设置的整个命令行选项列出如下:

ethn=mcast,ethernet address,multicast address,multicast port,ttl

(6)TUN/TAP和uml_net辅助程序

TUN/TAP驱动程序实现了虚拟网卡的功能,TUN 表示虚拟的是点对点设备,TAP表示虚拟的是以太网设备,这两种设备针对网络包实施不同的封装。利用TUN/TAP驱动,可以将tcp/ip协议栈处理好的网络分包传给任何一个使用TUN/TAP驱动的进程,由进程重新处理后再发到物理链路中。

TUN/TAP是与主机交换包的较好机制,主机建立TUN/TAP较简单的方法是使用uml_net辅助程序,它包括插入tun.o内核模块、配置设备、建立转发IP、路由和代理ARP。

如果在设备的主机侧指定了IP地址,uml_net将在主机上做所有的建立工作。粘附设备到TUN/TAP设备的命令行格式列出如下:

eth <n> =tuntap,,, <host IP address>

例如:下面参数将粘附UML的eth0到下一个可用的tap设备,指定IP地址192.168.0.254么tap设备的主机侧,并指定一个基于IP地址的以太网地址。

eth0=tuntap,,,192.168.0.254

(7)带有预配置的tap设备的TUN/TAP

如果用户没有更好的uml_net,可以预先建立TUN/TAP。步骤如下:

用工具tunctl创建tap设备,方法如下:

host# tunctl -u uid

上述命令中,uid是用户的ID或UML将运行登录的用户名。

配置设备IP地址,方法如下:

host# ifconfig tap0 192.168.0.254 up

建立路由和ARP,方法如下:

host# bash -c 'echo 1 > /proc/sys/net/ipv4/ip_forward'

host# route add -host 192.168.0.253 dev tap0

host# bash -c 'echo 1 > /proc/sys/net/ipv4/conf/tap0/proxy_arp'

host# arp -Ds 192.168.0.253 eth0 pub

注意:这个配置没有重启机时失效,每次主机启动时,应重新设置它。最好的方法是用一个小应用程序,每次启动时,读出配置文件重新建立设置的配置。

使用网桥

为了不使用2个IP地址和ARP,还可通过对UML使用网桥提供对用户LAN直接访问,方法如下:

host# brctl addbr br0

host# ifconfig eth0 0.0.0.0 promisc up

host# ifconfig tap0 0.0.0.0 promisc up

host# ifconfig br0 192.168.0.1 netmask 255.255.255.0 up

host# brctl stp br0 off

host# brctl setfd br0 1

host# brctl sethello br0 1

host# brctl addif br0 eth0

host# brctl addif br0 tap0

注意:用户应该用eth0的IP地址通过ifconfig建立"br0"。

运行UML

一旦设备建立好后,运行UML,命令格式为: eth0=tuntap,devicename,例如:一个样例列出如下:

eth0=tuntap,tap0

如果用户不再使用tap设置,可以用下面命令删除它:

host# tunctl -d tap device

最后,tunctl有一个"-b"(用于简捷模式)切换,仅输出它所创建的tap设备的名字。它很适合于被一个脚本使用,方法如下:

host# TAP=`tunctl -u 1000 -b`

(8)交换机后台

交换机后台uml_switch以前称为uml_router,它提供了创建整个虚拟网络的机制。缺省下,它不提供对主机网络的连接。

首先,用户需要运行uml_switch,无参数运行时,表示它将监听缺省的unix域socket。使用选项"-unix socket"可指定不同的socket,"-hub"可将交换机后台变为集线器(Hub)。如果用户期望交换机后台连接到主机网络(允许UML访问通过主机访问外部的网络),可使用选项"-tap tap0"。

uml_switch还可作为后台运行,方法如下:

host% uml_switch [ options ] < /dev/null > /dev/null

内核命令行交换机的通用命令行格式列出如下:

ethn=daemon,ethernet address,socket type,socket

通常只需要指定参数"daemon",其他使用缺省参数,如果用户运行没有参数的交换机后台,在同一台机器上使用选项"eth0=daemon"运行UML,etho驱动程序会直接粘附它自己到交换机后台。参数socket为unix域socket的文件名,用于uml_switch和UML之间网络通信。

(9)Slirp

slirp通常使用外部程序/usr/bin/slirp,仅通过主机提供IP网络连接。它类似于防火墙的IP伪装,跃然传输有用户空间进行,而不是由内核进行。slirp不在主机上建立任何接口或改变路由。slirp在主机上不需要root权限或运行setuid。

slirp命令行的通用格式为:

ethn=slirp,ethernet address,slirp path

在UML上,用户应使用没有网关IP的etho设置缺省路由,方法如下:

UML# route add default dev eth0

slirp提供了UML可使用的大量有用IP地址,如:10.0.2.3,是DNS服务器的一个别名,定义在主机/etc/resolv.conf中,或者它是slirp的选项"dns"中给定的IP地址。

(10)pcap

pcap对网络上传输的数据包进行截获和过滤。通过命令行或pcap传输粘附到UML以太网设备uml_mconsole工具,语法格式如下:

ethn=pcap,host interface,filter expression,option1,option2

其中,expression和option1、option2是可选的。

这个接口是主机上用户想嗅探(sniff)的任何网络设备,过滤器表达式(filter expression)与工具tcpdump使用的一样,option1为"promisc"或"nopromisc",控制pcap是否将主机接口设为"promiscuous"(混杂)模式;option2为"optimize "或"nooptimize",表示是否使用pcap表达式优化器。

一个设置pcap的样例列出如下:

eth0=pcap,eth0,tcp

eth1=pcap,eth0,!tcp

上述语句将引起在主机eth0上的UML eth0将所有的tcp发出,并且在主机eth0上的UML eth1将发出所有非tcp包。

(11)用户建立主机

主机上的网络设备需要配置IP地址,还需要用值为1484的mtu配置tap设备。slip设置还需要配置点到点(pointopoint)地址,方法如下:

host# ifconfig tap0 arp mtu 1484 192.168.0.251 up

host# ifconfig sl0 192.168.0.251 pointopoint 192.168.0.250 up

如果正建立tap设备,就将路由设置到UML IP。方法如下:

UML# route add -host 192.168.0.250 gw 192.168.0.251

为了允许网络上其他主机看见这个虚拟机,化理ARP设置如下:

host# arp -Ds 192.168.0.250 eth0 pub

最后,将主机设置到路由包,方法如下:

host# echo 1 > /proc/sys/net/ipv4/ip_forward

在虚拟机间共享文件系统

在虚拟机间共享文件系统的方法是使用ubd(UML Block Device)块设备驱动程序的写拷贝(copy-on-write,COW)分层能力实现。COW支持在仅读的共享设备上分层读写私有设备。一个虚拟机的写数据存储在它的私有设备上,而读来自任一请求块有效的设备。如果请求的块有效,读取私有设备,如果无效,就读取共享设备。

用这种方法,数据大部分在多个虚拟机间共享,每个虚拟机有多个小文件用于存放虚拟机所做的修改。当大量UML从一个大的根文件系统启动时,这将节约大量磁盘空间。它还提供执行性能,因为主机可用较小的内存缓存共享数据,主机的内存而不是硬盘提供UML硬盘请求服务。

可通过简单地加COW文件的名字到合适的ubd,实现加一个COW层到存在的块设备文件。方法如下:

ubd0=root_fs_cow,root_fs_debian_22

上述语句中,"root_fs_cow"是私有的COW文件,"root_fs_debian_22"是存在的共享文件系统。COW文件不必要存在,如果它不存在,驱动程序将创建并初始化它。一旦COW文件已初始化,可在以命令行中使用它,方法如下:

ubd0=root_fs_cow

后备文件(backing file)的名字存在COW文件头中,因此在命令行中继续指定它将是多余的。

COW文件是稀疏的,因此它的长度不同于硬盘的实际使用长度。可以用命令"ls –ls"查看硬盘的实际消耗,用"ls –l"查看COW文件和后备文件(backing file)的长度,方法如下:

host% ls -l cow.debian debian2.2

-rw-r--r-- 1 jdike jdike 492504064 Aug 6 21:16 cow.debian

-rwxrw-rw- 1 jdike jdike 537919488 Aug 6 20:42 debian2.2

host% ls -ls cow.debian debian2.2

880 -rw-r--r-- 1 jdike jdike 492504064 Aug 6 21:16 cow.debian

525832 -rwxrw-rw- 1 jdike jdike 537919488 Aug 6 20:42 debian2.2

从上述显示结构,用户会发现COW文件实际硬盘消耗小于1M,面不是492M。

一旦文件系统用作一个COW文件的仅读后备文件,不要直接从它启动或修改它。这样,会使使用经的任何COW文件失效。后备文件在创建时它的修改时间mtime和大小size存放在COW文件头中,它们必须相匹配。如果不匹配,驱动程序将拒绝使用COW文件。

如果用户手动地改变后备文件或COW头,将得到一个崩溃的文件系统。

操作COW文件的方法说明如下:

(1)删除后备文件

由于UML存放后备文件名和它的修改时间mtime在COW头中,如果用户删除后文件文件,这些信息将变成无效的。因此,删除后备文件的步骤如下:

用保护时间戳的方式删除文件。通常,使用"-p"选项。拷贝操作命令"cp –a"中的"-a"隐含了"-p"。

通过启动UML更新COW头,命令行指定COW文件和新的后备文件的位置,方法如下:

ubda=COW file,新的后备文件位置

UML将注意到命令行和COW头之间的不匹配,检查新后文件路径的大小和修改时间mtime,并更新COW头。

如果当用户删除后备文件时忘记保留时间戳,用户可手动整理mtime,方法如下:

host% mtime=UML认定的修改时间mtime; \

touch --date="`date -d 1970-01-01\ UTC\ $mtime\ seconds`" 后备文件

注意如果对真正修改过而不是刚删除的后备文件进行上述操作,那么将会文件崩溃,用户将丢失文件系统。

(2)uml_moo :将COW文件与它的后备文件融合

依赖于用户如何使用UML和COW设备,系统可能建议每隔一段时间融合COW文件中的变化到后备文件中。用户可以用工具uml_moo完成该操作,方法如下:

host% uml_moo COW file new backing file

由于信息已在COW文件头中,因此,不必指定后备文件。

uml_moo在缺省下创建一个新的后备文件,它还有一个破坏性的融合选项,直接将融合COW文件到它当前的后备文件。当后备文件仅有一个COW文件与它相关时,该选项很有用。如果多个COW与一个后备文件相关,融合选项"-d"将使所有其他的COW无效。但是,如果硬盘空间不够时,使用融合选项"-d"很方便快捷,方法如下:

host% uml_moo -d COW file

(3)uml_mkcow :创建新COW文件

正常创建COW文件的方法是以UML命令行中指定一个不存在的COW文件,让UML创建COW文件。但是,用户有时想创建一个COW文件,但不想启动UML。此时,可以使用uml_mkcow工具。方法如下:

host% uml_mkcow 新COW文件 存在的后备文件

如果用户想销毁一个存在的COW文件,可以加"-f"选项强制重写旧的COW文件,方法如下:

host% uml_mkcow -f 存在的COW文件 存在的后备文件

创建UML的文件系统

如果根文件系统硬盘空间不够大,或者想使用不同于ext2的文件系统,用户就可能想创建和挂接新的UML文件系统,用户可以用如下方法创建UML的根文件系统:

(1)创建文件系统的文件

使用命令dd创建一个合适尺寸的空文件,用户可以创建稀疏文件,该文件直到实际使用时才分配硬盘空间。例如:下面的命令创建一个100M填满0的稀疏文件:

host% dd if=/dev/zero of=new_filesystem seek=100 count=1 bs=1M

(2)指定文件给一个UML设备

在UML命令行上加入下面的选项:

ubdd=new_filesystem

上述命令中,ubdd应确保没被使用。

(3)创建和挂接文件系统

创建和挂接文件系统方法如下:

host# mkreiserfs /dev/ubdd

UML# mount /dev/ubdd /mnt

主机文件访问

如果用户在UML中想访问主机上的文件,用户可将主机当作独立的机器,可以使用nfs从主机挂接目录,或者用scp和rcp拷贝文件到虚拟机,因为UML运行在主机上,它能象其他进程一样访问这些文件,并使它们在虚拟机内部可用,而不需要使用网络。

还可以使用hostfs虚拟文件系统,用户通过它可以挂接一个主机目录到UML文件系统,并像在主机上一样访问该目录中的文件。

(1)使用hostfs

首先,确认虚拟机内部是否有hostfs可用,方法如下:

UML# cat /proc/filesystems

如果没有列出hostfs,则需要重编译内核,配置hostfs,将它编译成一个内核模块,并用"insmod"插入该内核模块。

挂接hostfs文件系统,例如:将hostfs挂接到虚拟机的/mnt/host下,方法如下:

UML# mount none /mnt/host -t hostfs

如果用户不想挂接主机的root目录,他可以用"-o"选项指定挂接的子目录。例如:挂接主机的/home到虚拟机的/mnt/home,方法如下:

UML# mount none /mnt/home -t hostfs -o /home

(2)hostfs命令行选项

在UML命令行选项可使用hostfs选项,用来指定多个hostfs挂接到一个主机目录或阻止hostfs用户从主机上销毁数据,方法如下:

hostfs=directory,options

当前可用的选项是"append",用来阻止所有的文件在追加方式打开,并不允许删除文件。

(3)hostfs作为根文件系统

还可以通过hostfs从主机上的目录而不是在一个文件中的标准文件系统启动UML。最简单的方法是用loop挂接一个存在的root_fs文件,方法如下:

host# mount root_fs uml_root_dir -o loop

用户需要将/etc/fstab中的文件类型改变为"hostfs",fstab中的该行列出如下:

none / hostfs defaults 1 1

接着用户可以用chown将目录中root拥有的所有文件改变为用户拥有,方法如下:

host# find . -uid 0 -exec chown user {} \;

如果用户不想用上面的命令改变文件属主,用户可以用root身份运行UML。

接着,确保UML内核编译进hostfs,而不是以内核模块方式包含hostfs。那么,加入下面的命令行运行UML:

root=/dev/root rootflags=/path/to/uml/root rootfstype=hostfs

加入上述选项后,UML应该像正常的一样启动。

(4)编译hostfs

如果hostfs不在内核中,用户需要编译hostfs,用户可以将它编译进内核或内核模块。用户在内核配置界面上选项hostfs,并编译和安装内核。

内核调试

因为UML运行为正常的Linux进程,用户可以用gdb像调试其他进程一样调试内核,稍微不同的是:因为内核的线程已用系统调用ptrace进行拦截跟踪,因此,gdb不能ptrace它们。UML已加入了解决此问题的机制。

为了调试内核,用户需要从源代码编译,确保打开CONFIG_DEBUGSYM和CONFIG_PT_PROXY配置选项。它们分别用来确保编译内核带有"-g"选项和打开ptrace代理,以便gdb能与UML一起工作调试内核。

(1)在gdb下启动内核

用户可以在命令行中放入"debug"选项,在启动UML时将内核放在gdb的控制之下。用户可以得到一个运行gdb的xterm,内核将送一些命令到gdb,停在"start_kernel"处,用户可以输入"next", "step"或"cont"运行内核。

(2)检查睡眠的进程

并非每个bug在当前运行的进程中,有时候,当进程在信号量上或其他类似原因死锁时,原本不应该挂起的进程在内核中挂起。这种情况下,用户在gdb中用"Ctrl+C"时,得到一个跟踪栈,用户将可以看见到不相关的空闲线程。

用户本想看到的是不应该睡眠的进程的栈,为了看到睡眠的进程,用户可以在主机上用命令ps得到该进程的主机进程id。

用户将gdb与当前线程分离,方法如下:

(UML gdb) det

然后将gdb粘附到用户感兴趣的线程上,方法如下:

(UML gdb) att <host pid>

查看该线程的栈,方法如下:

(UML gdb) bt

(3)在UML上运行ddd

ddd可以工作于UML,用户可以主机上运行ddd,它给gdb提供了图形界面。运行ddd的步骤如下:

启动ddd,方法如下:

host% ddd linux

得到gdb的pid

用命令ps可以得到ddd启动的gdb的pid。

运行UML

在运行UML的命令行中加上选项"debug=parent gdb-pid=<pid>",启动并登录UML。

在ddd的gdb命令行中输入"att 1",gdb显示如下:

0xa013dc51 in __kill ()

(gdb)

在gdb中输入"c",UML将继续运行,用户可接着像调试其他进程一样调试了。

(4)调试内核模块

gdb已支持调试动态装载入进程的代码,这需要在UML下调试内核模块。调试内核模块有些复杂,用户需要告诉gdb装入UML的对象文件名以及它在内存中的位置。接着,它能读符号表,并从装载地址指出所有的符号。

当用户在rmmod内核模块后重装载它时,可得到更多信息。用户必须告诉gdb忘记所有它的符号,包括主UML的符号,接着再装载回所有的符号。

用户可以使用脚本umlgdb进行内核模块的重装载和读取它的符号表。用户还可以手动进行一步步处理完成符号表的获取工作。下面分别说明这两种方法。

1)运行脚本umlgdb调试内核模块

运行脚本umlgd较容易获取内核模块的符号表。

首先,用户应告诉内核模块所在的位置,在脚本中有一个列表类似如下:

set MODULE_PATHS {

"fat" "/usr/src/uml/linux-2.6.18/fs/fat/fat.ko"

"isofs" "/usr/src/uml/linux-2.6.18/fs/isofs/isofs.ko"

"minix" "/usr/src/uml/linux-2.6.18/fs/minix/minix.ko"

}

用户将上述列表改为将调试的内核模块的路径,接着,从UML的顶层目录运行该脚本,显示如下:

                • GDB pid is 21903 ********

Start UML as: ./linux <kernel switches> debug gdb-pid=21903

GNU gdb 5.0rh-5 Red Hat Linux 7.1

Copyright 2001 Free Software Foundation, Inc.

GDB is free software, covered by the GNU General Public License, and you are

welcome to change it and/or distribute copies of it under certain conditions.

Type "show copying" to see the conditions.

There is absolutely no warranty for GDB. Type "show warranty" for details.

This GDB was configured as "i386-redhat-linux"...

(gdb) b sys_init_module

Breakpoint 1 at 0xa0011923: file module.c, line 349.

(gdb) att 1

在用户运行UML后,用户只需要在"att 1"按回车,并继续执行它。方法如下:

Attaching to program: /home/jdike/linux/2.4/um/./linux, process 1

0xa00f4221 in __kill ()

(UML gdb) c

Continuing.

此时,当用户用insmod插入内核模块,显示列出如下:

      • Module hostfs loaded ***

Breakpoint 1, sys_init_module (name_user=0x805abb0 "hostfs",

mod_user=0x8070e00) at module.c:349

349 char *name, *n_name, *name_tmp = NULL;

(UML gdb) finish

Run till exit from #0 sys_init_module (name_user=0x805abb0 "hostfs",

mod_user=0x8070e00) at module.c:349

0xa00e2e23 in execute_syscall (r=0xa8140284) at syscall_kern.c:411

411 else res = EXECUTE_SYSCALL(syscall, regs);

Value returned is $1 = 0

(UML gdb)

p/x (int)module_list + module_list->size_of_struct

$2 = 0xa9021054

(UML gdb) symbol-file ./linux

Load new symbol table from "./linux" ' (y or n) y

Reading symbols from ./linux...

done.

(UML gdb)

add-symbol-file /home/jdike/linux/2.4/um/arch/um/fs/hostfs/hostfs.o 0xa9021054

add symbol table from file "/home/jdike/linux/2.4/um/arch/um/fs/hostfs/hostfs.o" at

.text_addr = 0xa9021054

(y or n) y

Reading symbols from /home/jdike/linux/2.4/um/arch/um/fs/hostfs/hostfs.o...

done.

(UML gdb) p *module_list

$1 = {size_of_struct = 84, next = 0xa0178720, name = 0xa9022de0 "hostfs",

size = 9016, uc = {usecount = {counter = 0}, pad = 0}, flags = 1,

nsyms = 57, ndeps = 0, syms = 0xa9023170, deps = 0x0, refs = 0x0,

init = 0xa90221f0 <init_hostfs>, cleanup = 0xa902222c <exit_hostfs>,

ex_table_start = 0x0, ex_table_end = 0x0, persist_start = 0x0,

persist_end = 0x0, can_unload = 0, runsize = 0, kallsyms_start = 0x0,

kallsyms_end = 0x0,

archdata_start = 0x1b855 <Address 0x1b855 out of bounds>,

archdata_end = 0xe5890000 <Address 0xe5890000 out of bounds>,

kernel_data = 0xf689c35d <Address 0xf689c35d out of bounds>}

>> Finished loading symbols for hostfs ...

(2)手动调试内核模块

在调试器中启动内核,并用insmod或modprobe装载内核模块。在gdb中执行下面命令:

(UML gdb) p module_list

这是已装载进内核的内核模块列表,通常用户期望的内核模块在module_lis中。如果不在,就进入下一个链接,查看name域,直到找到用户调试的内核模块。获取该结构的地址,并加上module.size_of_struct值,gdb可帮助获取该值,方法如下:

(UML gdb) printf "%#x\n", (int)module_list module_list->size_of_struct

从内核模块开始处的偏移偶尔会改变,因此,应检查init和cleanup的地址,方法如下:

(UML gdb) add-symbol-file /path/to/module/on/host that_address

如果断点不在正确的位置或不工作等 ,用户可以查看内核模块结构,init和cleanup域应该类似如下:

init = 0x588066b0 <init_hostfs>, cleanup = 0x588066c0 <exit_hostfs>

如果名字正确,但它们有偏移,那么,用户应该将偏移加到add-symbol-file所在地址上。

当用户想装载内核模块的新版本时,需要让gdb删除旧内核模块的所有符号。方法如下:

(UML gdb) symbol-file

接着,从内核二进制重装载符号,方法如下:

(UML gdb) symbol-file /path/to/kernel

然后,重复上面的装载符号过程。还需要重打开断点。

(5)粘附gdb到内核

如果用户还没有在gdb下运行内核,用户可以通过给跟踪线程发送一个SIGUSR1,用于以后粘附gdb到内核。控制台第一行的输出鉴别它的id,显示类似如下:

tracing thread pid = 20093

发送信号的方法如下:

host% kill -USR1 20093

上述命令运行后,用户将可看见带有gdb运行的xterm。

如果用户已将mconsole(UML的控制台)编译进UML,那么可用mconsole客户端启动gdb,方法如下:

(mconsole) (mconsole) config gdb=xterm

上述命令运行后,用户将可看见带有gdb运行的xterm。

(6)使用可替换的调试器

UML支持粘附到一个已运行的调试器,而不是启动gdb本身。当gdb是一些UI的子进程(如:emacs或ddd)时,这将是有用的。它还被用于在UML上运行非gdb的调试器。下面是一个使用strace作为可替代调试器的例子。

用户需要得到调试器的pid,并将pid用"gdb-pid=<pid>"选项与"debug"选项一起传递。

如果用户在UI下使用gdb,那么,应告诉UML"att 1",那么,UI将粘附到UML。

下面以替换调试器strace为例,用户可以用strace调试实际的内核,方法如下:

在shell中运行下述命令

host%

sh -c 'echo pid=$$; echo -n hit return; read x; exec strace -p 1 -o strace.out'

用"debug"和"gdb-pid=<pid>"运行UML。

strace输出将出现在输出文件中。

注意:运行下面的命令,结果不同于前面命令。

host% strace ./linux

上述命令将仅strace主UML线程,跟踪的线程不做任何实际的内核操作。它仅标识出虚拟机。而使用上述的strce将显示虚拟机低层的活动情况。

断言语句

在代码里面老能看到 BUG_ON() , WARN_ON() 这样的宏 , 类似 我们日常编程里面的断言(assert) 。

在include/asm-generic/bug.h

#ifdef CONFIG_BUG
 
#ifdef CONFIG_GENERIC_BUG
#ifndef __ASSEMBLY__
struct bug_entry {
	unsigned long	bug_addr;
#ifdef CONFIG_DEBUG_BUGVERBOSE
	const char	*file;
	unsigned short	line;
#endif
	unsigned short	flags;
};
#endif		/* __ASSEMBLY__ */
 
#define BUGFLAG_WARNING	(1<<0)
#endif	/* CONFIG_GENERIC_BUG */
 
#ifndef HAVE_ARCH_BUG
#define BUG() do { \
	printk("BUG: failure at %s:%d/%s()!\n", __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
 
#ifndef __WARN
#ifndef __ASSEMBLY__
extern void warn_on_slowpath(const char *file, const int line);
#define WANT_WARN_ON_SLOWPATH
#endif
#define __WARN() warn_on_slowpath(__FILE__, __LINE__)
#endif
 
#ifndef WARN_ON
#define WARN_ON(condition) ({						\
	int __ret_warn_on = !!(condition);				\
	if (unlikely(__ret_warn_on�					\
		__WARN();						\
	unlikely(__ret_warn_on);					\
})
#endif
 
#else /* !CONFIG_BUG */
#ifndef HAVE_ARCH_BUG
#define BUG()
#endif
 
#ifndef HAVE_ARCH_BUG_ON
#define BUG_ON(condition) do { if (condition) ; } while(0)
#endif
 
#ifndef HAVE_ARCH_WARN_ON
#define WARN_ON(condition) ({						\
	int __ret_warn_on = !!(condition);				\
	unlikely(__ret_warn_on);					\
})
#endif
#endif
 
#define WARN_ON_ONCE(condition)	({				\
	static int __warned;					\
	int __ret_warn_once = !!(condition);			\
								\
	if (unlikely(__ret_warn_once�				\
		if (WARN_ON(!__warned� 			\
			__warned = 1;				\
	unlikely(__ret_warn_once);				\
})
 
#ifdef CONFIG_SMP
# define WARN_ON_SMP(x)			WARN_ON(x)
#else
# define WARN_ON_SMP(x)			do { } while (0)
#endif

同步锁调试

锁验证器

内核锁验证器(Kernel lock validator)可以在死锁发生前检测到死锁,即使是很少发生的死锁。它将每个自旋锁与一个键值相关,相似的锁仅处理一次。加锁时,查看所有已获取的锁,并确信在其他上下文中没有已获取的锁,在新获取锁之后被获取。解锁时,确信正被解开的锁在已获取锁的顶部。

.

Validate spinlocks vs interrupts behavior.

当加锁动态发生时,锁验证器映射所有加锁规则,该检测由内核的spinlocks、rwlocks、mutexes和rwsems等锁机制触发。不管何时锁合法性检测器子系统检测到一个新加锁场景,它检查新规则是否违反正存在的规则集,如果新规则与正存在的规则集一致,则加入新规则,内核正常运行。如果新规则可能创建一个死锁场景,那么这种创建死锁的条件会被打印出来。

当判断加锁的有效性时,所有可能的"死锁场景"会被考虑到:假定任意数量的CPU、任意的中断上下文和任务上下文群、运行所有正存在的加锁场景的任意组合。在一个典型系统中,这意味着有成千上万个独立的场景。这就是为什么称它为"加锁正确性"验证器,对于所有被观察的规则来说,锁验证器用数学的确定性证明死锁不可能发生,假定锁验证器实现本身正确,并且它内部的数据结构不会被其他内核子系统弄坏。

还有,验证器的属性"所有可能的场景"也使查找变得复杂,特别是多CPU、多上下文竞争比单个上下文规则复杂得多,

为了增加验证器的效率,不是将每个锁实例进行映射,而是映射每个锁类型。例如:内核中所有的结构inode对象有inode->inotify_mutex,如果缓存了10000个inode,将会有10000个锁对象。但->inotify_mutex是单个锁类型,所有->inotify_mutex发生的加锁活动都归入单个锁类型。

Lock-class


验证器操作的基本对象是锁类Lock-class,一个锁类是一组锁,逻辑上有同样的加锁规则,尽管锁可能有多个实例。例如:在结构inode中的一个锁是一个类,而每个节点有它自己的锁类实例。

验证器跟踪锁类的状态和不同锁类之间的依赖性。验证器维护一个有关状态和依赖性是否正确的滚动证据。

不像一个锁实例,锁类lock-class它本身从不消失:当lock-class注册使用后,所有随后锁类的使用都会被附加到该lock-class上。

http://www.ahbank.org/

 

 

内核调试的方法

驱动程序的调试
一. 打印: prink, 自制proc文件
UBOOT传入console=ttySAC0 console=tty1
1. 内核处理UBOOT传入的参数
console_setup
add_preferred_console // 我想用名为"ttySAC0"的控制台,先记录下来


2. 硬件驱动的入口函数里:
drivers/serial/s3c2410.c
register_console(&s3c24xx_serial_console);


3. printk
vprintk
/* Emit the output into the temporary buffer */
// 先把输出信息放入临时BUFFER
vscnprintf

// Copy the output into log_buf.
// 把临时BUFFER里的数据稍作处理,再写入log_buf
// 比如printk("abc")会得到"<4>abc", 再写入log_buf
// 可以用dmesg命令把log_buf里的数据打印出来重现内核的输出信息


// 调用硬件的write函数输出
release_console_sem();
call_console_drivers(_con_start, _log_end);
// 从log_buf得到数据,算出打印级别
_call_console_drivers(start_print, cur_index, msg_level);
// 如果可以级别够格打印
if ((msg_log_level < console_loglevel
__call_console_drivers
con->write(con, &LOG_BUF(start), end - start);








二. 根据内核打印的段错误信息分析


oops信息 : 单词oops的含义是“惊讶”,当内核出错时(比如访问非法地址),打印出来的信息被称为oops信息。

a). 作为模块:


1,根据PC值,找到导致错误的指令。
pc = 0x00000000 它属于什么的地址?是内核的地址,还是通过insmod加载的驱动程序的地址?
先判断是否属于内核的地址 : 看 内核编译makefile目录下的 System.map(编译完内核都会发现在内核根目录下面多出来一个System.map文件)
确定内核的函数的地址范围 : c0004000~c03faa94。
所以可以确定 : 导致错误的指令不在内核的地址范围,则它属于insmod加载的驱动程序的地址范围。


2,假设它的加载的驱动程序引入的错误。那又怎么确定是哪一个驱动程序?
有时候 Modules linked in: 会指明是哪个驱动程序,但是很多时候加载的驱动程序很多,是不会指明具体是哪个。
所以还是需要根据PC值来确定究竟是哪个驱动程序。
先看看加载的驱动程序的地址范围。
在开发板目录下 : cat /proc/kallsyms >> kallsyms.txt (内核函数的地址、加载的函数的地址)
kallsyms.txt文件中的内容介绍 //T : 表示全局函数 t : 表示静态函数


从这些信息里找到一个相近的地址, 这个地址<=0xbf000018
比如找到了:
bf000000 t first_drv_open [first_drv]


3. 找到了first_drv.ko
在PC上反汇编它: arm-linux-objdump -D lcd.ko > lcd.dis
在dis文件里找到first_drv_open


   first_drv.dis文件里              insmod后
00000000 :       bf000000 t first_drv_open [first_drv]
00000018                         pc = bf000018
                                
18: e5923000 ldr r3, [r2]    //r2的值在下面可以找到,是56000050
此时,要通过汇编语言来找到对应的c语言的语句。考验汇编能力的时候。


./firstdrvtest on
//1,一段文本描述信息
Unable to handle kernel paging request at virtual address 56000050
内核使用56000050来访问时发生了错误
pgd = c3eb0000
[56000050] *pgd=00000000


//2,oops信息的序号,#1,表示是第1次。
Internal error: Oops: 5 [#1]

//3,内核中加载的模块的名称
Modules linked in: first_drv

//4,发送错误时,CPU的序号,对于单处理器系统,序号为0。
CPU: 0    Not tainted  (2.6.22.6 #1)

//5,PC就是发生错误时,指令的地址。
//大多时候,PC值只会给出一个地址,不会指示说是在哪个函数里面。
PC is at first_drv_open+0x18(该指令的偏移)/0x3c(该函数的总大小) [first_drv]
PC就是发生错误的指令的地址
大多时候,PC值只会给出一个地址,不到指示说是在哪个函数里


//__init_begin = c0008000, PC=__init_begin+0x3fff8000=
//6,LR寄存器的值。
LR is at chrdev_open+0x14c/0x164
LR寄存器的值


//7,发送错误时,CPU各个寄存器的值。
pc = 0xbf000018


pc : []    lr : []    psr: a0000013
sp : c3c7be88  ip : c3c7be98  fp : c3c7be94
r10: 00000000  r9 : c3c7a000  r8 : c049abc0
r7 : 00000000  r6 : 00000000  r5 : c3e740c0  r4 : c06d41e0
r3 : bf000000  r2 : 56000050  r1 : bf000964  r0 : 00000000
执行这条导致错误的指令时各个寄存器的值


Flags: NzCv  IRQs on  FIQs on  Mode SVC_32  Segment user
Control: c000717f  Table: 33eb0000  DAC: 00000015

//8,发生错误时,当前进程是它,并不是说发生错误的是这个进程
Process firstdrvtest (pid: 777, stack limit = 0xc3c7a258)
//发生错误时当前进程的名称是firstdrvtest


//9,栈信息
Stack: (0xc3c7be88 to 0xc3c7c000)
be80:                   c3c7bebc c3c7be98 c008d888 bf000010 00000000 c049abc0 
bea0: c3e740c0 c008d73c c0474e20 c3e766a8 c3c7bee4 c3c7bec0 c0089e48 c008d74c 
bec0: c049abc0 c3c7bf04 00000003 ffffff9c c002c044 c3d10000 c3c7befc c3c7bee8 
bee0: c0089f64 c0089d58 00000000 00000002 c3c7bf68 c3c7bf00 c0089fb8 c0089f40 
bf00: c3c7bf04 c3e766a8 c0474e20 00000000 00000000 c3eb1000 00000101 00000001 
bf20: 00000000 c3c7a000 c04a7468 c04a7460 ffffffe8 c3d10000 c3c7bf68 c3c7bf48 
bf40: c008a16c c009fc70 00000003 00000000 c049abc0 00000002 bec1fee0 c3c7bf94 
bf60: c3c7bf6c c008a2f4 c0089f88 00008520 bec1fed4 0000860c 00008670 00000005 
bf80: c002c044 4013365c c3c7bfa4 c3c7bf98 c008a3a8 c008a2b0 00000000 c3c7bfa8 
bfa0: c002bea0 c008a394 bec1fed4 0000860c 00008720 00000002 bec1fee0 00000001 
bfc0: bec1fed4 0000860c 00008670 00000002 00008520 00000000 4013365c bec1fea8 
bfe0: 00000000 bec1fe84 0000266c 400c98e0 60000010 00008720 00000000 00000000 


//10,栈回溯信息,可以从中看出函数调用关系:从最后一个函数 sys_init_module 开始,向上可以找到函数调用的关系。
//可以通过内核配置信息 make menuconfig 来指定是否输出 栈回溯信息。
Backtrace: (回溯)
[] (first_drv_open+0x0/0x3c [first_drv]) from [] (chrdev_open+0x14c/0x164)
[] (chrdev_open+0x0/0x164) from [] (__dentry_open+0x100/0x1e8)
 r8:c3e766a8 r7:c0474e20 r6:c008d73c r5:c3e740c0 r4:c049abc0
[] (__dentry_open+0x0/0x1e8) from [] (nameidata_to_filp+0x34/0x48)
[] (nameidata_to_filp+0x0/0x48) from [] (do_filp_open+0x40/0x48)
 r4:00000002
[] (do_filp_open+0x0/0x48) from [] (do_sys_open+0x54/0xe4)
 r5:bec1fee0 r4:00000002
[] (do_sys_open+0x0/0xe4) from [] (sys_open+0x24/0x28)
[] (sys_open+0x0/0x28) from [] (ret_fast_syscall+0x0/0x2c)
Code: e24cb004 e59f1024 e3a00000 e5912000 (e5923000) 
Segmentation fault



b). 编入内核
Modules linked in:
CPU: 0    Not tainted  (2.6.22.6 #2)
PC is at first_drv_open+0x18/0x3c
LR is at chrdev_open+0x14c/0x164
pc : []    lr : []    psr: a0000013
sp : c3a03e88  ip : c3a03e98  fp : c3a03e94
r10: 00000000  r9 : c3a02000  r8 : c03f3c60
r7 : 00000000  r6 : 00000000  r5 : c38a0c50  r4 : c3c1e780
r3 : c014e6a8  r2 : 56000050  r1 : c031a47c  r0 : 00000000
Flags: NzCv  IRQs on  FIQs on  Mode SVC_32  Segment user
Control: c000717f  Table: 339f0000  DAC: 00000015
Process firstdrvtest (pid: 750, stack limit = 0xc3a02258)


1. 根据pc值确定该指令属于内核还是外加的模块
pc = c014e6c0 它属于什么的地址?是内核的地址,还是通过insmod加载的驱动程序的地址?
先判断是否属于内核的地址 : 看 内核编译makefile目录下的 System.map(编译完内核都会发现在内核根目录下面多出来一个System.map文件)
确定内核的函数的地址范围 : c0004000~c03faa94。
所以可以确定 : 导致错误的指令不在内核的地址范围,则它属于insmod加载的驱动程序的地址范围。


2. 反汇编内核: arm-linux-objdump -D vmlinux > vmlinux.dis
/*
vmlinux是未压缩的内核,vmlinux 是ELF文件,即编译出来的最原始的文件。用于kernel-debug,产生system.map符号表,不能用于直接加载,不可以作为启动内核。只是启动过程中的中间媒体
vmlinuz是可引导的、压缩的内核。“vm”代表“Virtual Memory”。Linux 支持虚拟内存,不像老的操作系统比如DOS有640KB内存的限制
*/


first_drv.dis文件中:搜c014e6c0
c014e6a8 :
c014e6a8:       e1a0c00d        mov     ip, sp
c014e6ac:       e92dd800        stmdb   sp!, {fp, ip, lr, pc}
c014e6b0:       e24cb004        sub     fp, ip, #4      ; 0x4
c014e6b4:       e59f1024        ldr     r1, [pc, #36]   ; c014e6e0 <.text+0x1276e0>
c014e6b8:       e3a00000        mov     r0, #0  ; 0x0
c014e6bc:       e5912000        ldr     r2, [r1]
c014e6c0:       e5923000        ldr     r3, [r2] // 在此出错 r2=56000050,在下面可以找到






c).根据栈信息分析函数调用过程(作为模块 或者 编入内核都可,下面的实验是作为模块)
# ./firstdrvtest on
Unable to handle kernel paging request at virtual address 56000050
pgd = c3e78000
[56000050] *pgd=00000000
Internal error: Oops: 5 [#1]
Modules linked in: first_drv
CPU: 0    Not tainted  (2.6.22.6 #48)
PC is at first_drv_open+0x18/0x3c [first_drv]
LR is at chrdev_open+0x14c/0x164
pc : []    lr : []    psr: a0000013
1 根据PC确定出错位置,是在内核,还是在模块中,前面有详细的分析System.map
bf000018 属于 insmod的模块
bf000000 t first_drv_open       [first_drv]


2 确定它属于哪个函数
有时候 Modules linked in: 会指明是哪个驱动程序,但是很多时候加载的驱动程序很多,是不会指明具体是哪个。
所以还是需要根据PC值来确定究竟是哪个驱动程序。
先看看加载的驱动程序的地址范围。
在开发板目录下 : cat /proc/kallsyms >> kallsyms.txt (内核函数的地址、加载的函数的地址)
kallsyms.txt文件中的内容介绍 //T : 表示全局函数 t : 表示静态函数


3 找到了first_drv.ko
在PC上反汇编它: arm-linux-objdump -D first_drv.ko > first_drv.dis
在dis文件里找到first_drv_open


   first_drv.dis文件里              insmod后
00000000 :       bf000000 t first_drv_open [first_drv]
00000018                         pc = bf000018


first_drv.dis文件中:
c014e6a8 :
c014e6a8:       e1a0c00d        mov     ip, sp
c014e6ac:       e92dd800        stmdb   sp!, {fp, ip, lr, pc} 
//从这里可以看见 first_drv_open函数的栈信息,保存有4*4个字节






sp : c3e69e88  ip : c3e69e98  fp : c3e69e94
r10: 00000000  r9 : c3e68000  r8 : c0490620
r7 : 00000000  r6 : 00000000  r5 : c3e320a0  r4 : c06a8300
r3 : bf000000  r2 : 56000050  r1 : bf000964  r0 : 00000000
Flags: NzCv  IRQs on  FIQs on  Mode SVC_32  Segment user
Control: c000717f  Table: 33e78000  DAC: 00000015
Process firstdrvtest (pid: 752, stack limit = 0xc3e68258)


可以仅根据栈信息,来确定函数的调用关系。栈就是一块内存
分析如下 : 
Stack: (0xc3e69e88 to 0xc3e6a000)
9e80:                   c3e69ebc c3e69e98 c008c888 bf000010 00000000 c0490620 
fp         ip   返回地址=lr   pc
栈信息开始部分的4个数据 是first_drv_open 的 栈                chrdev_open is sp
first_drv_open执行完之后,返回地址lr=c008c888,再在 vmlinux.dis 中找到 调用者的函数。
 4个数据后面的数据,就是它的调用者的函数的栈空间中的数据。
9ea0: c3e320a0 c008c73c c0465e20 c3e36cb4 c3e69ee4 c3e69ec0 c0088e48 c008c74c 
                                                            lr=c0088e48      
                                                                                 
9ec0: c0490620 c3e69f04 00000003 ffffff9c c002b044 c06e0000 c3e69efc c3e69ee8 
      __dentry_open 的栈


9ee0: c0088f64 c0088d58 00000000 00000002 c3e69f68 c3e69f00 c0088fb8 c0088f40 
      lr=c0088f64       nameidata_to_filp的栈               lr=c0088fb8
      
9f00: c3e69f04 c3e36cb4 c0465e20 00000000 00000000 c3e79000 00000101 00000001 
      do_filp_open的栈


9f20: 00000000 c3e68000 c04c1468 c04c1460 ffffffe8 c06e0000 c3e69f68 c3e69f48 
9f40: c008916c c009ec70 00000003 00000000 c0490620 00000002 be94eee0 c3e69f94 
9f60: c3e69f6c c00892f4 c0088f88 00008520 be94eed4 0000860c 00008670 00000005 
               lr=c00892f4       do_sys_open的栈


9f80: c002b044 4013365c c3e69fa4 c3e69f98 c00893a8 c00892b0 00000000 c3e69fa8 
                                          lr=c00893a8       sys_open的栈


9fa0: c002aea0 c0089394 be94eed4 0000860c 00008720 00000002 be94eee0 00000001 
      lc=002aea0        ret_fast_syscall的栈
                        
9fc0: be94eed4 0000860c 00008670 00000002 00008520 00000000 4013365c be94eea8 
9fe0: 00000000 be94ee84 0000266c 400c98e0 60000010 00008720 00000000 00000000 

ret_fast_syscall()函数是被谁调用的,我们这里还不需要详细了解,只要知道是应用程序是通过swi中断来加入内核的。
注意 : 上面的信息,从下往上,从大地址到小地址方向。是从栈底开始,往栈顶方向打印栈中的值。也是函数调用的方向。














































三. 修改内核来定位系统僵死问题
1,\kernel-2.6.13\arch\arm\kernel\irq.c  //修改这个内核文件
修改这个文件的原因 : 只要系统还在运行,即使某个驱动程序卡死了,系统时钟中断是绝对不会停歇的。
                 所以,可以在系统中断代码中加入一些调试代码,来帮助我们找到bug。
/*
 * do_IRQ handles all hardware IRQ's.  Decoded IRQs should not
 * come via this function.  Instead, they should provide their
 * own 'handler'
 */
asmlinkage void asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
{
struct irqdesc *desc = irq_desc + irq;


static pid_t pre_pid;
static int count = 0; 


/*
* Some hardware gives randomly wrong interrupts.  Rather
* than crashing, do something sensible.
*/
if (irq >= NR_IRQS)
desc = &bad_irq_desc;

if(irq == 30) /*系统时钟中断*/
{
/*如果10秒之内,都是同一个进程在运行,就打印卡死信息。*/


/*静态局部变量:
只有在这个函数中能访问,但是生命周期是和全局变量差不多的,函数退出之后变量还在,
而且只在第一次进入的时候做初始化,以后会跳过初始化语句,保留原来的值*/

if(pre_pid == current->pid) /*当前进程 等于 之前记录下来的进程号*/
{
count++;
}
else /*当前进程 不等于 之前记录下来的进程号*/
{
count = 0;
pre_pid = current->pid; /*把新的当前进程记录下来。*/
}

if(count == 10*HZ) /*累计达到10秒的时候*/
{
count = 0;
/*明确是在哪个进程导致卡死的,明确PC值*/
printk("asm_do_IRQ==>s3c2410_timer_interrupt : pid=[%d], task_name=[%s], PC=[0x%08x]\n", current->pid, current->comm, regs->ARM_pc);
}
}


irq_enter();
spin_lock(&irq_controller_lock);
desc->handle(irq, desc, regs);


/*
* Now re-run any pending interrupts.
*/
if (!list_empty(&irq_pending))
do_pending_irqs(regs);


irq_finish(irq);


spin_unlock(&irq_controller_lock);
irq_exit();
}


重新编译内核 make clean;make
重新启动这个新的内核




2, 在这个驱动文件中加入死循环,来模拟系统僵死问题 \nfs_2.6.13\wxc\driver\chardriver\led\leddriver2.c
static int leddriver2_ioctl( struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
//printk("DEVICE:mydriver2_ioctl Called!\n");
printk("DEVICE:cmd=[%d], arg=[%ld]\n", cmd, arg);

while(1);  //故意加的

if(cmd == 0) //所有的灯全亮2s
{
....
}

return 0;
}


启动待测试的驱动程序
# insmod leddriver2.ko 
[kernel/sys.c][notifier_call_chain][175]
[kernel/sys.c][notifier_call_chain][187]
DEVICE:leddriver2_init
DEVICE:register leddriver2 OK! Major = [253]

# mknod /dev/leddriver2 c 253 0
[root@EmbedSky ko]# ls -lrt /dev/led*
crw-r--r--    1 root     root     253,   0 Jan  1 00:03 /dev/leddriver2


启动应用程序
# ./leddrivertest2 0 3
APP:open fd=[3]
APP:led_no=[0] time=[3]
DEVICE:cmd=[0], arg=[3]  //会发现驱动程序卡死在这里了,无论怎么也退出不了,下面打印出来了很多关键信息。
asm_do_IRQ==>s3c2410_timer_interrupt : pid=[806], task_name=[leddrivertest2], PC=[0xbf000044]
asm_do_IRQ==>s3c2410_timer_interrupt : pid=[806], task_name=[leddrivertest2], PC=[0xbf000044]
PC=0xbf000044 


3,定位僵死发生的代码的具体位置
3.1), PC=0xbf000044
从这些信息里找到一个相近的地址, 这个地址<=0xbf000044 
前面有分析 可知 地址 0xbf000044 不属于内核地址空间,属于外加的内核。
在开发板目录下 : 
# cat /proc/kallsyms >> kallsyms.txt
在这个文件下面,查找 地址 0xbf000044 大概在什么函数
bf000000 t $a [leddriver2]
bf000048 t $d [leddriver2]   // 对于中断, pc-4=0xbf000044 才是发生中断瞬间的地址,说明发生中断的驱动程序是 : leddriver2.ko。


bf002000 t leddriver2_init [leddriver2]
bf00004c t leddriver2_exit [leddriver2]
c481e178 ? __mod_license202 [leddriver2]
c481e184 ? __mod_description201 [leddriver2]
c481e1a4 ? __mod_author200 [leddriver2]
bf0008a0 b $d [leddriver2]
bf0008a4 b MYDRIVER_Major [leddriver2]
bf000698 d leddriver2_fops [leddriver2]
bf000698 d $d [leddriver2]
bf000028 t leddriver2_ioctl [leddriver2] //bf000028 是与 0xbf000044 最近的地址。说明 : 发生中断的驱动程序是 : leddriver2.ko。
bf000000 t leddriver2_open [leddriver2] //中断很有可能发生在 leddriver2_ioctl 函数中。
bf000014 t leddriver2_release [leddriver2]
bf000704 d GPBDAT_HIGH [leddriver2]
bf000714 d GPBDAT_LOW [leddriver2]
bf000724 d GPBUP_UPEN [leddriver2]
bf000734 d GPBCON_OUTP [leddriver2]
bf000744 d GPBCON_CLEAN [leddriver2]
bf000000 t $a [leddriver2]
bf000048 t $d [leddriver2]
bf002000 t $a [leddriver2]
bf002094 t $d [leddriver2]
bf00004c t $a [leddriver2]
bf000080 t $d [leddriver2]


3.2), 找到 leddriver2.ko
在PC上反汇编它: 
在ubuntu目录下 : 
# arm-linux-objdump -D leddriver2.ko > leddriver2.dis //PATH没有,就用下面一行
# /home/wangxc/linux/toolchain/crosstools_3.4.1_softfloat/arm-linux/gcc-3.4.1-glibc-2.3.3/bin/arm-linux-objdump -D leddriver2.ko > leddriver2.dis


在dis文件里找到 leddriver2_ioctl


00000028 :  //insmod后 上面的地址 bf000028 对应在这里
  28: e1a0c00d mov ip, sp
  2c: e92dd800 stmdb sp!, {fp, ip, lr, pc}
  30: e24cb004 sub fp, ip, #4 ; 0x4
  34: e59f000c ldr r0, [pc, #12] ; 48 <.text+0x48>
  38: e1a01002 mov r1, r2
  3c: e1a02003 mov r2, r3
  40: ebfffffe bl 40 // 对于中断, pc-4才是发生中断瞬间的地址,所以僵死的代码在这里
  44: ea00000f b 88 //所以 0xbf000044 在这里
  48: 00000000 andeq r0, r0, r0
Disassembly of section .init.text:


3.3), 分析汇编代码,找到对应的c语言代码
40: ebfffffe bl 40  //bl自己跳转到自己,就是一个死循环,
所以,c语言代码中的就是死循环的语句。
在驱动源码leddriver2_ioctl函数中寻找,可以找到 while(1);语句,说明僵死就发生在这里。

 

 

 

内核调试方法

 

目录[-]

·  大海里的鱼有很多,而我们需要的是鱼钩一只

·  一些前言

·  作者前言

·  知识从哪里来

·  为什么撰写本文档

·  为什么需要汇编级调试

·  ***第一部分:基础知识***

·  总纲:内核世界的陷阱

·  源码阅读的陷阱

·  代码调试的陷阱

·  原理理解的陷阱

·  建立调试环境

·  发行版的选择和安装

·  为什么选debian

·  debian与ubuntu

·  从0安装debian

·  debian重要命令

·  中文环境设置

·  debian的键盘设置更改

·  英文Locale下使用中文输入法

·  pdf乱码的解决

·  建立编译环境

·  安装交叉编译工具

·  交叉编译工具下载网址

·  安装arm-linux-gnueabi-XXX 工具集

·  什么是EABI

·  安装arm-elf-XXX 工具集

·  bin工具集的使用

·  arm-linux-gnueabi-gcc

·  arm-linux-gnueabi-gdb

·  qemu的使用

·  initrd.img的原理与制作

·  安装与使用

·  x86虚拟调试环境的建立

·  基于qemu和内核内置kgdb

·  基于qemu和qemu内置gdbstub

·  arm虚拟调试环境的建立

·  利用qemu

·  利用qemu安装debian linux

·  利用qemu安装能进行内核调试的系统

·  利用skyeye

·  skyeye虚拟机的内核调试

·  skyeye的安装与使用

·  快速试玩

·  快速配置能调试的环境

·  为s3c2410配置2.6.26内核

·  使用最新的skyeye

·  arm开发板调试环境的建立

·  基于串口

·  基于网口

·  gdb基础

·  基本命令

·  gdb之gui

·  gdb技巧

·  gdb

·  参考资料

·  gdb宏的使用

·  实例

·  链表遍历类

·  功能增强类

·  汇编基础--X86

·  用户手册

·  AT&T汇编格式

·  内联汇编

·  汇编与C函数的相互调用

·  调用链形成和参数传递

·  寄存器的角色与保护

·  调用链的形成

·  栈帧结构与参数传递

·  完整的调用过程

·  调用链回溯的代码实现

·  C难点的汇编解释

·  例1

·  例2

·  例3

·  例4

·  优化级别的影响

·  优化选项

·  例子

·  汇编基础--ARM

·  用户手册

·  调用链形成和参数传递

·  壮观的标准

·  别名的烦恼

·  寄存器的角色与保护

·  条件执行

·  调用链的形成

·  栈帧结构与参数传递

·  完整的调用过程

·  调用链回溯的实现

·  源码浏览工具

·  调用图生成工具

·  find + grep

·  wine + SI

·  优缺点

·  安装wine

·  安装SI

·  SI的设置

·  SI的使用

·  global

·  Source-Navigator

·  vim + cscope/ctags

·  优缺点

·  安装cscope/ctags

·  命令选项

·  使用

·  建立索引

·  利用vim浏览源码

·  快捷键的使用

·  kscope

·  lxr

·  SI等与gdb的特点

·  调用链、调用树和调用图

·  理想调用链

·  函数指针调用

·  调?”

kdb:只能在汇编代码级进行调试;

  优点是不需要两台机器进行调试。

  gdb:在调试模块时缺少一些至关重要的功能,它可用来查看内核的运行情况,包括反汇编内核函数。

  kgdb:能很方便的在源码级对内核进行调试,缺点是kgdb只能进行远程调试,它需要一根串口线及两台机器来调试内核(也可以是在同一台主机上用vmware软件运行两个操作系统来调试)

printk() 是调试内核代码时最常用的一种技术。在内核代码中的特定位置加入printk() 调试调用,可以直接把所关心的信息打打印到屏幕上,从而可以观察程序的执行路径和所关心的变量、指针等信息。 Linux 内核调试器(Linux kernel debugger,kdb)是 Linux 内核的补丁,它提供了一种在系统能运行时对内核内存和数据结构进行检查的办法。Oops、KDB在文章掌握 Linux 调试技术有详细介绍,大家可以参考。 Kprobes 提供了一个强行进入任何内核例程,并从中断处理器无干扰地收集信息的接口。使用 Kprobes 可以轻松地收集处理器寄存器和全局数据结构等调试信息,而无需对Linux内核频繁编译和启动,具体使用方法,请参考使用 Kprobes 调试内核。

/proc文件系统

在 /proc 文件系统中,对虚拟文件的读写操作是一种与内核通信的手段,要查看内核回环缓冲区中的消息,可以使用 dmesg 工具(或者通过 /proc 本身使用 cat /proc/kmsg 命令)。清单 6 给出了 dmesg 显示的最后几条消息。

清单 6. 查看来自 LKM 的内核输出

[root@plato]# dmesg | tail -5
cs: IO port probe 0xa00-0xaff: clean.
eth0: Link is down
eth0: Link is up, running at 100Mbit half-duplex
my_module_init called.  Module is now loaded.
my_module_cleanup called.  Module is now unloaded.


可以在内核输出中看到这个模块的消息。现在让我们暂时离开这个简单的例子,来看几个可以用来开发有用 LKM 的内核 API。

调试工具

  使用调试器来一步步地跟踪代码,查看变量和计算机寄存器的值。在内核中使用交互式调试器是一个很复杂的问题。内核在它自己的地址空间中运行。许多用户空间下的调试器所提供的常用功能很难用于内核之中,比如断点和单步调试等。

目录

[隐藏]

内核bug跟踪

oops消息分析

(1)oops消息产生机制

oops(也称 panic),称程序运行崩溃,程序崩溃后会产生oops消息。应用程序或内核线程的崩溃都会产生oops消息,通常发生oops时,系统不会发生死机,而在终端或日志中打印oops信息。

当使用NULL指针或不正确的指针值时,通常会引发一个 oops 消息,这是因为当引用一个非法指针时,页面映射机制无法将虚拟地址映像到物理地址,处理器就会向操作系统发出一个"页面失效"的信号。内核无法"换页"到并不存在的地址上,系统就会产生一个"oops"。

oops 显示发生错误时处理器的状态,包括 CPU 寄存器的内容、页描述符表的位置,以及其一些难理解的信息。这些消息由失效处理函数(arch/*/kernel/traps.c)中的printk 语句产生。较为重要的信息就是指令指针(EIP),即出错指令的地址。

由于很难从十六进制数值中看出含义,可使用符号解析工具klogd。klogd 守护进程能在 oops 消息到达记录文件之前对它们解码。klogd在缺省情况下运行并进行符号解码。

通常Oops文本由klogd从内核缓冲区里读取并传给syslogd,由syslogd写到syslog文件中,该文件典型为/var/log/messages(依赖于/etc/syslog.conf)。如果klogd崩溃了,用户可"dmesg > file"从内核缓冲区中读取数据并保存下来。还可用"cat /proc/kmsg > file"读取数据,此时,需要用户中止传输,因为kmsg是一个"永不结束的文件"。

当保护错误发生时,klogd守护进程自动把内核日志信息中的重要地址翻译成它们相应的符号。klogd执行静态地址翻译和动态地址翻译。静态地址翻译使用System.map文件将符号地址翻译为符号。klogd守护进程在初始化时必须能找到system.map文件。

动态地址翻译通常对内核模块中的符号进行翻译。内核模块的内存从内核动态内存池里分配,内核模块中符号的位置在内核装载后才最终确定。

Linux内核提供了调用,允许程序决定装载哪些模块和它们在内存中位置。通过这些系统调用,klogd守护进程生成一张符号表用于调试发生在可装载模块中的保护错误。内核模块的装载或者卸载都会自动向klogd发送信号,klogd可将内核模块符号的地址动态翻译为符号字符串。

(2)产生oops的样例代码

使用空指针和缓冲区溢出是产生oops的两个最常见原因。下面两个函数faulty_write和faulty_read是一个内核模块中的写和读函数,分别演示了这两种情况。当内核调用这两个函数时,会产生oops消息。

函数faulty_write删除一个NULL指针的引用,由于0不是一个有效的指针值,内核将打印oops信息,并接着,杀死调用些函数的进程。
ssize_t faulty_write (struct file *filp, const char _ _user *buf, size_t count, loff_t *pos)
{
    /* make a simple fault by dereferencing a NULL pointer */
    *(int *)0 = 0;
    return 0;
}

函数faulty_write产生oops信息列出如下(注意 EIP 行和 stack 跟踪记录中已经解码的符号):
Unable to handle kernel NULL pointer dereference at virtual address \
   00000000 

printing eip: c48370c3 *pde = 00000000 Oops: 0002 CPU: 0 EIP: 0010:[faulty:faulty_write+3/576] EFLAGS: 00010286 eax: ffffffea ebx: c2c55ae0 ecx: c48370c0 edx: c2c55b00 esi: 0804d038 edi: 0804d038 ebp: c2337f8c esp: c2337f8c ds: 0018 es: 0018 ss: 0018 Process cat (pid: 23413, stackpage=c2337000) Stack: 00000001 c01356e6 c2c55ae0 0804d038 00000001 c2c55b00 c2336000 \

          00000001 
     0804d038 bffffbd4 00000000 00000000 bffffbd4 c010b860 00000001 \ 
          0804d038 
     00000001 00000001 0804d038 bffffbd4 00000004 0000002b 0000002b \ 
          00000004 

Call Trace: [sys_write+214/256] [system_call+52/56]

Code: c7 05 00 00 00 00 00 00 00 00 31 c0 89 ec 5d c3 8d b6 00 00

上述oops消息中,字符串 3/576 表示处理器正处于函数的第3个字节上,函数整体长度为 576 个字节。 函数faulty_read拷贝一个字符串到本地变量,由于字符串比目的地数组长造成缓冲区溢出。当函数返回时,缓冲区溢出导致产生oops信息。因为返回指令引起指令指针找不到运行地址,这种错误很难发现和跟踪。
ssize_t faulty_read(struct file *filp, char _ _user *buf, size_t count, loff_t *pos)
{
    int ret;
    char stack_buf[4];
    /* Let's try a buffer overflow */
    memset(stack_buf, 0xff, 20);
    if (count > 4)
        count = 4;
    /* copy 4 bytes to the user */
    ret = copy_to_user(buf, stack_buf, count);
    if (!ret)
        return count;
    return ret;
}

函数faulty_read产生oops信息列出如下:
EIP: 0010:[<00000000>]

Unable to handle kernel paging request at virtual address ffffffff printing eip: ffffffff Oops: 0000 [#5] SMP CPU: 0 EIP: 0060:[] Not tainted EFLAGS: 00010296 (2.6.6) EIP is at 0xffffffff eax: 0000000c ebx: ffffffff ecx: 00000000 edx: bfffda7c esi: cf434f00 edi: ffffffff ebp: 00002000 esp: c27fff78 ds: 007b es: 007b ss: 0068 Process head (pid: 2331, threadinfo=c27fe000 task=c3226150) Stack: ffffffff bfffda70 00002000 cf434f20 00000001 00000286 cf434f00 fffffff7 bfffda70 c27fe000 c0150612 cf434f00 bfffda70 00002000 cf434f20 00000000 00000003 00002000 c0103f8f 00000003 bfffda70 00002000 00002000 bfffda70 Call Trace: [] sys_read+0x42/0x70 [] syscall_call+0x7/0xb

Code: Bad EIP value.

在上述oops消息中,由于缓冲区溢出,仅能看到函数调用栈的一部分,看不见函数名vfs_read和faulty_read,并且代码(Code)处仅输出"bad EIP value.",列在栈上开始处的地址"ffffffff"表示内核栈已崩溃。

(3)oops信息分析

面对产生的oops信息,首先应查找源程序发生oops的位置,通过查看指令指令寄存器EIP的值,可以找到位置,如:EIP: 0010:[faulty:faulty_write+3/576]。

再查找函数调用栈(call stack)可以得到更多的信息。从函数调用栈可辨别出局部变量、全局变量和函数参数。例如:在函数faulty_read的oops信息的函数调用栈中,栈顶为ffffffff,栈顶值应为一个小于ffffffff的值,为此值,说明再找不回调用函数地址,说明有可能因缓冲区溢出等原因造成指针错误。

在x86构架上,用户空间的栈从0xc0000000以下开始,递归值bfffda70可能是用户空间的栈地址。实际上它就是传递给read系统调用的缓冲区地址,系统调用read进入内核时,将用户空间缓冲区的数据拷贝到内核空间缓冲区。

如果oops信息显示触发oops的地址为0xa5a5a5a5,则说明很可能是因为没有初始化动态内存引起的。

另外,如果想看到函数调用栈的符号,编译内核时,请打开CONFIG_KALLSYMS选项。

klogd 提供了许多信息来帮助分析。为了使 klogd 正确地工作,必须在 /boot 中提供符号表文件 System.map。如果符号表与当前内核不匹配,klogd 就会拒绝解析符号。

有时内核错误会将系统完全挂起。例如代码进入一个死循环,系统不会再响应任何动作。这时可通过在一些关键点上插入 schedule 调用可以防止死循环。

系统崩溃重启动

由于内核运行错误,在某些极端情况下,内核会运行崩溃,内核崩溃时会导致死机。为了解决此问题,内核引入了快速装载和重启动新内核机制。内核通过kdump在崩溃时触发启动新内核,存储旧内存映像以便于调试,让系统在新内核上运行 ,从而避免了死机,增强了系统的稳定性。

(1)工具kexec介绍

kexec是一套系统调用,允许用户从当前正执行的内核装载另一个内核。用户可用shell命令"yum install kexec-tools"安装kexec工具包,安装后,就可以使用kexec命令。

工具kexec直接启动进入一个新内核,它通过系统调用使用户能够从当前内核装载并启动进入另一个内核。在当前内核中,kexec执行BootLoader的功能。在标准系统启动和kexec启动之间的主要区别是:在kexec启动期间,依赖于硬件构架的固件或BIOS不会被执行来进行硬件初始化。这将大大降低重启动的时间。

为了让内核的kexec功能起作用,内核编译配置是应确认先择了"CONFIG_KEXEC=y",在配置后生成的.config文件中应可看到此条目。

工具kexec的使用分为两步,首先,用kexec将调试的内核装载进内存,接着,用kexec启动装载的内核。

装载内核的语法列出如下:

kexec -l kernel-image --append=command-line-options --initrd=initrd-image

上述命令中,参数kernel-image为装载内核的映射文件,该命令不支持压缩的内核映像文件bzImage,应使用非压缩的内核映射文件vmlinux;参数initrd-image为启动时使用initrd映射文件;参数command-line-options为命令行选项,应来自当前内核的命令行选项,可从文件"/proc/cmdline"中提取,该文件的内容列出如下:

^-^$ cat /proc/cmdline

ro root=/dev/VolGroup00/LogVol00 rhgb quiet

例如:用户想启动的内核映射为/boot/vmlinux,initrd为/boot/initrd,则kexec加载命令列出如下:

Kexec –l /boot/vmlinux –append=/dev/VolGroup00/LogVol00 initrd=/boot/initrd

还可以加上选项-p或--load-panic,表示装载新内核在系统内核崩溃使用。

在内核装载后,用下述命令启动装载的内核,并进行新的内核中运行:

kexec -e

当kexec将当前内核迁移到新内核上运行时,kexec拷贝新内核到预保留内存块,该保留位置如图1所示, 原系统内核给kexec装载内核预保留一块内存(在图中的阴影部分),用于装载新内核,其他内存区域在未装载新内核时,由原系统内核使用。

Linux内核调试方法总结
    




Linux内核调试方法

图1 kexec装载的内核所在预保留位置示意图

在x86构架的机器上,系统启动时需要使用第一个640KB物理内存,用于内核装载,kexec在重启动进入转储捕捉的内核之前备份此区域。相似地,PPC64构架的机器在启动里需要使用第一个32KB物理内核,并需要支持64K页,kexec备份第一个64KB内存。

(2)kdump介绍

kdump是基于kexec的崩溃转储机制(kexec-based Crash Dumping),无论内核内核需要转储时,如:系统崩溃时,kdump使用kexec快速启动进入转储捕捉的内核。在这里,原运行的内核称为系统内核或原内核,新装载运行的内核称为转储捕捉的内核或装载内核或新内核。

在重启动过程中,原内核的内存映像被保存下来,并且转储捕捉的内核(新装载的内核)可以访问转储的映像。用户可以使用命令cp和scp将内存映射拷贝到一个本地硬盘上的转储文件或通过网络拷贝到远程计算机上。

当前仅x86, x86_64, ppc64和ia64构架支持kdump和kexec。

当系统内核启动时,它保留小部分内存给转储(dump)捕捉的内核,确保了来自系统内核正进行的直接内存访问(Direct Memory Access:DMA)不会破坏转储捕捉的内核。命令kexec –p装载新内核到这个保留的内存。

在崩溃前,所有系统内核的核心映像编码为ELF格式,并存储在内核的保留区域。ELF头的开始物理地址通过参数elfcorehdr=boot传递到转储捕捉的内核。

通过使用转储捕捉的内核,用户可以下面两种方式访问内存映像或旧内存:

(1)通过/dev/oldmem设备接口,捕捉工具程序能读取设备文件并以原始流的格式写出内存,它是一个内存原始流的转储。分析和捕捉工具必须足够智能以判断查找正确信息的位置。

(2)通过/proc/vmcore,能以ELF格式文件输出转储信息,用户可以用GDB(GNU Debugger)和崩溃调试工具等分析工具调试转储文件。

(3)建立快速重启动机制和安装工具

1)安装工具kexec-tools

可以下载源代码编译安装工具kexec-tools。由于工具kexec-tools还依赖于一些其他的库,因此,最好的方法是使用命令"yum install kexec-tools"从网上下载安装并自动解决依赖关系。

2)编译系统和转储捕捉的内核

可编译独立的转储捕捉内核用于捕捉内核的转储,还可以使用原系统内核作为转储捕捉内核,在这种情况下,不需要再编译独立的转储捕捉内核,但仅支持重定位内核的构架才可以用作转储捕捉的内核,如:构架i386和ia64支持重定位内核。

对于系统和转储捕捉内核来说,为了打开kdump支持,内核需要设置一些特殊的配置选项,下面分别对系统内核和转储捕捉内核的配置选项进行说明:

系统内核的配置选项说明如下:

  • 在菜单条目"Processor type and features."中打开选项"kexec system call",使内核编译安装kexe系统调用。配置文件.config生成语句"CONFIG_KEXEC=y"。
  • 在菜单条目"Filesystem"->"Pseudo filesystems."中打开选项"sysfs file system support",使内核编译安装文件系统sysfs.配置文件.config生成语句"CONFIG_SYSFS=y"。
  • 在菜单条目"Kernel hacking."中打开选项"Compile the kernel with debug info ",使内核编译安装后支持调试信息输出,产生调试符号用于分析转储文件。配置文件.config生成语句"CONFIG_DEBUG_INFO=Y"。

转储捕捉内核配置选项(不依赖于处理器构架)说明如下:

  • 在菜单条目"Processor type and features"中打开选项"kernel crash dumps",配置文件.config生成语句" CONFIG_CRASH_DUMP=y"。
  • 在菜单条目"Filesystems"->"Pseudo filesystems"中打开选项"/proc/vmcore support",配置文件.config生成语句"CONFIG_PROC_VMCORE=y"。

转储捕捉内核配置选项(依赖于处理器构架i386和x86_64)说明如下:

  • 在处理器构架i386上,在菜单条目"Processor type and features"中打开高端内存支持,配置文件.config生成语句"CONFIG_HIGHMEM64G=y"或"CONFIG_HIGHMEM4G"。
  • 在处理器构架i386和x86_64上,在菜单条目"rocessor type and features"中关闭对称多处理器支持,配置文件.config生成语句"CONFIG_SMP=n"。如果配置文件中的设置为"CONFIG_SMP=y",则可在装载转储捕捉内核的内核命令行上指定"maxcpus=1"。
  • 如果想构建和使用可重定位内核,在菜单条目"rocessor type and featuresIf"中打开选项"Build a relocatable kernel",配置文件.config生成语句"CONFIG_RELOCATABLE=y"。
  • 在菜单"Processor type and features"下的条目"Physical address where the kernel is loaded"设置合适的值用于内核装载的物理地址。它仅在打开了"kernel crash dumps"时出现。合适的值依赖于内核是否可重定位。

如果设置了值"CONFIG_PHYSICAL_START=0x100000",则表示使用可重定位内核。它将编译内核在物理地址1MB处,内核是可重定位的,因此,内核可从任何物理地址运行。Kexec BootLoader将装载内核到用于转储捕捉内核的内核保留区域。

否则,将使用启动参数"crashkernel=Y@X"指定第二个内核保留内核区域的开始地址,其中,Y表示内存区域的大小,X表示保留给转储捕捉内核的内存区域的开始地址,通过X为16MB (0x1000000),因此用户可设置"CONFIG_PHYSICAL_START=0x1000000"。

在配置完内核后,编译和安装内核及内核模块。

3)扩展的crashkernel语法

在系统内核的启动命令行选项中,通常语法"crashkernel=size[@offset]"对于大多数据配置已够用了,但有时候保留的内存依赖于系统RAM。此时可通过扩展的crashkernel命令行对内存进行 限制避免从机器上移去一部分内核后造成系统不可启动。扩展的crashkernel语法列出如下:

crashkernel=<range1>:<size1>[,<range2>:<size2>,...][@offset]

其中,range=start-[end]。

例如:crashkernel=512M-2G:64M,2G-:128M,含义为:如果内存小于512M,不设置保留内存,如果内存为512M到2G之间,设置保留内存区域为64M,如果内存大于128M,设置保留内存区域为128M。

4)启动进入系统内核

必要时更新BootLoader。然后用参数"crashkernel=Y@X"启动系统内核,如:crashkernel=64M@16M,表示告诉系统内核保留从物理地址0x01000000 (16MB)开始的64MB大小给转储捕捉内核使用。通常x86和x86_64平台设置"crashkernel=64M@16M",ppc64平台设置"crashkernel=128M@32M"。

5)装载转储捕捉内核

在启动进入系统内核后,需要装载转储捕捉内核。根据处理器构架和映射文件的类型(可否重定位),可以选择装载不压缩的vmlinux或压缩的bzImage/vmlinuz内核映像。选择方法说明如下:

对于i386和x86_64平台:

  • 如果内核不是可重定位的,使用vmlinux。
  • 如果内核是可重定位的,使用bzImage/vmlinuz。

对于ppc64平台:

  • 使用vmlinux。

对于ia64平台:

  • 使用vmlinux或vmlinuz.gz。
如果用户使用不压缩的vmlinux映像,那么使用下面的命令装载转储捕捉内核:
kexec -p <dump-capture-kernel-vmlinux-image> \
   --initrd=<initrd-for-dump-capture-kernel> --args-linux \
   --append="root=<root-dev> <arch-specific-options>"

如果用户使用压缩的bzImage/vmlinuz映像,那么使用下面的命令装载转储捕捉内核:
kexec -p <dump-capture-kernel-bzImage>\
  --initrd=<initrd-for-dump-capture-kernel> \
   --append="root=<root-dev> <arch-specific-options>"

注意:参数--args-linux在ia64平台中不用指定。

下面是在装载转储捕捉内核时使用的构架特定命令行选项:

  • 对于i386, x86_64和ia64平台,选项为"1 irqpoll maxcpus=1 reset_devices"。
  • 对于ppc64平台,选项为"1 maxcpus=1 noirqdistrib reset_devices"。

在装载转储捕捉内核时需要注意的事项说明如下:

  • 缺省设置下,ELF头以ELF64格式存储,以支持多于4GB内核的系统,在i386上,kexec自动检查物理RAM尺寸是否超过4GB限制,如果没有超过,使用ELF32。因此,在非PAE系统上ELF头总是使用ELF32格式。
  • 选项--elf32-core-headers可用于强制产生ELF32头,这是必要的,因为在32位系统上,GDB当前不能打开带有ELF64头的vmcore文件。
  • 在转储捕捉内核中,启动参数irqpoll减少了由于共享中断引起的驱动程序初始化失败。
  • 用户必须以命令mount输出的根设备名的格式指定<root-dev>。
  • 启动参数"1"将转储捕捉内核启动进入不支持网络的单用户模式。如果用户想使用网络,需要设置为3。
  • 通常不必让转储捕捉内核以SMP方式运行。因此,通常编译一个单CPU转储捕捉内核或装载转储捕捉内核时指定选项"maxcpus=1"。

6)内核崩溃时触发内核启动

在装载转储捕捉内核后,如果系统发生崩溃(Kernel Panic),系统将重启动进入转储捕捉内核。重启动的触发点在函数die(), die_nmi()和sysrq处理例程(按ALT-SysRq-c组合键)。

下面条件将执行一个崩溃触发点:

  • 如果检测到硬件锁住,并且配置了"NMI watchdog",系统将调用函数die_nmi()启动进入转储捕捉内核。
  • 如果调用了函数die(),并且该线程的pid为0或1,或者在中断上下文中调用die(),或者设置了panic_on_oops并调用了die(),系统将启动进入转储捕捉内核。
  • 在powerpc系统,当一个软复位产生时,所有的CPU调用die(),并且系统将启动进入转储捕捉内核。
  • 为了测试目的,用户可以使用"ALT-SysRq-c","echo c > /proc/sysrq-trigger"触发一个崩溃,或者写一个内核模块强制内核崩溃。

7)写出转储文件

在转储捕捉内核启动后,可用下面的命令写出转储文件:

cp /proc/vmcore <dump-file>

用户还可以将转储内存作为设备/dev/oldmem以线性原始流视图进行访问,使用下面的命令创建该设备:

mknod /dev/oldmem c 1 12

使用命令dd拷贝转储内存的特定部分,拷贝整个内存的命令列出如下:

dd if=/dev/oldmem of=oldmem.001

8)转储文件分析

在分析转储映像之前,用户应重启动进入一个稳定的内核。用户可以用GDB对拷贝出的转储进行有限分析。编译vmlinux时应加上-g选项,才能生成调试用的符号,然后,用下面的命令调试vmlinux:

gdb vmlinux <dump-file>

SysRq魔术组合键打印内核信息

SysRq"魔术组合键"是一组按键,由键盘上的"Alt+SysRq+[CommandKey]"三个键组成,其中CommandKey为可选的按键。SysRq魔术组合键根据组合键的不同,可提供控制内核或打印内核信息的功能。SysRq魔术组合键的功能说明如表1所示。

表1 SysRq组合键的功能说明
键名 功能说明
b 在没有同步或卸载硬盘的情况下立即启动。
c 为了获取崩溃转储执行kexe重启动。
d 显示被持的所有锁。
e 发送信号SIGTERM给所有进程,除了init外。
f 将调用oom_kill杀死内存热进程。
g 在平台ppc和sh上被kgdb使用。
h 显示帮助信息。
i 发送信号SIGKILL给所有的进程,除了init外。
k 安全访问密钥(Secure Access Key,SAK)杀死在当前虚拟终端上的所有程序。
m 转储当前的内存信息到控制台。
n 用于设置实时任务为可调整nice的。
o 将关闭系统(如果配置为支持)。
p 打印当前寄存器和标识到控制台。
q 将转储所有正运行定时器的列表。
r 关闭键盘Raw模式并设置为XLATE模式。
s 尝试同步所有挂接的文件系统。
t 将转储当前的任务列表和它们的信息到控制台。
u 尝试以仅读的方式重挂接所有已挂接的文件系统。
v 转储Voyager SMP处理器信息到控制台。
w 转储的所有非可中断(已阻塞)状态的任务。
x 在平台ppc/powerpc上被xmon(X监视器)接口使用。
0~9 设备控制台日志级别,控制将打印到控制台的内核信息。例如:0仅打印紧急信息,如:PANIC和OOPS信息。

默认SysRq组合键是关闭的。可用下面的命令打开此功能:

# echo 1 > /proc/sys/kernel/sysrq

关闭此功能的命令列出如下:

# echo 0 > /proc/sys/kernel/sysrq

如果想让此功能总是起作用,可在/etc/sysctl.conf文件中设置kernel.sysrq值为1。 系统重新启动以后,此功能将会自动打开。

打开SysRq组合键功能后,有终端访问权限的用户就可以自用它打印内核信息了。

注意:SysRq组合键在X windows上是无法使用的。必须先要切换到文本虚拟终端下。如果在图形界面,可以按Ctrl+Alt+F1切换到虚拟终端。在串口终端上,需要先在终端上发送Break信号,然后在5秒内输入sysrq组合键。如果用户有root权限,可把commandkey字符写入到/proc/sysrq-trigger文件,触发一个内核信息打印,打印的信息存放在/var/log/messages中。下面是一个命令样例:
^-^$ echo 't' > sysrq-trigger
^-^vim /var/log/messages
Oct 29 17:51:43 njllinux kernel: SysRq : Show State
Oct 29 17:51:43 njllinux kernel:  task                        PC stack   pid father
Oct 29 17:51:43 njllinux kernel: init          S ffffffff812b76a0     0     1      0
Oct 29 17:51:43 njllinux kernel: ffff81013fa97998 0000000000000082 0000000000000000 ffff81013fa9795c
Oct 29 17:51:43 njllinux kernel: 000000003fa97978 ffffffff81583700 ffffffff81583700 ffff81013fa98000
Oct 29 17:51:43 njllinux kernel: ffffffff813cc5b0 ffff81013fa98350 000000003c352a50 ffff81013fa98350
Oct 29 17:51:43 njllinux kernel: Call Trace:
Oct 29 17:51:43 njllinux kernel: 000300000004 ffff8101333cb090
Oct 29 17:51:43 njllinux kernel: Call Trace:
Oct 29 17:51:43 njllinux kernel: [<ffffffff81040c2e>] sys_pause+0x19/0x22
Oct 29 17:51:43 njllinux kernel: [<ffffffff8100c291>] tracesys+0xd0/0xd5
Oct 29 17:51:43 njllinux kernel:
Oct 29 17:51:43 njllinux kernel: lighttpd      S ffffffff812b76a0     0  3365      1
Oct 29 17:51:43 njllinux kernel: ffff810132d49b18 0000000000000082 0000000000000000 ffff810132d49adc
Oct 29 17:51:43 njllinux kernel: ffff81013fb2d148 ffffffff81583700 ffffffff81583700 ffff8101354896a0
Oct 29 17:51:43 njllinux kernel: ffffffff813cc5b0 ffff8101354899f0 0000000032d49ac8 ffff8101354899f0
Oct 29 17:51:43 njllinux kernel: Call Trace:
Oct 29 17:51:43 njllinux kernel: [<ffffffff81040722>] ? __mod_timer+0xbb/0xcd
Oct 29 17:51:43 njllinux kernel: [<ffffffff8129b2ee>] schedule_timeout+0x8d/0xb4
Oct 29 17:51:43 njllinux kernel: [<ffffffff81040100>] ? process_timeout+0x0/0xb
Oct 29 17:51:43 njllinux kernel: [<ffffffff8129b2e9>] ? schedule_timeout+0x88/0xb4
Oct 29 17:51:43 njllinux kernel: [<ffffffff810b9498>] do_sys_poll+0x2a8/0x370
……

命令strace

命令strace 显示程序调用的所有系统调用。使用 strace 工具,用户可以清楚地看到这些调用过程及其使用的参数,了解它们与操作系统之间的底层交互。当系统调用失败时,错误的符号值(如 ENOMEM)和对应的字符串(如Out of memory)都能被显示出来。

starce 的另一个用处是解决和动态库相关的问题。当对一个可执行文件运行ldd时,它会告诉你程序使用的动态库和找到动态库的位置

strace命令行选项说明如表1。常用的选项为-t, -T, -e, -o等。

表1 命令strace的命令行选项说明
选项 说明
-c 统计每个系统调用执行的时间、次数和出错的次数等。
-d 输出一些strace自身的调试信息到标准输出。
-f 跟踪当前进程由系统调用fork产生的子进程。
-ff 如果使用选项-o filename,则将跟踪结果输出到相应的filename.pid中,pid是各进程的进程号。
-F 尝试跟踪vfork调用.在-f时,vfork不被跟踪。
-h 输出简要的帮助信息。
-i 在系统调用的时候打印指令指针。
-q 禁止输出关于粘附和脱离的信息,发生在输出重定向到文件且直接而不是粘附运行命令时。
-r 依赖于每个系统调用的入口打印相对时间戳。
-t 在输出中的每一行前加上时间信息。
-tt 在输出中的每一行前加上时间信息,包括毫秒。
-ttt 毫秒级输出,以秒表示时间。
-T 显示系统调用所花费的时间。
-v 输出所有的系统调用的信息。一些关于环境变量,状态,输入输出等调用由于使用频繁,默认不输出。
-V 输出strace的版本信息。
-x 以十六进制形式输出非ASCII标准字符串。
-xx 所有字符串以十六进制形式输出。
-a column 以特定的列数对齐返回值,缺省值为40。
-e expr 指定一个表达式,用来控制如何跟踪.格式如下:
[qualifier=][!]value1[,value2]...
qualifier只能是 trace,abbrev,verbose,raw,signal,read,write其中之一。value是用来限定的符号或数字。默认的qualifier是 trace。感叹号是否定符号。
-eopen 等价于 -e trace=open,表示只跟踪open调用。而-etrace!=open表示跟踪除了open以外的其他调用。
-e trace=set 只跟踪指定的系统调用。例如:-e trace=open,close,rean,write表示只跟踪这四个系统调用。默认的为set=all。
-e trace=file 只跟踪文件名作为参数的系统调用,一般为文件操作。
-e trace=process 只跟踪有关进程控制的系统调用。
-e trace=network 只跟踪与网络有关的所有系统调用。
-e strace=signal 跟踪所有与系统信号有关的系统调用。
-e trace=ipc 跟踪所有与进程间通信有关的系统调用。
-o filename 将strace的输出写入文件filename。
-p pid 跟踪指定的进程pid。
-s strsize 指定最大字符串打印长度,默认值为32。
-u username 以username的UID和GID执行命令。
例如:命令strace pwd的输出部分列出如下:
execve("/bin/pwd", ["pwd"], [/* 39 vars */]) = 0
uname({sys="Linux", node="sammy", ...}) = 0
brk(0)                                  = 0x804c000
old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x4001...
	fstat64(3, {st_mode=S_IFREG|0644, st_size=115031, ...}) = 0
old_mmap(NULL, 115031, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40017000
close(3)                                = 0
open("/lib/tls/libc.so.6", O_RDONLY)    = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\360U\1"..., 1024) = 1024
fstat64(3, {st_mode=S_IFREG|0755, st_size=1547996, ...}) = 0

用函数printk打印内核信息

Linux内核用函数printk打印调试信息,该函数的用法与C库打印函数printf格式类似,但在内核使用。用户可在内核代码中的某位置加入函数printk,直接把所关心的信息打打印到屏幕上或日志文件中。

函数printk根据日志级别(loglevel)对调试信息进行分类。日志级别用宏定义,展开为一个字符串,在编译时由预处理器将它和消息文本拼接成一个字符串,因此函数printk中的日志级别和格式字符串间不能有逗号。

下面两个 printk 的例子,一个是调试信息,一个是临界信息:
printk(KERN_DEBUG "Here I am: %s:%i\n", _ _FILE_ _, _ _LINE_ _); 
printk(KERN_CRIT "I'm trashed; giving up on %p\n", ptr);

样例:在用户空间或内核中开启及关闭打印调试消息 用户还可以在内核或用户空间应用程序定义统一的函数打印调试信息,可在Makefile文件中打开或关闭调试函数。定义方法列出如下:
/*debug_on_off.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

在文件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" 
else 
 DEBFLAGS = -O2 
endif 
 
CFLAGS += $(DEBFLAGS)

更改makefile中的DEBUG值,需要调试信息时,DEBUG = y,不需要时,DEBUG赋其它值。再用make编译即可。

内核探测kprobe

kprobe(内核探测,kernel probe)是一个动态地收集调试和性能信息的工具,如:收集寄存器和全局数据结构等调试信息,无需对Linux内核频繁编译和启动。用户可以在任何内核代码地址进行陷阱,指定调试断点触发时的处理例程。工作机制是:用户指定一个探测点,并把用户定义的处理函数关联到该探测点,当内核执行到该探测点时,相应的关联函数被执行,然后继续执行正常的代码路径。

kprobe允许用户编写内核模块添加调试信息到内核。当在远程机器上调试有bug的程序而日志/var/log/messages不能看出错误时,kprobe显得非常有用。用户可以编译一个内核模块,并将内核模块插入到调试的内核中,就可以输出所需要的调试信息了。

内核探测分为kprobe, jprobe和kretprobe(也称return probe,返回探测)三种。kprobe可插入内核中任何指令处;jprobe插入内核函数入口,方便于访问函数的参数;return probe用于探测指定函数的返回值。

内核模块的初始化函数init安装(或注册)了多个探测函数,内核模块的退出函数exit将注销它们。注册函数(如:register_kprobe())指定了探测器插入的地方、探测点触发的处理例程。

(1)配置支持kprobe的内核

配置内核时确信在.config文件中设置了CONFIG_KPROBES、CONFIG_MODULES、CONFIG_MODULE_UNLOAD、CONFIG_KALLSYMS_ALL和CONFIG_DEBUG_INFO。

配置了CONFIG_KALLSYMS_ALL,kprobe可用函数kallsyms_lookup_name从地址解析代码。配置了CONFIG_DEBUG_INFO后,可以用命令"objdump -d -l vmlinux"查看源到对象的代码映射。

调试文件系统debugfs含有kprobe的调试接口,可以查看注册的kprobe列表,还可以关闭/打开kprobe。

查看系统注册probe的方法列出如下:

#cat /debug/kprobes/list
c015d71a  k  vfs_read+0x0
c011a316  j  do_fork+0x0
c03dedc5  r  tcp_v4_rcv+0x0

第一列表示探测点插入的内核地址,第二列表示内核探测的类型,k表示kprobe,r表示kretprobe,j表示jprobe,第三列指定探测点的"符号+偏移"。如果被探测的函数属于一个模块,模块名也被指定。

打开和关闭kprobe的方法列出如下:

#echo ‘1’ /debug/kprobes/enabled
#echo ‘0’ /debug/kprobes/enabled

(2)kprobe样例

Linux内核源代码在目录samples/kpobges下提供了各种kprobe类型的探测处理例程编写样例,分别对应文件kprobe_example.c、jprobe_example.c和kretprobe_example.c,用户稍加修改就可以变成自己的内核探测模块。下面仅说明kprobe类型的探测例程。

样例kprobe_example是kprobe类型的探测例程内核模块,显示了在函数do_fork被调用时如何使用kprobe转储栈和选择的寄存器。当内核函数do_fork被调用创建一个新进程时,在控制台和/var/log/messages中将显示函数printk打印的跟踪数据。样例kprobe_example列出如下(在samples/kprobe_example.c中):

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>
 
/* 对于每个探测,用户需要分配一个kprobe对象*/
static struct kprobe kp = {
	.symbol_name	= "do_fork",
};
 
/* 在被探测指令执行前,将调用预处理例程 pre_handler,用户需要定义该例程的操作*/
static int handler_pre(struct kprobe *p, struct pt_regs *regs)
{
#ifdef CONFIG_X86
	printk(KERN_INFO "pre_handler: p->addr = 0x%p, ip = %lx,"
			" flags = 0x%lx\n",
		p->addr, regs->ip, regs->flags);  /*打印地址、指令和标识*/
#endif
#ifdef CONFIG_PPC
	printk(KERN_INFO "pre_handler: p->addr = 0x%p, nip = 0x%lx,"
			" msr = 0x%lx\n",
		p->addr, regs->nip, regs->msr);
#endif
 
	/* 在这里可以调用内核接口函数dump_stack打印出栈的内容*/
	return 0;
}
 
/* 在被探测指令执行后,kprobe调用后处理例程post_handler */
static void handler_post(struct kprobe *p, struct pt_regs *regs,
				unsigned long flags)
{
#ifdef CONFIG_X86
	printk(KERN_INFO "post_handler: p->addr = 0x%p, flags = 0x%lx\n",
		p->addr, regs->flags);
#endif
#ifdef CONFIG_PPC
	printk(KERN_INFO "post_handler: p->addr = 0x%p, msr = 0x%lx\n",
		p->addr, regs->msr);
#endif
}
 
/*在pre-handler或post-handler中的任何指令或者kprobe单步执行的被探测指令产生了例外时,会调用fault_handler*/
static int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr)
{
	printk(KERN_INFO "fault_handler: p->addr = 0x%p, trap #%dn",
		p->addr, trapnr);
	/* 不处理错误时应该返回*/
	return 0;
}
 
/*初始化内核模块*/
static int __init kprobe_init(void)
{
	int ret;
	kp.pre_handler = handler_pre;
	kp.post_handler = handler_post;
	kp.fault_handler = handler_fault;
 
	ret = register_kprobe(&kp);  /*注册kprobe*/
	if (ret < 0) {
		printk(KERN_INFO "register_kprobe failed, returned %d\n", ret);
		return ret;
	}
	printk(KERN_INFO "Planted kprobe at %p\n", kp.addr);
	return 0;
}
 
static void __exit kprobe_exit(void)
{
	unregister_kprobe(&kp);
	printk(KERN_INFO "kprobe at %p unregistered\n", kp.addr);
}
 
module_init(kprobe_init)
module_exit(kprobe_exit)
MODULE_LICENSE("GPL");

Systemtap调试

(1)Systemtap原理

Systemtap是一个基于kprobe调试内核的开源软件。调试者只需要写一些脚本,通过Systemtap提供的命令行接口对正在运行的内核进行诊断调试,不需要修改或插入调试代码、重新编译内核、安装内核和重启动等工作,使内核调试变得简单容易。Systemtap调试过程与在gdb调试器中用断点命令行调试类似。

Systemtap用类似于awk语言的脚本语言编写调试脚本,该脚本命名事件并给这些事件指定处理例程。只要指定的事件发生,Linux内核将运行对应的处理例程。

有几种类型的事件,如:进入或退出一个函数,一个定时器超时或整个systemtap会话开始或停止。处理例程是一系列脚本语言语句指定事件发生时所做的工作,包括从事件上下文提取数据,存储它们进入内部变量或打印结果。

Systemtap的运行过程如图2所示,用户调试时用Systemtap编写调试脚本,Systemtap的翻译模块(translator)将脚本经语法分析(parse)、功能处理(elaborate)和翻译后生成C语言调试程序,然后,运行C编译器编译(build)创建调试内核模块。再接着将该内核模块装载入内核,通过kprobe机制,内核的hook激活所有的探测事件。当任何处理器上有这些事件发生时,对应的处理例程被触发工作,kprobe机制在内核获取的调试数据通过文件系统relayfs传回Systemtap,输出调试数据probe.out。在调试结束时,会话停止,内核断开hook连接,并卸载内核模块。整个操作过程由单个命令行程序strap驱动控制。

Linux内核调试方法总结
    




Linux内核调试方法

图2 Systemtap运行过程

(2)stap程序

stap程序是Systemtap工具的前端,它接受用systemtap脚本语言编写的探测指令,翻译这些指令到C语言代码,编译C代码产生并装载内核模块到正运行的Linux内核,执行请求的跟踪或探测函数。用户可在一个命名文件中提供脚本或从命令行中提供调试语句。

命令stap的用法列出如下:

stap [ OPTIONS ] FILENAME [ ARGUMENTS ]

stap [ OPTIONS ] - [ ARGUMENTS ]

stap [ OPTIONS ] -e SCRIPT [ ARGUMENTS ]

stap [ OPTIONS ] -l PROBE [ ARGUMENTS ]

选项[ OPTIONS ]说明如下:

-h 显示帮助信息。

-V 显示版本信息。

-k 在所有操作完成后,保留临时目录。对于检查产生的C代码或重使用编译的内核对象来说,这是有用的。

-u 非优化编译模式。.

-w 关闭警告信息。

-b 让内核到用户数据传输使用bulk模式。

-t 收集时间信息:探测执行的次数、每个探测花费的平均时间量。

-sNUM 内核到用户数据传输使用NUM MB 的缓冲区。当多个处理器工作在bulk模式时,这是单个处理器的缓冲区大小。

-p NUM Systemtap在通过NUM个步骤后停止。步骤数为1-5: parse, elaborate, translate, compile, run。

-I DIR 添加tapset库(用于翻译C代码的函数集)搜索目录。

-D NAME=VALUE 添加C语言宏定义给内核模块Makefile,用于重写有限的参数。

-R DIR 在给定的目录查找Systemtap运行源代码。

-r RELEASE 为给定的内核发布版本RELEASE而不是当前运行内核编译内核模块。

-m MODULE 给编译产生的内核模块命名MODULE,替代缺省下的随机命名。产生的内核模块被拷贝到当前目录。

-o FILE 发送标准输出到命名文件FILE。在bulk模式,每个CPU的文件名将用"FILE_CPU序号"表示。

-c CMD 开始探测,运行CMD,当CMD完成时退出。

-x PID 设置target()????到PID。这允许脚本作为指定进程的过滤器。

-l PROBE 代替运行一个探测脚本,它仅列出所有匹配给定模式PROBE可用的探测点,模式PROBE可用通配符。

--kmap[=FILE] 指定符号文件,缺省文件为/boot/System.map-VER-SION。探测的函数的地址和名字需要通过内核或内核模块的符号表解析。如果内核编译时没有调试信息或者探测是在没有调试信息的汇编语言言论的,这将是有用的。

--ignore-vmlinux 忽略vmlinux文件。

(3)Systemtap脚本语法

Systemtap脚本语法类似于C语言,它使用了三种数据类型:整数(integers)、字符串(strings)和关联数组(associative Arrays)。它有与C语言一样的控制结构。Systemtap脚本语法详细内容请参考《Systemtap tutorial》。

Systemtap脚本由探测点(probe)和探测输出函数组成。每个Systemtap脚本至少定义一个探测点。函数是探测点的处理例程。

#!/usr/bin/env stap    #Systemtap脚本的标志
#
# 显示在最后5秒内调用最后10个系统调用
display the top 10 syscalls called in last 5 seconds
#
global syscalls          #定义全局变量
function print_top () {  #定义函数
        cnt=0            #局部变量
        log ("SYSCALL\t\t\t\tCOUNT")      #打印表头标题“SYSCALL COUNT”
        foreach ([name] in syscalls-) {   #查询每个系统调用的计数值
                printf("%-20s\t\t%5d\n",name, syscalls[name])  #按格式打印
                if (cnt++ == 10)
                        break
        }
        printf("--------------------------------------\n")
        delete syscalls                 #删除全局变量
}
probe syscall.* {       #在系统调用探测点
        syscalls[probefunc()]++      #系统调用计数
}
probe timer.ms(5000) {
        print_top ()       #调用函数
}

kdb内核调试器

Kdb(Kernel Debug)是SGI公司开发的遵循GPL的内建Linux内核调试工具。标准的Linux内核不包括kdb,需要从ftp://oss.sgi.com/www/projects/kdb/download/ix86 下载对应标准版本内核的kdb补丁,对标准内核打补丁,然后,编译打过补丁的内核代码。目前kdb支持包括x86(IA32)、IA64和MIPS在内的体系结构。

Kdb调试器是Linux内核的一部分,提供了检查内存和数据结构的方法。通过附加命令,它可以格式化显示给定地址或ID的基本系统数据结构。kdb当前的命令集可以完全控制内核的操作,包括单步运行一个处理器、在指定的指令执行处理暂停、在访问或修改指定虚拟内存的位置暂停、在输入-输出地址空间对一个寄存器访问处暂停、通过进程ID跟踪任务、指令反汇编等。

安装kdb

标准内核不包含kdb,因此,用户需要先下载kdb补丁,如:kdb-v4.4-2.6.24-x86-2.bz2,接着,应打补丁、配置、编译和安装内核。

(1)打补丁

下载和解压缩补丁,将补丁打进标准内核中。方法如下:

$ upzip kdb-v4.4-2.6.24-x86-2.bz2
$cp kdb-v4.4-2.6.24-x86-2  linux-2.6.24/
$ cd linux-2.6.24
$ patch -p1 < kdb-v4.4-2.6.24-x86-2.bz
$ make xconfig

(2)配置新内核

运行make xconfig,在配置界面上选择CONFIG_KDB选项,为了更好地调试,建议用户从配置界面上选择CONFIG_FRAME_POINTER选项,尽管该选项使用了格外的寄存器并产生稍慢一些的内核。

(3)编译与安装新内核

按下面步骤重新编译和安装新内核:

#make   
#make install  
#make modules_install

使用kdb调试命令

运行支持kdb的内核后,在控制台上按下 Pause(或 Break)键将启动调试。当内核发生 oop或到达某个断点时,也会启动 kdb。kdb提示符如下所示:

Entering kdb (current=0xc03b0000,pid 0)on processor 0 due to Keyboard Entry

[0]kdb>

在kdb提示符下,用户可以输入kdb命令,详细的kdb命令使用说明请参考man kdb文档,一些常见的命令说明如表1.

表1 常见kdb命令说明

命令 命令说明
' 命令可以用于显示所有kdb命令。
bp 设置或显示一个断点。
bph 设置一个硬件断点。
bc 清除一个断点。
bl 列出所有当前断点。
bt 显示当前进程的堆栈跟踪情况。
go 退出调试器并重启内核运行。
Id 反汇编指令。
md 显示指定地址内容。
mds 以符号形式显示内存。
mm 修改内存。
reboot 立即重启机器。
rd 显示寄存器内容。
ss 单步执行(一次一条指令)。
ssb 单步执行CPU直到到达一分支。

下面以调试scull驱动程序为例简单说明kdb的使用方法:

假定scull驱动程序内核模块已装载入内核,先在驱动程序的函数scull_read 中设置一个断点,方法如下:

[1]kdb> bp scull_read

Instruction(i) BP #0 at 0xc8833514 (scull_read) is enabled on cpu 1

[1]kdb> go

  命令bp在函数scull_read 开始处设置了一个断点,接着,命令go退出调试器,重启内核运行。内核下一次进入函数scull_read 时暂停运行。产生如下的状态:

Entering kdb (0xc3108000) on processor 0 due to Breakpoint @ 0xc8833515 
Instruction(i) breakpoint #0 at 0xc8833514 
scull_read+0x1:   movl   %esp,%ebp 
[0]kdb>

  kdb当前scull_read断点位置。可用命令bt查看堆栈跟踪记录,检查函数调用层次树,方法如下:

[0]kdb> bt 
   EBP       EIP         Function(args) 
0xc3109c5c 0xc8833515  scull_read+0x1 
0xc3109fbc 0xfc458b10  scull_read+0x33c255fc( 0x3, 0x803ad78, 0x1000, 
0x1000, 0x804ad78) 
0xbffffc88 0xc010bec0  system_call 
[0]kdb>

再可用命令mds显示指定内存的数据,如:查询 scull_devices 指针的值方法如下:

[0]kdb> mds scull_devices 1

c8836104: c4c125c0 ....

上面命令查看指针scull_devices所指位置的一个双字(4个字节)数据,表示设备结构数组的起始地址为c4c125c0。再用mds查看设备结构的数据,方法如下:

[0]kdb> mds c4c125c0

c4c125c0: c3785000 ....

c4c125c4: 00000000 ....

c4c125c8: 00000fa0 ....

c4c125cc: 000003e8 ....

c4c125d0: 0000009a ....

c4c125d4: 00000000 ....

c4c125d8: 00000000 ....

c4c125dc: 00000001 ....

  上面8行分别对应结构Scull_Dev的8个成员。再与数据结构Scull_Dev的定义相对照,可知这8个数据的含义。

还可以使用命令mm修改数据。例如:将结构Scull_Dev的某一成员值设置为0x50,方法如下:

[0]kdb> mm c4c125d0 0x50

0xc4c125d0 = 0x50

kgdb

kgdb调试原理

调试器GNU gdb主要用于调试用户级程序,通过串口线或网络将两台计算机以主机/目标机(host machine/target machine)方式连接时,gdb还可用于调试linux内核。这种方式需要给内核打进包含kgdb驱动程序在内的补丁。

kgdb是Linux内核的源代码级调试器,与gdb配合使用可以调试Linux内核。在Linux内核的kgdb配合下,内核开发者可以用类似于调试应用程序的方式通过gdb调试内核,可以方便以使用gdb的命令在内核代码放置断点、单步调试内核和观察内核变量值等。

kgdb进行源码级内核调试的原理图如图1所示。在两台计算中机,一台用作开发计算机,称为主机或开发机;一台用作测试计算机,称为目标机或测试机。两台计算机可通过串行线或以太网进行通信。内核在测试机上调试,gdb在开发机上运行,gdb通过串行线用null modem与调试的内核通信。两台计算机也可以使用一台计算机上的两个虚拟机进行替代。

Linux内核调试方法总结
    




Linux内核调试方法

图1 kgdb进行源码级内核调试原理图

目前,kgdb支持i386, x86_64, ppc, arm, mips和ia64等处理器构架,开发机和测试机可用串行线或以太网进行连接通信。

kgdb补丁将下面的内容加入到内核代码中:

●gdb stub - gdb stub("树桩")是调试器的核心,它处理来自开发机上gdb的请求。当测试机运行了帯有kgdb的内核时,gdb stub控制测试机中所有的处理器。

●对出错处理例程的修改- 当一个不期望的错误发生时,内核将控制传递给kgdb调试器。不含有kgdb的内核在出现不可预测错误时会崩溃(panic),通过对出错处理的修改,kgdb允许开发者分析不可预测的出错。

●串行通信-该部件通过内核的串行驱动程序,为内核中的stub提供接口,负责在串行连接线上发送和接收数据,还负责处理开发机上gdb发送的处理控制断点请求。

建立kdbg联机调试的方法

下面说明建立kdbg联机调试的步骤:

(1)软件建立和应用kgdb补丁

1)下载Linux内核源代码:linux-2.6.15.5.tar.bz2。

2)下载与内核版本对应的kgdb补丁:linux-2.6.15.5-kgdb-2.4.tar.bz2。

3)解压缩软件包,方法如下:

cd ${BASE_DIR}

tar -jxvf linux-2.6.15.5.tar.bz2

cd ${BASE_DIR}/linux-2.6.15.5

tar -jxvf linux-2.6.15.5-kgdb-2.4.tar.bz2

在${BASE_DIR}/linux-2.6.15.5目录中,给Linux内核打kgdb补丁,方法如下:

patch -p1 < ${BASE_DIR}/linux-2.6.15.5-kgdb-2.4/core-lite.patch

patch -p1 < ${BASE_DIR}/linux-2.6.15.5-kgdb-2.4/i386.patch

(2)在开发机上编译内核

1)在${BASE_DIR}/linux-2.6.15.5/Makefile,设置EXTRAVERSION = -kgdb。

2)运行命令make xconfig或make oldconfig,出现如图2所示的内核配置界面。在配置界面中,为目标机硬件选择合适的选项;在"Kernel hacking"条目下,选择kgdb的选项。

Linux内核调试方法总结
    




Linux内核调试方法

图2 在Linux内核配置界面中与kgdb相关的配置选项

3)运行make bzImage编译内核。

4)将编译的内核从开发机上传送到目标机上,拷贝内核映像${BASE_DIR}/linux-2.6.15.5/arch/i386/boot/bzImage到目标机/boot/vmlinuz-2.6.15.5-kgdb。然后,再拷贝映射文件${BASE_DIR}/linux-2.6.15.5/System.map到目标机/boot/System.map-2.6.15.5-kgdb。再如下建立符号链接:

ln -s /boot/vmlinuz-2.6.15.5-kgdb /boot/vmlinuz

ln -s /boot/System.map-2.6.15.5-kgdb /boot/System.map

5)在目标机上编辑文件/boot/grub/grub.conf, 在该文件加入含有kgdb的内核条目,方法如下:

title Linux-2.6.15.5-kgdb

root (hd0,0)

kernel /boot/vmlinuz-2.6.15.5-kgdb ro root=/dev/hda1 kgdbwait

(3)在开发机上开始调试会话

1)在启动目标机后,它将等待开发机连接,显示下面的消息:

Waiting for connection from remote gdb...

2)用命令cd ${BASE_DIR}/linux-2.6.15.5进入目录linux-2.6.15.5目录。

3)用root用户登录设置调试会话波特率,方法如下:

<root#> gdb ./vmlinux

(gdb) set remotebaud 115200

(gdb) target remote /dev/ttyS0

Remote debugging using /dev/ttyS0

breakpoint () at kernel/kgdb.c:1212

1212 atomic_set(&kgdb_setting_breakpoint, 0);

warning: shared library handler failed to enable breakpoint

(gdb)

4)在开发机上输入调试命令

此时,gdb已连接到目标机上的内核,目标机上的内核正等待接收命令进行测试。输入命令(gdb) c(表示继续运行)时,目标机系统正常启动,在配置内核时,如果开发机选择了通过gdb输出控制台消息,则控制台log消息会从gdb上显示。

由gdb连接到测试内核,如果测试内核发生内核崩溃(kernel panic),它将首先将控制权转移给gdb,以让gdb分析崩溃原因。

(4)使用kgdb以太网接口

kgdb还可能通过以太网接口调试内核,用以太网接口建立连接的步骤说明如下:

1)添加下面的行到grub条目中:

kgdboe=@10.0.0.6/,@10.0.0.3/ (that's kgdboe=@LOCAL-IP/,@REMOTE-IP/)
# Sample grub.conf which will by default boot the kgdb enabled kernel
title Linux-2.6.15.5-kgdb(eth)
root (hd0,0)
kernel /boot/vmlinuz-2.6.15.5-kgdb ro root=/dev/hda1 kgdboe=@10.0.0.6/,@10.0.0.3/
console=ttyS0,115200

2)接着在gdb中用下面的命令开始调试会话:

(gdb) ./vmlinux

(gdb) target remote udp:HOSTNAME:6443

调试内核模块

内核可加载模块的调试具有其特殊性,内核模块中各段的地址在模块加载进内核后才最终确定的,开发机的gdb无法得到各种符号地址信息。因此,用户需要使用特殊版本的gdb,可以检测到内核模块的装载和卸载。另外,还需要将内核模块的符号装载到gdb中,这样,gdb才能解析到符号。

(1)准备检测内核模块装载和卸载代码的gdb派生版本

此步骤在开发机上完成。

安装在开发机上的gdb应含有内核模块调试特征,用户需要安装含有检测内核模块装载和卸载代码的gdb派生版本,该版本gdb派生于标准的gdb。用户可以从网址http://kgdb.linsyssoft.com/downloads.htm下载gdb-6.4-kgdb-2.4.tar.bz2,然后,编译安装生成gdb派生版本。或者下载gdbmod-2.4.bz2,解压缩后得到可执行的gdb派生版本。

在测试机上不需要特别的安装,内核模块可以出现在根文件系统或一个ramdisk中。

(2)装载内核模块符号到gdb中

此步骤在开发机上完成。

在开发机上,用户应装载内核模块的符号到gdb,让gdb调试时可以解析到二进制代码对应的符号。

首先,内核模块编译时应打开调试信息。然后,用下面方法设置内核、调试接口和定位内核模块位置:

#cd /usr/src/linux 2.6.13 
#gdbmod 2.4 vmlinux 
(gdb) target remote /dev/ttyS0
Remote debugging using /dev/ttyS0
breakpoint () at gdbstub.c:1153
1153 }
(gdb)set solib search path /usr/linux 2.6.13/drivers/net

一旦kgdb通知一个内核模块装载时,gdb必须能定位模块文件。因此,用户需要用命令"set solib-search-path"设置内核模块文件所在的路径。

(3)插入内核模块到内核

插入内核模块到内核,方法 如下:

# insmod mymodule.ko

到此,已装载了内核模块符号,内核模块可以像正常的内核代码一样调试了。

样例:调试内核模块test

1)编写内核模块

首先,在开发机上编写简单的内核模块test,代码如下:

void test_func()
{
    printk("test_func\n");
    printk("aaaaaaaaaaa\n");
}
 
int test_init()
{
    printk("test_init_module\n");
 
    return 0;
}
 
void test_exit()
{
    printk("test_cleanup_module\n");
}
 
module_init(test_init);
module_exit(test_exit);

2)编译安装内核模块

接着,编译内核模块,并将内核模块拷贝到测试机上。方法如下:

#cd /root/mymodule
#gcc -D__KERNEL__ -DMODULE -I/usr/src/linux-2.6.15/kernel/include -O -Wall -g -c -o test.ko test.c
#scp test.ko root@192.168.1.130:/root

3)开始调试

装载内核符号到gdb中,设置内核模块所在路径,方法如下:

# gdbmod vmlinux
 (gdb) set solib-search-path /root/mymodule

执行命令rmt,进入测试机调试,方法如下:

(gdb) rmt
breakpoint () at kgdbstub.c:1005
1005                    atomic_set(&kgdb_setting_breakpoint, 0);

在内核模块初始化处设置断点。查内核源码可知,内核模块初始化函数init在module.c文件函数sys_init_module函数中的mod->init处调用,对应行号为2168(根据不同版本的内核,行号可能不同)。设置断点方法如下:

(gdb) b module.c:2168
Breakpoint 1 at 0xc011cd83: file module.c, line 2168.

让测试机上的内核继续运行,方法如下:

(gdb) c
Continuing.
[New Thread 1352]
[Switching to Thread 1352]

测试机用命令"insmod test.ko"执行插入内核模块操作时,开发机会在断点处被暂停,暂停时显示如下

Breakpoint 1, sys_init_module (name_user=0xc03401bc "\001",
mod_user=0x80904d8) at module.c:2168
2168             ret = mod->init();

用step命令进入内核模块test的函数init,方法如下:

(gdb) step
test_init () at test.c:12
12              printk("test_init_module\n");
(gdb) n
15      }
(gdb)

对内核模块的非init函数调试时,由于测试机上已插入模块,模块的符号也已加载,只需要直接需要调试的代码处设置断点。如:在函数test_func处设置断点的方法如下:

(gdb)bt test_func

调试内核

用gdb调试内核类型于调试应用程序进程,kgdb支持gdb的执行控制命令、栈跟踪和线程分析等。但kgdb不支持watchpoint,kgdb通过gdb宏来执行watchpoint。

调试内核的命令说明如下:

(1)停止内核执行

用户在gdb终端中按下Ctrl + C键,gdb将发送停止消息给kgdb stub,kgdb stub控制内核的运行,并与gdb通信。

(2)继续内核运行

gdb命令"(gdb) c"告诉kgdb stub继续内核运行,直到遇到一个断点,或者gdb执行Ctrl + C,或其他原因,内核运行才停顿下来。

(3)断点

gdb断点(Breakpoints)用于在一个函数或代码行处暂停内核运行,设置断点命令如:"(gdb) b module.c:2168"。

(4)进入代码

使用命令"(gdb) step"进入一个函数或在暂停后执行下一个程序行;使用命令"(gdb) next"跳过一个函数执行下一个程序行或暂停后执行下一个程序;

(5)栈跟踪(Stack Trace)

使用命令"(gdb) bt"或"(gdb) backtrace"显示程序栈,它显示了调用函数的层次列表,表明了函数的调用函数。该命令还打印调解函数的参数值。

例如,运行命令backtrace的样例列出如下:

(gdb) backtrace
#0 breakpoint () at gdbstub.c:1160
#1 0xc0188b6c in gdb_interrupt (irq=3, dev_id=0x0, regs=0xc02c9f9c) at gdbserial.c:143
#2 0xc0108809 in handle_IRQ_event (irq=3, regs=0xc02c9f9c, action=0xc12fd200)3 0xc0108a0d in do_IRQ (regs={ebx = 1072672288, ecx = 0, edx = 1070825472, esi = 1070825472, edi = 1072672288, ebp = 1070817328, eax = 0, xds = 1072693224, xes = 1072693224, orig_eax = 253, eip = 1072672241, xcs = 16, eflags = 582, esp = 1070817308, xss = 1072672126}) at irq.c:621
#4 0xc0106e04 in ret_from_intr () at af_packet.c:1878
#5 0xc0105282 in cpu_idle () at process.c:135
#6 0xc02ca91f in start_kernel () at init/main.c:599
#7 0xc01001cf in L6 () at af_packet.c:1878
Cannot access memory at address 0x8e000

除非栈帧数作为命令backtrace的参数外,gdb仅在栈跟踪走出了可访问地址空间时才停止打印栈的信息。上面例子中,函数调用层次次序从上到下为:ret_from_intr, do_IRQ, handle_IRQ_event, gdb_interrupt。

放置一个断点在函数ext2_readlink,并访问一个符号链接,以便运行到该断点。 设置断点方法如下:

(gdb) br ext2_readlink
Breakpoint 2 at 0xc0158a05: file symlink.c, line 25.
(gdb) c
Continuing.

在测试机上运行命令"ls -l /boot/vmlinuz"显示一个符号链接。在测试机上,内核会运行到上述断点处,并暂停。然后,将断点信息传回开发机。在开发机上,用户可以查看栈或运行其他调试命令。例如:运行栈跟踪命令,显示的结果列出如下:

Breakpoint 2, ext2_readlink (dentry=0xc763c6c0, buffer=0xbfffed84 "\214\005",buflen=4096) at symlink.c:25 25 char *s = (char *)dentry >d_inode >u.ext2_i.i_data; 
(gdb) bt
#0 ext2_readlink (dentry=0xc763c6c0, buffer=0xbfffed84 "\214\005",buflen=4096) at symlink.c:25
#1 0xc013b027 in sys_readlink (path=0xbfffff77 "/boot/vmlinuz", buf=0xbfffed84 "\214\005", bufsiz=4096) at stat.c:262
#2 0xc0106d83 in system_call () at af_packet.c:1878
#3 0x804aec8 in ?? () at af_packet.c:1878
#4 0x8049697 in ?? () at af_packet.c:1878
#5 0x400349cb in ?? () at af_packet.c:1878

上述栈跟踪中,gdb打印一些无效的栈帧(#3~#5),这是因为gdb不知道在哪里停止栈跟踪,可以忽略这些无效的栈帧。

系统调用readlink在函数system_call进入内核,该函数显示在af_packet.c中,这是不对的,因为对于汇编语言文件的函数,gdb不能指出正常的代码行。但gdb可以正确处理在C语言文件中内联汇编代码。更多的调用层次是:sys_readlink和ext2_readlink。

在调试完后,用户可用"删除"命令和"继续"命令删除断点,并继续内核的运行,方法如下:

(gdb) delete
Delete all breakpoints? (y or n) y
(gdb) c
Continuing.

(6)内联函数

使用gdb栈跟踪命令通常足够找出一个函数的调用层次关系。但当其中一个栈帧在扩展的内联函数中,或者从一个内联函数访问另一个内联函数时,栈跟踪命令是不够用的,栈跟踪仅显示在内联函数中的源代码文件名和语句的行号,通过查看外面的函数,这可能知道调用的内联函数,但如果调用了两次内联函数,它就不知道是哪个内联函数了。

下面的处理流程可用来找出内联函数信息:

在栈跟踪中,gdb还与函数名一起显示代码地址,在调用了一个内联函数的语句中,gdb显示了这些代码调用和被调用的地址。脚本disasfun.sh可用来反汇编,源代码从vmlinux文件引用一个内核函数。文件vmlinux含有内核函数的绝对地址,因此,在汇编代码中看见的地址是在内存中的地址。

下面是一个样例。

配置内核时,kgdb应打开线程分析(CONFIG_KGDB_THREAD),gdb应连接到目标内核。

用"Ctrl+C"中断内核,放置一个断点在函数__down处,并继续运行,方法如下:

Program received signal SIGTRAP, Trace/breakpoint trap.
breakpoint () at gdbstub.c:1160
1160    }
(gdb) break __down
Breakpoint 1 at 0xc0105a43: file semaphore.c, line 62.
(gdb) c
Continuing.

为了让程序运行到断点处,在目标机上运行"man lilo"。程序会运行到断点,gdb会进入命令行模式。输入栈跟踪命令,显示如下:

Breakpoint 1, __down (sem=0xc7393f90) at semaphore.c:62
62              add_wait_queue_exclusive(&sem->wait, &wait);
(gdb) backtrace
#0  __down (sem=0xc7393f90) at semaphore.c:62
#1  0xc0105c70 in __down_failed () at af_packet.c:1878
#2  0xc011433b in do_fork (clone_flags=16657, stack_start=3221199556,
     regs=0xc7393fc4, stack_size=0)
     at /mnt/work/build/old-pc/linux-2.4.6-kgdb/include/asm/semaphore.h:120
#3  0xc010594b in sys_vfork (regs={ebx = 1074823660, ecx = 1074180970,
      edx = 1074823660, esi = -1073767732, edi = 134744856, ebp = -1073767712,
      eax = 190, xds = 43, xes = 43, orig_eax = 190, eip = 1074437320,
      xcs = 35, eflags = 518, esp = -1073767740, xss = 43}) at process.c:719

在函数sys_vfork中行号显示为719,这与文件process.c中的行号一致,查看该文件可得到确认,方法如下:

(gdb) list process.c:719
714      * do not have enough call-clobbered registers to hold all
715      * the information you need.
716      */
717     asmlinkage int sys_vfork(struct pt_regs regs)
718     {
719             return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, &regs, 0);
720     }
721
722     /*
723      * sys_execve() executes a new program.

就像gdb显示的一样,函数sys_vfork调用函数do_fork,再看栈跟踪显示的第2帧,gdb显示它在文件semaphore.h中的行号是120,显示的行号虽然没有用但是正确的。查看该文件可得到确认,方法如下:

(gdb) list semaphore.h:118
113      */
114     static inline void down(struct semaphore * sem)
115     {
116     #if WAITQUEUE_DEBUG
117             CHECK_MAGIC(sem->__magic);
118     #endif
119
120             __asm__ __volatile__(                                              <-----
121                     "# atomic down operation\n\t"
122                     LOCK "decl %0\n\t"     /* --sem->count */

上述代码中,在箭头所指示语句处,得到的信息仅是它在do_fork的一个扩展的内联函数down中。gdb还打印了在do_fork中从下一个被调用的函数开始代码的绝对地址:0xc011433b。这里我们用脚本disasfun找出该地址所对应的代码行。命令"disasfun vmlinux do_fork"输出的部分结果显示如下:

if ((clone_flags & CLONE_VFORK) && (retval > 0))
c011431d:       8b 7d 08                mov    0x8(%ebp),%edi
c0114320:       f7 c7 00 40 00 00       test   $0x4000,%edi
c0114326:       74 13                   je     c011433b <do_fork+0x707>
c0114328:       83 7d d4 00             cmpl   $0x0,0xffffffd4(%ebp)
c011432c:       7e 0d                   jle    c011433b <do_fork+0x707>
#if WAITQUEUE_DEBUG
CHECK_MAGIC(sem->__magic);
#endif
        __asm__ __volatile__(
c011432e:       8b 4d d0                mov    0xffffffd0(%ebp),%ecx
c0114331:       f0 ff 4d ec             lock decl 0xffffffec(%ebp)
c0114335:       0f 88 68 95 13 00       js     c024d8a3 <stext_lock+0x7bf>
                down(&sem);
        return retval;
c011433b:       8b 45 d4                mov    
0xffffffd4(%ebp),%eax              <-----
c011433e:       e9 8d 00 00 00          jmp    c01143d0 <do_fork+0x79c>
Looking at the code in fork.c  we know where above code is:
fork_out:
if ((clone_flags & CLONE_VFORK) && (retval > 0))
    down(&sem)

(7)线程分析

gdb具有分析应用程序线程的特征,它提供了应用程序创建的线程的列表,它允许开发者查看其中任何一个线程。gdb的特征可用来与kgdb一起查看内核线程。gdb能提供内核中所有线程的列表。开发者可指定一个线程进行分析。像backtrace,info regi这样的gdb命令接着可以显示指定线程上下文的信息。

应用程序创建的所有线程分享同一地址空间,相似地,所有内核线程共享内核地址空间。每个内核线程的用户地址空间可能不同,因此,gdb线程可较好地分析内核代码和驻留在内核空间的数据结构。

gdb info给出了关于gdb线程分析方面更多的信息。下面列出一个内核线程分析的样例:

gdb命令"info threads"给出了内核线程的列表,显示如下:

(gdb) info thr
  21 thread 516  schedule_timeout (timeout=2147483647) at sched.c:411
  20 thread 515  schedule_timeout (timeout=2147483647) at sched.c:411
  19 thread 514  schedule_timeout (timeout=2147483647) at sched.c:411
  18 thread 513  schedule_timeout (timeout=2147483647) at sched.c:411
  17 thread 512  schedule_timeout (timeout=2147483647) at sched.c:411
  16 thread 511  schedule_timeout (timeout=2147483647) at sched.c:411
  15 thread 438  schedule_timeout (timeout=2147483647) at sched.c:411
  14 thread 420  schedule_timeout (timeout=-1013981316) at sched.c:439
  13 thread 406  schedule_timeout (timeout=-1013629060) at sched.c:439
  12 thread 392  do_syslog (type=2, buf=0x804dc20 "run/utmp", len=4095)
    at printk.c:182
  11 thread 383  schedule_timeout (timeout=2147483647) at sched.c:411
  10 thread 328  schedule_timeout (timeout=2147483647) at sched.c:411
  9 thread 270  schedule_timeout (timeout=-1011908724) at sched.c:439
  8 thread 8  interruptible_sleep_on (q=0xc02c8848) at sched.c:814
  7 thread 6  schedule_timeout (timeout=-1055490112) at sched.c:439
  6 thread 5  interruptible_sleep_on (q=0xc02b74b4) at sched.c:814
  5 thread 4  kswapd (unused=0x0) at vmscan.c:736
  4 thread 3  ksoftirqd (__bind_cpu=0x0) at softirq.c:387
  3 thread 2  context_thread (startup=0xc02e93c8) at context.c:101
  2 thread 1  schedule_timeout (timeout=-1055703292) at sched.c:439
* 1 thread 0  breakpoint () at gdbstub.c:1159
(gdb)

如上所显示,gdb为每个线程设定在gdb中唯一的id,当gdb内部引用一个线程时,可以使用这个id。例如:线程7(PID 7)具有gdb id 8。为了分析内核线程8 ,我们指定线程9给gdb。gdb接着切换到该线程中,准备做更多的分析。

下面是分析线程的命令显示:

(gdb) thr 9
[Switching to thread 9 (thread 270)]
#0  schedule_timeout (timeout=-1011908724) at sched.c:439
439             del_timer_sync(&timer);
(gdb) bt
#0  schedule_timeout (timeout=-1011908724) at sched.c:439
#1  0xc0113f36 in interruptible_sleep_on_timeout (q=0xc11601f0, timeout=134)
    at sched.c:824
#2  0xc019e77c in rtl8139_thread (data=0xc1160000) at 8139too.c:1559
#3  0xc010564b in kernel_thread (fn=0x70617773, arg=0x6361635f,
    flags=1767859560) at process.c:491
#4  0x19 in uhci_hcd_cleanup () at uhci.c:3052
#5  0x313330 in ?? () at af_packet.c:1891
Cannot access memory at address 0x31494350
(gdb) info regi
eax            0xc38fdf7c       -1013981316
ecx            0x86     134
edx            0xc0339f9c       -1070358628
ebx            0x40f13  266003
esp             0xc3af7f74       0xc3af7f74
ebp            0xc3af7fa0       0xc3af7fa0
esi            0xc3af7f8c       -1011908724
edi            0xc3af7fbc       -1011908676
eip            0xc011346d       0xc011346d
eflags         0x86     134
cs             0x10     16
ss             0x18     24
ds             0x18     24
es             0x18     24
fs             0xffff   65535
gs             0xffff   65535
fctrl          0x0      0
fstat          0x0      0
ftag           0x0      0
fiseg          0x0      0
fioff          0x0      0
foseg          0x0      0
fooff          0x0      0
---Type <return> to continue, or q <return> to quit---
fop            0x0      0
(gdb) thr 7
[Switching to thread 7 (thread 6)]
#0  schedule_timeout (timeout=-1055490112) at sched.c:439
439             del_timer_sync(&timer);
(gdb) bt
#0  schedule_timeout (timeout=-1055490112) at sched.c:439
#1  0xc0137ef2 in kupdate (startup=0xc02e9408) at buffer.c:2826
#2  0xc010564b in kernel_thread (fn=0xc3843a64, arg=0xc3843a68,
    flags=3280222828) at process.c:491
#3  0xc3843a60 in ?? ()
Cannot access memory at address 0x1f4
(gdb)

用户可从http://sourceware.org/gdb/download/下载进程信息宏ps和psname,ps宏提供了运行在内核的线程的名字和ID。运行结果显示如下:

(gdb) ps
0 swapper
1 init
2 keventd
3 ksoftirqd_C
4 kswapd
5 bdflush
6 kupdated
8 khubd
270 eth0
328 portmap
383 syslogd
392 klogd
406 atd
420 crond
438 inetd
511 mingetty
512 mingetty
513 mingetty
514 mingetty
515 mingetty
516 mingetty
(gdb) 
The psname macro can be used to get name of a thread when it's id is known.
(gdb) psname 8
8 khubd
(gdb) psname 7
(gdb)
 
(7)Watchpoints

(7)Watchpoints

kgdb stub使用x86处理器的调试特征支持硬件断点,这些断点不需要代码修改。它们使用调试寄存器。x86体系中的ia32处理器有4个硬件断点可用。每个硬件断点可以是下面三个类型之一:

执行断点 当代码在断点地址执行时,触发执行断点。由于硬件断点有限,建议通过gdb break命令使用软件断点,除非可避免修改代码。

写断点 当系统对在断点地址的内存位置进行写操作时,触发一个写断点。写断点可以放置可变长度的数据。写断点的长度指示为观察的数据类型长度,1表示为字节数据,2表示为2字节数据,3表示为4字节数据。

访问断点 当系统读或写断点地址的内存时,触发一个访问断点。访问断点也有可变长度数据类型。

ia-32处理器不支持IO断点。

因为gdb stub目前不使用gdb用于硬件断点的协议,因此,它通过gdb宏访问硬件断点。硬件断点的gdb宏说明如下:

1)hwebrk – 放置一个执行断点。

用法:hwebrk breakpointno address

2)hwwbrk – 放置一个写断点。

用法:hwwbrk breakpointno length address

3)hwabrk – 放置一个访问断点。

用法:hwabrk breakpointno length address

4)hwrmbrk – 删除一个断点

用法:hwrmbrk breakpointno

5)exinfo – 告诉是否有一个软件或硬件断点发生。如果硬件断点发生,打印硬件断点的序号。

这些命令要求的参数说明如下:

breakpointno – 0~3

length - 1~3

address - 16进制内存位置(没有0x),如:c015e9bc

使用UML调试Linux内核

用户模式Linux(User Mode Linux,UML)不同于其他Linux虚拟化项目,UML尽量将它自己作为一个普通的程序。UML与其他虚拟化系统相比,优点说明如下:

良好的速度

UML编译成本地机器的代码,像主机上的其他已编译应用程序一样运行。它比在软件上应用整个硬件构架的虚拟机快得多。另一方面,UML不需要考虑依赖于特定CPU的虚拟化系统的硬件特异性。

获益于Liunx更新

每次Linux的改进,UML自动得到这些功能,虚拟化系统并不一定能从更新中获益。

弹性编码

内核需要与硬件或虚拟硬件交互,但UML可将交互看作其他方式。例如:可以将这些交互转换成共享的库,其他程序可以在使用时连接该库。它还可作为其他应用程序的子shell启动,能任何其他程序的stin/stdout使用。

可移植性

UML将来可以移植到x86 Windows, PowerPC Linux, x86 BSD或其他系统上运行。

从Linux2.6.9版本起,用户模式Linux(User mode Linux,UML)已随Linux内核源代码一起发布,它存放于arch/um目录下。编译好UML的内核之后,可直接用gdb运行编译好的内核并进行调试。

UML原理

用户模式Linux(User mode Linux,UML)将Linux内核的一部分作为用户空间的进程运行,称为客户机内核。UML运行在基于Linux系统调用接口所实现的虚拟机。UML运行的方式如图1所示。UML像其他应用程序一样与一个"真实"的Linux内核(称为"主机内核")交互。应用程序还可运行在UML中,就像运行在一个正常的Linux内核下。

 

Linux内核调试方法总结
    




Linux内核调试方法

图1 UML在Linux系统中运行的位置

使用UML的优点列出如下:

如果UML崩溃,主机内核还将运行完好。

可以用非root用户运行UML。

可以像正常进程一样调试UML。

在不中断任何操作下与内核进行交互。

用UML作为测试新应用程序的"沙箱",用于测试可能有伤害的程序。

可以用UML安全地开发内核。

可以同时运行不同的发布版本。

由于UML基于以Linux系统调用接口实现的虚拟机,UML无法访问主机的硬件设备。因此,UML不适合于调试与硬件相关的驱动程序。

编译UML模式客户机Linux内核

(1)获取源代码

http://www.kernel.org/下载linux-2.6.24.tar.bz2,解压缩源代码,方法如下:

host% bunzip2 linux-2.6.24.tar.bz2

host% tar xf linux-2.6.24.tar

host% cd linux-2.6.24

(2)配置UML模式内核

如果使用缺省配置,那么,方法如下:

host% make defconfig ARCH=um

如果运行配置界面,方法如下:

host% make menuconfig ARCH=um

如果不使用缺省配置defconfig,那么,内核编译将使用主机的配置文件,该配置文件在主机/boot目录下。对于UML模式内核来说,这是不对的,它将编译产生缺乏重要的驱动程序和不能启动的UML。

以编译UML时,每个make命令应加上选项"ARCH=um",或者设置环境变量"export ARCH=um"。

当再次配置时,可以先运行下面的命令清除所有原来编译产生的影响:

host% make mrproper

host% make mrproper ARCH=um

内核提供了配置选项用于内核调试,这些选项大部分在配置界面的kernel hacking菜单项中。一般需要选取CONFIG_DEBUG_INFO选项,以使编译的内核包含调试信息。

(3)编译UML模式内核

编译内核的方法如下:

host% make ARCH=um

当编译完成时,系统将产生名为"linux"的UML二进制。查看方法如下:

host% ls -l linux

-rwxrwxr-x 2 jdike jdike 18941274 Apr 7 15:18 linux

由于UML加入了调试符号,UML模式内核变得很大,删除这些符号将会大大缩小内核的大小,变为与标准内核接近的UML二进制。

现在,用户可以启动新的UML模式内核了。

(4)UML的工具

使用UML和管理UML的工具说明如下:

UMLd – 用于创建UML实例、管理实例启动/关闭的后台程序。

umlmgr –用于管理正运行的UML实例的前台工具程序。

UML Builder – 编译根文件系统映像(用于UML模式操作系统安装)。

uml switch2 用于后台传输的用户空间虚拟切换。

VNUML – 基于XML的语言,定义和启动基于UML的虚拟网络场景。

UMLazi – 配置和运行基于虚拟机的UML的管理工具。

vmon – 运行和监管多个UML虚拟机的轻量级工具,用Python 书写。

umvs – umvs是用C++和Bash脚本写的工具,用于管理UML实例。该应用程序的目的是简化UML的配置和管理。它使用了模板,使得编写不同的UML配置更容易。

MLN - MLN (My Linux Network) 是一个perl程序,用于从配置文件创建UML系统的完整网络,使得虚拟网络的配置和管理更容易。MLN基于它的描述和简单的编程语言编译和配置文件系统模板,并用一种组织方式存储它们。它还产生每个虚拟主机的启动和停止脚本,在一个网络内启动和停止单个虚拟机。MLN可以一次使用几个独立的网络、项目,甚至还可以将它们连接在一起。

Marionnet – 一个完全的虚拟网络实验,基于UML,带有用户友好的图形界面。

运行UML

(1)启动UML

为了运行UML实例,用户需要运行Linux操作系统主机和带有自己文件系统的UML客户机。用户可以从http://uml.nagafix.co.uk/下载UML(如:kernel)和客户机文件系统(如:root_fs),运行UML实例的方法如下:

$ ./kernel ubda= root_fs mem=128M

上述命令中,参数mem指定虚拟机的内存大小;参数ubda表示根文件系统root_fs作为虚拟机第一个块设备,虚拟机用/dev/udba表示虚拟机的第一个块设备,与Linux主机系统的第一个物理块设备/dev/sda类似。

用户还可以自己创建虚拟块设备,例如:建立交换分区并在UML上使用它的方法如下:

$ dd if=/dev/zero of=swap bs=1M count=128

$ ./kernel ubda= root_fs ubdb=swap mem=128M

上述命令,创建了128M的交换分区,作为第二个块设备ubdb,接着,启动UML模式内核,用ubdb作为它的交换分区。

(2)登录

预打包的文件系统有一个带有"root"密码的root帐户,还有一个带有"user"密码的user帐户。用户登录后可以进入虚拟机。预打包的文件系统已安装了各种命令和实用程序,用户还可容易地添加工具或程序。

还有一些其他登录方法,说明如下:

在虚拟终端上登录

每个已配置(设备存在于/dev,并且/etc/inittab在上面运行了一个getty)的虚拟终端有它自己的xterm。.

通过串行线登录

在启动输出中,找到类似下面的一行:

serial line 0 assigned pty /dev/ptyp1

粘贴用户喜爱的终端程序到相应的tty,如:minicom,方法如下:

host% minicom -o -p /dev/ttyp1

通过网络登录

如果网络正运行,用户可用telnet连接到虚拟机。

虚拟机运行后,用户可像一般Linux一样运行各种shell命令和应用程序。

建立串行线和控制台

可以粘附UML串行线和控制台到多个类型的主机I/O通道,通过命令行指定,用户可以粘附它们到主机ptys, ttys, 文件描述子和端口。常用连接方法说明如下:

让UML控制台出现在不用的主机控制台上。

将两个虚拟机连接在一起,一个粘到pty,另一个粘附到相应的tty。

创建可从网络访问的虚拟,粘附虚拟机的控制台到主机的一个端口。

(1)指定设备

用选项"con"或"ssl"(分别代表控制台和串行线)指定设备。例如:如果用户想用3号控制台或10号串行线交互,命令行选项分别为"con3"和"ssl10"。

例如:指定pty给每个串行线的样例选项列出如下:

ssl=pty ssl0=tty:/dev/tty0 ssl1=tty:/dev/tty1

(2)指定通道

可以粘附UML设备到多个不同类型的通道,每个类型有不同的指定方法,分别说明如下:

伪终端为:device=pty,pts终端为:device=pts

UML分配空闲的主机伪终端给。用户可以通过粘附终端程序到相应的tty访问伪终端,方法如下:

screen /dev/pts/n

screen /dev/ttyxx

minicom -o -p /dev/ttyxx #minicom似乎不能处理pts设备

kermit #启动它,打开设备,然后连接设备

终端为:device=tty:tty设备文件

UML将粘附设备到指定的tty,例如:一个样例选项列出如下:

con1=tty:/dev/tty3

上面语句将粘附UML的控制台1到主机的/dev/tty3。如果用户指定的tty是tty/pty对的slave端,则相应的pty必须已打开。

xterms为:device=xterm

UML将运行一个xterm,并且将设备粘附到xterm。

端口为:device=port:端口号

上述选项将粘附UML设备到指定的主机端口。例如:粘附控制台1到主机的端口9000,方法如下:

con1=port:9000

粘附所有串行线到主机端口,方法如下:

ssl=port:9000

用户可以通过telnet到该端口来访问这些设备,每个激活的telnet会话得到不同的设备,如果有比粘附到端口的UML设备多的telnet连接到一个端口,格外的telnet会话将阻塞正存在的telnet断线,或直到其他设备变为激活(如:通过在/etc/inittab中设置激活)。

已存在的文件描述子:device=文件描述子

如果用户在UML命令行中建立了一个文件描述子,他可以粘附UML设备到文件描述子。这最常用于在指定所有其他控制台后将主控制台放回到stdin和stdout上。方法如下:

con0=fd:0,fd:1 con=pts

null设备:device=null

与"none"选项相比,上述选项允许打开设备,但读将阻塞,并且写将成功,但数据会被丢掉。

无设备:device=none

上述选项将引起设备消失。如果你正使用devfs,设备将不出现在/dev下。如果设备出现,尝试打开它将返回错误-ENODEV。

用户还可以指定不同的输入和输出通道给一个设备,最常用的用途是重粘附主控制到stdin和stdout。例如:一个样例选项列出如下:

ssl3=tty:/dev/tty2,xterm

上述诗句将引起在主机/dev/tty3上的串行线3接受输入,显示输出在xterm上。

如果用户决定将主控制台从stdin/stdout移开,初始的启动输出将出现在用户正运行UML所在的终端。然而,一旦控制台驱动程序已初始化,启动及随后的输出将出现在控制台0所在的地方。

建立网络

UML实例可以用网络访问主机、本地网络上的其他机器和网络的其他部分。新的辅助程序uml_net进行主机建立时需要root权限。

当前UML虚拟机有5种传输类型用于与其他主机交换包,分别是:ethertap,TUN/TAP, Multicast,交换机后台(switch daemon),slip,slirp和pcap。

TUN/TAP, ethertap, slip和slirp传输允许UML实例与主机交换包。它们可定向到主机或主机可扮作路由器提供对其他物理或虚拟机的访问。

pcap传输是一个综合的仅读接口,用libpcap二进制从主机上的接口收集包并过滤包。这对于构建预配置的交通监管器或sniffer来说,是有用的。

后台和多播传输提供了完全虚拟的网络络其他虚拟机器。该网络完全从物理网络断开,除非某一个虚拟机扮作网关。

如何选择这些主机传输类型 '用户可根据用途进行选择,选择方法说明如下:

ethertap – 如果用户想对主机网络进行访问,并且运行以2.2以前版本时,使用它。

TUN/TAP – 如果用户想访问主机网络,可使用它。TUN/TAP 运行在2.4以后的版本,比ethertap 更有效率。TUN/TAP 传输还能使用预置的设备,避免对uml_net辅助程序进行setuid操作。

Multicast – 如果用户期望建立一个纯虚拟网络,并且仅想建立UML,就使用它。

交换机后台 – 如果用户想建立一个纯虚拟网络,并且不介意为了得到较好执行效率而建立后台,就使用它。

slip – 没有特殊理由不要运行slip后端,除非ethertap和TUN/TAP不可用。

slirp – 如果用户在主机上没有root权限对建立网络进行访问,或者如果用户不想分配对UML分配IP时,使用它。

pcap – 对实际的网络连接没有太多用途,但用于主机上监管网络交通很有用。

(1)网络建立通用步骤

首先,用户必须已在UML中打开虚拟网络。如果运行下载的预编译的内核,则已打开虚拟网络。如果用户自己编译内核,则在配置界面上的"Network device support"菜单中,打开"Network device support"和三种传输选项。

下一步是提供网络设备给虚拟机,通过在内核命令行中进行描述,格式如下:

eth <n> = <transport> , <transport args>

例如:一个虚拟以太网设备可以如下粘附到一个主机ethertap上:

eth0=ethertap,tap0,fe:fd:0:0:0:1,192.168.0.254

上述语句在虚拟机内部建立eth0,粘附它自己到主机/dev/tap0,指定一个以太网地址,并指定给主机tap0接口一个IP地址。

一旦用户决定如何建立设备后,就可以启动UML、登录、配置设备的UML侧,并设置对外界的路由。此后,UML就可以与网络任何其他机器(物理或虚拟的)通信。

(2)用户空间后台

http://www.user-mode-linux.org/cvs/tools/下载工具uml_net和uml_switch,编译并安装。uml_switch是在UML系统之间管理虚拟网络的后台,而不用连接到主机系统的网络。uml_switch将在UNIX域的socket上监听连接,并在连接到UNIX域的客户端之间转发包。

(3)指定以太网地址

TUN/TAP, ethertap和daemon接口允许用户给虚拟以太网设备指定硬件地址。但通常不需要指定硬件地址。如果命令行没有指定硬件地址,它将提供地址为fe:fd:nn:nn:nn:nn,其中,nn.nn.nn.nn是设备IP地址。这种方法通常足够保证有唯一的硬件地址。

(4)UML接口建立

一旦用命令行描述网络设备,用户在启动UML和登录后,第一件事应是建立接口,方法如下:

UML# ifconfig ethn ip-address up

此时,用户应可以ping通主机。为了能查看网络,用户设置缺省的路由为到达主机,方法如下:

UML# route add default gw host ip

例如:主机IP为192.168.0.4,设置路由方法如下:

UML# route add default gw 192.168.0.4

注意:如果UML不能与物理以太网上其他主机通信,可能是因为网络路由自动建立,可以运行"route –n"查看路由,结果类似如下:

Destination Gateway Genmask Flags Metric Ref Use Iface

192.168.0.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0

掩码不是255.255.255.255,因此,就使用到用户主机的路由替换它,方法如下:

UML# route del -net 192.168.0.0 dev eth0 netmask 255.255.255.0

UML# route add -host 192.168.0.4 dev eth0

添加缺省的路由到主机,将允许UML与用户以太网上任何机器交换包。

(5)多播

在多个UML之间建立一个虚拟网络的最简单方法是使用多播传输。用户的系统必须在内核中打开多播(multicast),并且在主机上必须有一个多播能力的网络设备。通常它是eth0。

为了使用多播,运行两个UML,命令行带有"eth0=mcast"选项。登录后,用户在每个虚拟机上用不同的IP地址配置以太网设备,方法如下:

UML1# ifconfig eth0 192.168.0.254

UML2# ifconfig eth0 192.168.0.253

这两个虚拟机应能相互通信。

传输设置的整个命令行选项列出如下:

ethn=mcast,ethernet address,multicast address,multicast port,ttl

(6)TUN/TAP和uml_net辅助程序

TUN/TAP驱动程序实现了虚拟网卡的功能,TUN 表示虚拟的是点对点设备,TAP表示虚拟的是以太网设备,这两种设备针对网络包实施不同的封装。利用TUN/TAP驱动,可以将tcp/ip协议栈处理好的网络分包传给任何一个使用TUN/TAP驱动的进程,由进程重新处理后再发到物理链路中。

TUN/TAP是与主机交换包的较好机制,主机建立TUN/TAP较简单的方法是使用uml_net辅助程序,它包括插入tun.o内核模块、配置设备、建立转发IP、路由和代理ARP。

如果在设备的主机侧指定了IP地址,uml_net将在主机上做所有的建立工作。粘附设备到TUN/TAP设备的命令行格式列出如下:

eth <n> =tuntap,,, <host IP address>

例如:下面参数将粘附UML的eth0到下一个可用的tap设备,指定IP地址192.168.0.254么tap设备的主机侧,并指定一个基于IP地址的以太网地址。

eth0=tuntap,,,192.168.0.254

(7)带有预配置的tap设备的TUN/TAP

如果用户没有更好的uml_net,可以预先建立TUN/TAP。步骤如下:

用工具tunctl创建tap设备,方法如下:

host# tunctl -u uid

上述命令中,uid是用户的ID或UML将运行登录的用户名。

配置设备IP地址,方法如下:

host# ifconfig tap0 192.168.0.254 up

建立路由和ARP,方法如下:

host# bash -c 'echo 1 > /proc/sys/net/ipv4/ip_forward'

host# route add -host 192.168.0.253 dev tap0

host# bash -c 'echo 1 > /proc/sys/net/ipv4/conf/tap0/proxy_arp'

host# arp -Ds 192.168.0.253 eth0 pub

注意:这个配置没有重启机时失效,每次主机启动时,应重新设置它。最好的方法是用一个小应用程序,每次启动时,读出配置文件重新建立设置的配置。

使用网桥

为了不使用2个IP地址和ARP,还可通过对UML使用网桥提供对用户LAN直接访问,方法如下:

host# brctl addbr br0

host# ifconfig eth0 0.0.0.0 promisc up

host# ifconfig tap0 0.0.0.0 promisc up

host# ifconfig br0 192.168.0.1 netmask 255.255.255.0 up

host# brctl stp br0 off

host# brctl setfd br0 1

host# brctl sethello br0 1

host# brctl addif br0 eth0

host# brctl addif br0 tap0

注意:用户应该用eth0的IP地址通过ifconfig建立"br0"。

运行UML

一旦设备建立好后,运行UML,命令格式为: eth0=tuntap,devicename,例如:一个样例列出如下:

eth0=tuntap,tap0

如果用户不再使用tap设置,可以用下面命令删除它:

host# tunctl -d tap device

最后,tunctl有一个"-b"(用于简捷模式)切换,仅输出它所创建的tap设备的名字。它很适合于被一个脚本使用,方法如下:

host# TAP=`tunctl -u 1000 -b`

(8)交换机后台

交换机后台uml_switch以前称为uml_router,它提供了创建整个虚拟网络的机制。缺省下,它不提供对主机网络的连接。

首先,用户需要运行uml_switch,无参数运行时,表示它将监听缺省的unix域socket。使用选项"-unix socket"可指定不同的socket,"-hub"可将交换机后台变为集线器(Hub)。如果用户期望交换机后台连接到主机网络(允许UML访问通过主机访问外部的网络),可使用选项"-tap tap0"。

uml_switch还可作为后台运行,方法如下:

host% uml_switch [ options ] < /dev/null > /dev/null

内核命令行交换机的通用命令行格式列出如下:

ethn=daemon,ethernet address,socket type,socket

通常只需要指定参数"daemon",其他使用缺省参数,如果用户运行没有参数的交换机后台,在同一台机器上使用选项"eth0=daemon"运行UML,etho驱动程序会直接粘附它自己到交换机后台。参数socket为unix域socket的文件名,用于uml_switch和UML之间网络通信。

(9)Slirp

slirp通常使用外部程序/usr/bin/slirp,仅通过主机提供IP网络连接。它类似于防火墙的IP伪装,跃然传输有用户空间进行,而不是由内核进行。slirp不在主机上建立任何接口或改变路由。slirp在主机上不需要root权限或运行setuid。

slirp命令行的通用格式为:

ethn=slirp,ethernet address,slirp path

在UML上,用户应使用没有网关IP的etho设置缺省路由,方法如下:

UML# route add default dev eth0

slirp提供了UML可使用的大量有用IP地址,如:10.0.2.3,是DNS服务器的一个别名,定义在主机/etc/resolv.conf中,或者它是slirp的选项"dns"中给定的IP地址。

(10)pcap

pcap对网络上传输的数据包进行截获和过滤。通过命令行或pcap传输粘附到UML以太网设备uml_mconsole工具,语法格式如下:

ethn=pcap,host interface,filter expression,option1,option2

其中,expression和option1、option2是可选的。

这个接口是主机上用户想嗅探(sniff)的任何网络设备,过滤器表达式(filter expression)与工具tcpdump使用的一样,option1为"promisc"或"nopromisc",控制pcap是否将主机接口设为"promiscuous"(混杂)模式;option2为"optimize "或"nooptimize",表示是否使用pcap表达式优化器。

一个设置pcap的样例列出如下:

eth0=pcap,eth0,tcp

eth1=pcap,eth0,!tcp

上述语句将引起在主机eth0上的UML eth0将所有的tcp发出,并且在主机eth0上的UML eth1将发出所有非tcp包。

(11)用户建立主机

主机上的网络设备需要配置IP地址,还需要用值为1484的mtu配置tap设备。slip设置还需要配置点到点(pointopoint)地址,方法如下:

host# ifconfig tap0 arp mtu 1484 192.168.0.251 up

host# ifconfig sl0 192.168.0.251 pointopoint 192.168.0.250 up

如果正建立tap设备,就将路由设置到UML IP。方法如下:

UML# route add -host 192.168.0.250 gw 192.168.0.251

为了允许网络上其他主机看见这个虚拟机,化理ARP设置如下:

host# arp -Ds 192.168.0.250 eth0 pub

最后,将主机设置到路由包,方法如下:

host# echo 1 > /proc/sys/net/ipv4/ip_forward

在虚拟机间共享文件系统

在虚拟机间共享文件系统的方法是使用ubd(UML Block Device)块设备驱动程序的写拷贝(copy-on-write,COW)分层能力实现。COW支持在仅读的共享设备上分层读写私有设备。一个虚拟机的写数据存储在它的私有设备上,而读来自任一请求块有效的设备。如果请求的块有效,读取私有设备,如果无效,就读取共享设备。

用这种方法,数据大部分在多个虚拟机间共享,每个虚拟机有多个小文件用于存放虚拟机所做的修改。当大量UML从一个大的根文件系统启动时,这将节约大量磁盘空间。它还提供执行性能,因为主机可用较小的内存缓存共享数据,主机的内存而不是硬盘提供UML硬盘请求服务。

可通过简单地加COW文件的名字到合适的ubd,实现加一个COW层到存在的块设备文件。方法如下:

ubd0=root_fs_cow,root_fs_debian_22

上述语句中,"root_fs_cow"是私有的COW文件,"root_fs_debian_22"是存在的共享文件系统。COW文件不必要存在,如果它不存在,驱动程序将创建并初始化它。一旦COW文件已初始化,可在以命令行中使用它,方法如下:

ubd0=root_fs_cow

后备文件(backing file)的名字存在COW文件头中,因此在命令行中继续指定它将是多余的。

COW文件是稀疏的,因此它的长度不同于硬盘的实际使用长度。可以用命令"ls –ls"查看硬盘的实际消耗,用"ls –l"查看COW文件和后备文件(backing file)的长度,方法如下:

host% ls -l cow.debian debian2.2

-rw-r--r-- 1 jdike jdike 492504064 Aug 6 21:16 cow.debian

-rwxrw-rw- 1 jdike jdike 537919488 Aug 6 20:42 debian2.2

host% ls -ls cow.debian debian2.2

880 -rw-r--r-- 1 jdike jdike 492504064 Aug 6 21:16 cow.debian

525832 -rwxrw-rw- 1 jdike jdike 537919488 Aug 6 20:42 debian2.2

从上述显示结构,用户会发现COW文件实际硬盘消耗小于1M,面不是492M。

一旦文件系统用作一个COW文件的仅读后备文件,不要直接从它启动或修改它。这样,会使使用经的任何COW文件失效。后备文件在创建时它的修改时间mtime和大小size存放在COW文件头中,它们必须相匹配。如果不匹配,驱动程序将拒绝使用COW文件。

如果用户手动地改变后备文件或COW头,将得到一个崩溃的文件系统。

操作COW文件的方法说明如下:

(1)删除后备文件

由于UML存放后备文件名和它的修改时间mtime在COW头中,如果用户删除后文件文件,这些信息将变成无效的。因此,删除后备文件的步骤如下:

用保护时间戳的方式删除文件。通常,使用"-p"选项。拷贝操作命令"cp –a"中的"-a"隐含了"-p"。

通过启动UML更新COW头,命令行指定COW文件和新的后备文件的位置,方法如下:

ubda=COW file,新的后备文件位置

UML将注意到命令行和COW头之间的不匹配,检查新后文件路径的大小和修改时间mtime,并更新COW头。

如果当用户删除后备文件时忘记保留时间戳,用户可手动整理mtime,方法如下:

host% mtime=UML认定的修改时间mtime; \

touch --date="`date -d 1970-01-01\ UTC\ $mtime\ seconds`" 后备文件

注意如果对真正修改过而不是刚删除的后备文件进行上述操作,那么将会文件崩溃,用户将丢失文件系统。

(2)uml_moo :将COW文件与它的后备文件融合

依赖于用户如何使用UML和COW设备,系统可能建议每隔一段时间融合COW文件中的变化到后备文件中。用户可以用工具uml_moo完成该操作,方法如下:

host% uml_moo COW file new backing file

由于信息已在COW文件头中,因此,不必指定后备文件。

uml_moo在缺省下创建一个新的后备文件,它还有一个破坏性的融合选项,直接将融合COW文件到它当前的后备文件。当后备文件仅有一个COW文件与它相关时,该选项很有用。如果多个COW与一个后备文件相关,融合选项"-d"将使所有其他的COW无效。但是,如果硬盘空间不够时,使用融合选项"-d"很方便快捷,方法如下:

host% uml_moo -d COW file

(3)uml_mkcow :创建新COW文件

正常创建COW文件的方法是以UML命令行中指定一个不存在的COW文件,让UML创建COW文件。但是,用户有时想创建一个COW文件,但不想启动UML。此时,可以使用uml_mkcow工具。方法如下:

host% uml_mkcow 新COW文件 存在的后备文件

如果用户想销毁一个存在的COW文件,可以加"-f"选项强制重写旧的COW文件,方法如下:

host% uml_mkcow -f 存在的COW文件 存在的后备文件

创建UML的文件系统

如果根文件系统硬盘空间不够大,或者想使用不同于ext2的文件系统,用户就可能想创建和挂接新的UML文件系统,用户可以用如下方法创建UML的根文件系统:

(1)创建文件系统的文件

使用命令dd创建一个合适尺寸的空文件,用户可以创建稀疏文件,该文件直到实际使用时才分配硬盘空间。例如:下面的命令创建一个100M填满0的稀疏文件:

host% dd if=/dev/zero of=new_filesystem seek=100 count=1 bs=1M

(2)指定文件给一个UML设备

在UML命令行上加入下面的选项:

ubdd=new_filesystem

上述命令中,ubdd应确保没被使用。

(3)创建和挂接文件系统

创建和挂接文件系统方法如下:

host# mkreiserfs /dev/ubdd

UML# mount /dev/ubdd /mnt

主机文件访问

如果用户在UML中想访问主机上的文件,用户可将主机当作独立的机器,可以使用nfs从主机挂接目录,或者用scp和rcp拷贝文件到虚拟机,因为UML运行在主机上,它能象其他进程一样访问这些文件,并使它们在虚拟机内部可用,而不需要使用网络。

还可以使用hostfs虚拟文件系统,用户通过它可以挂接一个主机目录到UML文件系统,并像在主机上一样访问该目录中的文件。

(1)使用hostfs

首先,确认虚拟机内部是否有hostfs可用,方法如下:

UML# cat /proc/filesystems

如果没有列出hostfs,则需要重编译内核,配置hostfs,将它编译成一个内核模块,并用"insmod"插入该内核模块。

挂接hostfs文件系统,例如:将hostfs挂接到虚拟机的/mnt/host下,方法如下:

UML# mount none /mnt/host -t hostfs

如果用户不想挂接主机的root目录,他可以用"-o"选项指定挂接的子目录。例如:挂接主机的/home到虚拟机的/mnt/home,方法如下:

UML# mount none /mnt/home -t hostfs -o /home

(2)hostfs命令行选项

在UML命令行选项可使用hostfs选项,用来指定多个hostfs挂接到一个主机目录或阻止hostfs用户从主机上销毁数据,方法如下:

hostfs=directory,options

当前可用的选项是"append",用来阻止所有的文件在追加方式打开,并不允许删除文件。

(3)hostfs作为根文件系统

还可以通过hostfs从主机上的目录而不是在一个文件中的标准文件系统启动UML。最简单的方法是用loop挂接一个存在的root_fs文件,方法如下:

host# mount root_fs uml_root_dir -o loop

用户需要将/etc/fstab中的文件类型改变为"hostfs",fstab中的该行列出如下:

none / hostfs defaults 1 1

接着用户可以用chown将目录中root拥有的所有文件改变为用户拥有,方法如下:

host# find . -uid 0 -exec chown user {} \;

如果用户不想用上面的命令改变文件属主,用户可以用root身份运行UML。

接着,确保UML内核编译进hostfs,而不是以内核模块方式包含hostfs。那么,加入下面的命令行运行UML:

root=/dev/root rootflags=/path/to/uml/root rootfstype=hostfs

加入上述选项后,UML应该像正常的一样启动。

(4)编译hostfs

如果hostfs不在内核中,用户需要编译hostfs,用户可以将它编译进内核或内核模块。用户在内核配置界面上选项hostfs,并编译和安装内核。

内核调试

因为UML运行为正常的Linux进程,用户可以用gdb像调试其他进程一样调试内核,稍微不同的是:因为内核的线程已用系统调用ptrace进行拦截跟踪,因此,gdb不能ptrace它们。UML已加入了解决此问题的机制。

为了调试内核,用户需要从源代码编译,确保打开CONFIG_DEBUGSYM和CONFIG_PT_PROXY配置选项。它们分别用来确保编译内核带有"-g"选项和打开ptrace代理,以便gdb能与UML一起工作调试内核。

(1)在gdb下启动内核

用户可以在命令行中放入"debug"选项,在启动UML时将内核放在gdb的控制之下。用户可以得到一个运行gdb的xterm,内核将送一些命令到gdb,停在"start_kernel"处,用户可以输入"next", "step"或"cont"运行内核。

(2)检查睡眠的进程

并非每个bug在当前运行的进程中,有时候,当进程在信号量上或其他类似原因死锁时,原本不应该挂起的进程在内核中挂起。这种情况下,用户在gdb中用"Ctrl+C"时,得到一个跟踪栈,用户将可以看见到不相关的空闲线程。

用户本想看到的是不应该睡眠的进程的栈,为了看到睡眠的进程,用户可以在主机上用命令ps得到该进程的主机进程id。

用户将gdb与当前线程分离,方法如下:

(UML gdb) det

然后将gdb粘附到用户感兴趣的线程上,方法如下:

(UML gdb) att <host pid>

查看该线程的栈,方法如下:

(UML gdb) bt

(3)在UML上运行ddd

ddd可以工作于UML,用户可以主机上运行ddd,它给gdb提供了图形界面。运行ddd的步骤如下:

启动ddd,方法如下:

host% ddd linux

得到gdb的pid

用命令ps可以得到ddd启动的gdb的pid。

运行UML

在运行UML的命令行中加上选项"debug=parent gdb-pid=<pid>",启动并登录UML。

在ddd的gdb命令行中输入"att 1",gdb显示如下:

0xa013dc51 in __kill ()

(gdb)

在gdb中输入"c",UML将继续运行,用户可接着像调试其他进程一样调试了。

(4)调试内核模块

gdb已支持调试动态装载入进程的代码,这需要在UML下调试内核模块。调试内核模块有些复杂,用户需要告诉gdb装入UML的对象文件名以及它在内存中的位置。接着,它能读符号表,并从装载地址指出所有的符号。

当用户在rmmod内核模块后重装载它时,可得到更多信息。用户必须告诉gdb忘记所有它的符号,包括主UML的符号,接着再装载回所有的符号。

用户可以使用脚本umlgdb进行内核模块的重装载和读取它的符号表。用户还可以手动进行一步步处理完成符号表的获取工作。下面分别说明这两种方法。

1)运行脚本umlgdb调试内核模块

运行脚本umlgd较容易获取内核模块的符号表。

首先,用户应告诉内核模块所在的位置,在脚本中有一个列表类似如下:

set MODULE_PATHS {

"fat" "/usr/src/uml/linux-2.6.18/fs/fat/fat.ko"

"isofs" "/usr/src/uml/linux-2.6.18/fs/isofs/isofs.ko"

"minix" "/usr/src/uml/linux-2.6.18/fs/minix/minix.ko"

}

用户将上述列表改为将调试的内核模块的路径,接着,从UML的顶层目录运行该脚本,显示如下:

                • GDB pid is 21903 ********

Start UML as: ./linux <kernel switches> debug gdb-pid=21903

GNU gdb 5.0rh-5 Red Hat Linux 7.1

Copyright 2001 Free Software Foundation, Inc.

GDB is free software, covered by the GNU General Public License, and you are

welcome to change it and/or distribute copies of it under certain conditions.

Type "show copying" to see the conditions.

There is absolutely no warranty for GDB. Type "show warranty" for details.

This GDB was configured as "i386-redhat-linux"...

(gdb) b sys_init_module

Breakpoint 1 at 0xa0011923: file module.c, line 349.

(gdb) att 1

在用户运行UML后,用户只需要在"att 1"按回车,并继续执行它。方法如下:

Attaching to program: /home/jdike/linux/2.4/um/./linux, process 1

0xa00f4221 in __kill ()

(UML gdb) c

Continuing.

此时,当用户用insmod插入内核模块,显示列出如下:

      • Module hostfs loaded ***

Breakpoint 1, sys_init_module (name_user=0x805abb0 "hostfs",

mod_user=0x8070e00) at module.c:349

349 char *name, *n_name, *name_tmp = NULL;

(UML gdb) finish

Run till exit from #0 sys_init_module (name_user=0x805abb0 "hostfs",

mod_user=0x8070e00) at module.c:349

0xa00e2e23 in execute_syscall (r=0xa8140284) at syscall_kern.c:411

411 else res = EXECUTE_SYSCALL(syscall, regs);

Value returned is $1 = 0

(UML gdb)

p/x (int)module_list + module_list->size_of_struct

$2 = 0xa9021054

(UML gdb) symbol-file ./linux

Load new symbol table from "./linux" ' (y or n) y

Reading symbols from ./linux...

done.

(UML gdb)

add-symbol-file /home/jdike/linux/2.4/um/arch/um/fs/hostfs/hostfs.o 0xa9021054

add symbol table from file "/home/jdike/linux/2.4/um/arch/um/fs/hostfs/hostfs.o" at

.text_addr = 0xa9021054

(y or n) y

Reading symbols from /home/jdike/linux/2.4/um/arch/um/fs/hostfs/hostfs.o...

done.

(UML gdb) p *module_list

$1 = {size_of_struct = 84, next = 0xa0178720, name = 0xa9022de0 "hostfs",

size = 9016, uc = {usecount = {counter = 0}, pad = 0}, flags = 1,

nsyms = 57, ndeps = 0, syms = 0xa9023170, deps = 0x0, refs = 0x0,

init = 0xa90221f0 <init_hostfs>, cleanup = 0xa902222c <exit_hostfs>,

ex_table_start = 0x0, ex_table_end = 0x0, persist_start = 0x0,

persist_end = 0x0, can_unload = 0, runsize = 0, kallsyms_start = 0x0,

kallsyms_end = 0x0,

archdata_start = 0x1b855 <Address 0x1b855 out of bounds>,

archdata_end = 0xe5890000 <Address 0xe5890000 out of bounds>,

kernel_data = 0xf689c35d <Address 0xf689c35d out of bounds>}

>> Finished loading symbols for hostfs ...

(2)手动调试内核模块

在调试器中启动内核,并用insmod或modprobe装载内核模块。在gdb中执行下面命令:

(UML gdb) p module_list

这是已装载进内核的内核模块列表,通常用户期望的内核模块在module_lis中。如果不在,就进入下一个链接,查看name域,直到找到用户调试的内核模块。获取该结构的地址,并加上module.size_of_struct值,gdb可帮助获取该值,方法如下:

(UML gdb) printf "%#x\n", (int)module_list module_list->size_of_struct

从内核模块开始处的偏移偶尔会改变,因此,应检查init和cleanup的地址,方法如下:

(UML gdb) add-symbol-file /path/to/module/on/host that_address

如果断点不在正确的位置或不工作等 ,用户可以查看内核模块结构,init和cleanup域应该类似如下:

init = 0x588066b0 <init_hostfs>, cleanup = 0x588066c0 <exit_hostfs>

如果名字正确,但它们有偏移,那么,用户应该将偏移加到add-symbol-file所在地址上。

当用户想装载内核模块的新版本时,需要让gdb删除旧内核模块的所有符号。方法如下:

(UML gdb) symbol-file

接着,从内核二进制重装载符号,方法如下:

(UML gdb) symbol-file /path/to/kernel

然后,重复上面的装载符号过程。还需要重打开断点。

(5)粘附gdb到内核

如果用户还没有在gdb下运行内核,用户可以通过给跟踪线程发送一个SIGUSR1,用于以后粘附gdb到内核。控制台第一行的输出鉴别它的id,显示类似如下:

tracing thread pid = 20093

发送信号的方法如下:

host% kill -USR1 20093

上述命令运行后,用户将可看见带有gdb运行的xterm。

如果用户已将mconsole(UML的控制台)编译进UML,那么可用mconsole客户端启动gdb,方法如下:

(mconsole) (mconsole) config gdb=xterm

上述命令运行后,用户将可看见带有gdb运行的xterm。

(6)使用可替换的调试器

UML支持粘附到一个已运行的调试器,而不是启动gdb本身。当gdb是一些UI的子进程(如:emacs或ddd)时,这将是有用的。它还被用于在UML上运行非gdb的调试器。下面是一个使用strace作为可替代调试器的例子。

用户需要得到调试器的pid,并将pid用"gdb-pid=<pid>"选项与"debug"选项一起传递。

如果用户在UI下使用gdb,那么,应告诉UML"att 1",那么,UI将粘附到UML。

下面以替换调试器strace为例,用户可以用strace调试实际的内核,方法如下:

在shell中运行下述命令

host%

sh -c 'echo pid=$$; echo -n hit return; read x; exec strace -p 1 -o strace.out'

用"debug"和"gdb-pid=<pid>"运行UML。

strace输出将出现在输出文件中。

注意:运行下面的命令,结果不同于前面命令。

host% strace ./linux

上述命令将仅strace主UML线程,跟踪的线程不做任何实际的内核操作。它仅标识出虚拟机。而使用上述的strce将显示虚拟机低层的活动情况。

断言语句

在代码里面老能看到 BUG_ON() , WARN_ON() 这样的宏 , 类似 我们日常编程里面的断言(assert) 。

在include/asm-generic/bug.h

#ifdef CONFIG_BUG
 
#ifdef CONFIG_GENERIC_BUG
#ifndef __ASSEMBLY__
struct bug_entry {
	unsigned long	bug_addr;
#ifdef CONFIG_DEBUG_BUGVERBOSE
	const char	*file;
	unsigned short	line;
#endif
	unsigned short	flags;
};
#endif		/* __ASSEMBLY__ */
 
#define BUGFLAG_WARNING	(1<<0)
#endif	/* CONFIG_GENERIC_BUG */
 
#ifndef HAVE_ARCH_BUG
#define BUG() do { \
	printk("BUG: failure at %s:%d/%s()!\n", __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
 
#ifndef __WARN
#ifndef __ASSEMBLY__
extern void warn_on_slowpath(const char *file, const int line);
#define WANT_WARN_ON_SLOWPATH
#endif
#define __WARN() warn_on_slowpath(__FILE__, __LINE__)
#endif
 
#ifndef WARN_ON
#define WARN_ON(condition) ({						\
	int __ret_warn_on = !!(condition);				\
	if (unlikely(__ret_warn_on�					\
		__WARN();						\
	unlikely(__ret_warn_on);					\
})
#endif
 
#else /* !CONFIG_BUG */
#ifndef HAVE_ARCH_BUG
#define BUG()
#endif
 
#ifndef HAVE_ARCH_BUG_ON
#define BUG_ON(condition) do { if (condition) ; } while(0)
#endif
 
#ifndef HAVE_ARCH_WARN_ON
#define WARN_ON(condition) ({						\
	int __ret_warn_on = !!(condition);				\
	unlikely(__ret_warn_on);					\
})
#endif
#endif
 
#define WARN_ON_ONCE(condition)	({				\
	static int __warned;					\
	int __ret_warn_once = !!(condition);			\
								\
	if (unlikely(__ret_warn_once�				\
		if (WARN_ON(!__warned� 			\
			__warned = 1;				\
	unlikely(__ret_warn_once);				\
})
 
#ifdef CONFIG_SMP
# define WARN_ON_SMP(x)			WARN_ON(x)
#else
# define WARN_ON_SMP(x)			do { } while (0)
#endif

同步锁调试

锁验证器

内核锁验证器(Kernel lock validator)可以在死锁发生前检测到死锁,即使是很少发生的死锁。它将每个自旋锁与一个键值相关,相似的锁仅处理一次。加锁时,查看所有已获取的锁,并确信在其他上下文中没有已获取的锁,在新获取锁之后被获取。解锁时,确信正被解开的锁在已获取锁的顶部。

.

Validate spinlocks vs interrupts behavior.

当加锁动态发生时,锁验证器映射所有加锁规则,该检测由内核的spinlocks、rwlocks、mutexes和rwsems等锁机制触发。不管何时锁合法性检测器子系统检测到一个新加锁场景,它检查新规则是否违反正存在的规则集,如果新规则与正存在的规则集一致,则加入新规则,内核正常运行。如果新规则可能创建一个死锁场景,那么这种创建死锁的条件会被打印出来。

当判断加锁的有效性时,所有可能的"死锁场景"会被考虑到:假定任意数量的CPU、任意的中断上下文和任务上下文群、运行所有正存在的加锁场景的任意组合。在一个典型系统中,这意味着有成千上万个独立的场景。这就是为什么称它为"加锁正确性"验证器,对于所有被观察的规则来说,锁验证器用数学的确定性证明死锁不可能发生,假定锁验证器实现本身正确,并且它内部的数据结构不会被其他内核子系统弄坏。

还有,验证器的属性"所有可能的场景"也使查找变得复杂,特别是多CPU、多上下文竞争比单个上下文规则复杂得多,

为了增加验证器的效率,不是将每个锁实例进行映射,而是映射每个锁类型。例如:内核中所有的结构inode对象有inode->inotify_mutex,如果缓存了10000个inode,将会有10000个锁对象。但->inotify_mutex是单个锁类型,所有->inotify_mutex发生的加锁活动都归入单个锁类型。

Lock-class


验证器操作的基本对象是锁类Lock-class,一个锁类是一组锁,逻辑上有同样的加锁规则,尽管锁可能有多个实例。例如:在结构inode中的一个锁是一个类,而每个节点有它自己的锁类实例。

验证器跟踪锁类的状态和不同锁类之间的依赖性。验证器维护一个有关状态和依赖性是否正确的滚动证据。

不像一个锁实例,锁类lock-class它本身从不消失:当lock-class注册使用后,所有随后锁类的使用都会被附加到该lock-class上。

相关文章: