Linux内核调试方法总结

2019年11月05日 阅读数:882
这篇文章主要向大家介绍Linux内核调试方法总结,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。
Linux内核调试方法总结

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


一  调试前的准备

在调试一个bug以前,咱们所要作的准备工做有: 
html

  • 有一个被确认的bug。前端

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

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

  • 该bug能够复现。若是可以找到复现规律,那么离找到问题的缘由就不远了。linux

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

 


二  内核中的bug

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


三  内核调试配置选项

学习编写驱动程序要构建安装本身的内核(标准主线内核)。最重要的缘由之一是:内核开发者已经创建了多项用于调试的功能。可是因为这些功能会形成额外的输出,并致使能降低,所以发行版厂商一般会禁止发行版内核中的调试功能。 

程序员

1  内核配置

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

?
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文件来进行配置,能够选择其余的输出文件。


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和日志系统的整体结构


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

图 1. RHEL6.2 执行流程 

                     图 1. RHEL6.2 执行流程

图 2. sles11 执行流程 

                                      图 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发布支持i38六、x86_6四、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的控制权从新交还给内核  

2  Kgdb的安装与设置

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


2.1  软硬件准备

如下软硬件配置取自笔者进行试验的系统配置状况: 
kgdb补丁的版本遵循以下命名模式:Linux-A-kgdb-B,其中A表示Linux的内核版本号,B为kgdb的版本号。以试验使用的kgdb补丁为例,linux内核的版本为linux-2.6.7,补丁版本为kgdb-2.2。 
物理链接好串口线后,使用如下命令来测试两台机器之间串口链接状况,stty命令能够对串口参数进行设置: 
在development机上执行: 

?
1
stty ispeed 115200 ospeed 115200 -F  /dev/ttyS0

 

在target机上执行: 

?
1
stty ispeed 115200 ospeed 115200 -F  /dev/ttyS0

 

在developement机上执行: 

?
1
echo  hello >  /dev/ttyS0

 

在target机上执行: 

?
1
cat  /dev/ttyS0

 

若是串口链接没问题的话在将在target机的屏幕上显示"hello"。 


2.2  安装与配置

下面咱们须要应用kgdb补丁到Linux内核,设置内核选项并编译内核。这方面的资料相对较少,笔者这里给出详细的介绍。下面的工做在开发机(developement)上进行,以上面介绍的试验环境为例,某些具体步骤在实际的环境中可能要作适当的改动: 


I、内核的配置与编译

 

?
1
2
3
[root@lisl tmp] # tar -jxvf linux-2.6.7.tar.bz2
[root@lisl tmp] #tar -jxvf linux-2.6.7-kgdb-2.2.tar.tar
[root@lisl tmp] #cd inux-2.6.7

 

请参照目录补丁包中文件README给出的说明,执行对应体系结构的补丁程序。因为试验在i386体系结构上完成,因此只须要安装一下补丁:core-lite.patch、i386-lite.patch、8250.patch、eth.patch、core.patch、i386.patch。应用补丁文件时,请遵循kgdb软件包内series文件所指定的顺序,不然可能会带来预想不到的问题。eth.patch文件是选择以太网口做为调试的链接端口时须要运用的补丁。 
应用补丁的命令以下所示: 

?
1
[root@lisl tmp] #patch -p1 <../linux-2.6.7-kgdb-2.2/core-lite.patch

 

若是内核正确,那么应用补丁时应该不会出现任何问题(不会产生*.rej文件)。为Linux内核添加了补丁以后,须要进行内核的配置。内核的配置能够按照你的习惯选择配置Linux内核的任意一种方式。 

?
1
[root@lisl tmp] #make menuconfig

 

在内核配置菜单的Kernel hacking选项中选择kgdb调试项,例如: 

?
1
2
3
4
5
[*] KGDB: kernel debugging with remote  gdb
       Method  for  KGDB communication (KGDB: On generic serial port (8250))  --->  
  [*] KGDB: Thread analysis 
  [*] KGDB: Console messages through  gdb
[root@lisl tmp] #make

 

编译内核以前请注意Linux目录下Makefile中的优化选项,默认的Linux内核的编译都以-O2的优化级别进行。在这个优化级别之下,编译器要对内核中的某些代码的执行顺序进行改动,因此在调试时会出现程序运行与代码顺序不一致的状况。能够把Makefile中的-O2选项改成-O,但不可去掉-O,不然编译会出问题。为了使编译后的内核带有调试信息,注意在编译内核的时候须要加上-g选项。 
不过,当选择"Kernel debugging->Compile the kernel with debug info"选项后配置系统将自动打开调试选项。另外,选择"kernel debugging with remote gdb"后,配置系统将自动打开"Compile the kernel with debug info"选项。 
内核编译完成后,使用scp命令进行将相关文件拷贝到target机上(固然也能够使用其它的网络工具,如rcp)。 

?
1
2
[root@lisl tmp] #scp arch/i386/boot/bzImage 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内核引导参数的说明: 
 
如表中所述,在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个硬件断点,这些宏的用法以下: 
 
在有些状况下,硬件断点的使用对于内核的调试是很是方便的。 

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进行调试的硬件要求,也简化了创建调试环境的过程。 
 


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调试器等,以方便内核的调试工做。 
 


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