目录[-]
一 调试前的准备
在调试一个bug之前,我们所要做的准备工作有:- 有一个被确认的bug。
- 包含这个bug的内核版本号,需要分析出这个bug在哪一个版本被引入,这个对于解决问题有极大的帮助。可以采用二分查找法来逐步锁定bug引入版本号。
- 对内核代码理解越深刻越好,同时还需要一点点运气。
- 该bug可以复现。如果能够找到复现规律,那么离找到问题的原因就不远了。
- 最小化系统。把可能产生bug的因素逐一排除掉。
二 内核中的bug
内核中的bug也是多种多样的。它们的产生有无数的原因,同时表象也变化多端。从隐藏在源代码中的错误到展现在目击者面前的bug,其发作往往是一系列连锁反应的事件才可能出发的。虽然内核调试有一定的困难,但是通过你的努力和理解,说不定你会喜欢上这样的挑战。三 内核调试配置选项
学习编写驱动程序要构建安装自己的内核(标准主线内核)。最重要的原因之一是:内核开发者已经建立了多项用于调试的功能。但是由于这些功能会造成额外的输出,并导致能下降,因此发行版厂商通常会禁止发行版内核中的调试功能。1 内核配置
为了实现内核调试,在内核配置上增加了几项:
Kernel hacking ---> [*] Magic SysRq key [*] Kernel debugging [*] Debug slab memory allocations [*] Spinlock and rw-lock 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
启用选项例如:
slab layer debugging(slab层调试选项) high-memory debugging(高端内存调试选项) I/O mapping debugging(I/O映射调试选项) spin-lock debugging(自旋锁调试选项) stack-overflow checking(栈溢出检查选项) sleep-inside-spinlock checking(自旋锁内睡眠选项)
2 调试原子操作
从内核2.5开发,为了检查各类由原子操作引发的问题,内核提供了极佳的工具。 内核提供了一个原子操作计数器,它可以配置成,一旦在原子操作过程中,进城进入睡眠或者做了一些可能引起睡眠的操作,就打印警告信息并提供追踪线索。 所以,包括在使用锁的时候调用schedule(),正使用锁的时候以阻塞方式请求分配内存等,各种潜在的bug都能够被探测到。 下面这些选项可以最大限度地利用该特性:CONFIG_PREEMPT = y CONFIG_DEBUG_KERNEL = y CONFIG_KLLSYMS = y CONFIG_SPINLOCK_SLEEP = y
四 引发bug并打印信息
1 BUG()和BUG_ON()
一些内核调用可以用来方便标记bug,提供断言并输出信息。最常用的两个是BUG()和BUG_ON()。
定义在<include/asm-generic>中:
#ifndef HAVE_ARCH_BUG #define BUG() do { printk("BUG: failure at %s:%d/%s()! ", __FILE__, __LINE__, __FUNCTION__); panic("BUG!"); /* 引发更严重的错误,不但打印错误消息,而且整个系统业会挂起 */ } while (0) #endif #ifndef HAVE_ARCH_BUG_ON #define BUG_ON(condition) do { if (unlikely(condition)) BUG(); } while(0) #endif
#ifndef __WARN_TAINT#ifndef __ASSEMBLY__extern void warn_slowpath_fmt(const char *file, const int line, const char *fmt, ...) __attribute__((format(printf, 3, 4)));extern void warn_slowpath_fmt_taint(const char *file, const int line, unsigned taint, const char *fmt, ...) __attribute__((format(printf, 4, 5)));extern void warn_slowpath_null(const char *file, const int line);#define WANT_WARN_ON_SLOWPATH#endif#define __WARN() warn_slowpath_null(__FILE__, __LINE__)#define __WARN_printf(arg...) warn_slowpath_fmt(__FILE__, __LINE__, arg)#define __WARN_printf_taint(taint, arg...) \ warn_slowpath_fmt_taint(__FILE__, __LINE__, taint, arg)#else#define __WARN() __WARN_TAINT(TAINT_WARN)#define __WARN_printf(arg...) do { printk(arg); __WARN(); } while (0)#define __WARN_printf_taint(taint, arg...) \ do { printk(arg); __WARN_TAINT(taint); } while (0)#endif#ifndef WARN_ON#define WARN_ON(condition) ({ \ int __ret_warn_on = !!(condition); \ if (unlikely(__ret_warn_on)) \ __WARN(); \ unlikely(__ret_warn_on); \})#endif#ifndef WARN#define WARN(condition, format...) ({ \ int __ret_warn_on = !!(condition); \ if (unlikely(__ret_warn_on)) \ __WARN_printf(format); \ unlikely(__ret_warn_on); \})#endif
3 dump_stack()
有些时候,只需要在终端上打印一下栈的回溯信息来帮助你调试。这时可以使用dump_stack()。这个函数只在终端上打印寄存器上下文和函数的跟踪线索。if (!debug_check) { printk(KERN_DEBUG “provide some information…/n”); dump_stack(); }
五 printk()
内核提供的格式化打印函数。1 printk函数的健壮性
健壮性是printk最容易被接受的一个特质,几乎在任何地方,任何时候内核都可以调用它(中断上下文、进程上下文、持有锁时、多处理器处理时等)。2 printk函数脆弱之处
在系统启动过程中,终端初始化之前,在某些地方是不能调用的。如果真的需要调试系统启动过程最开始的地方,有以下方法可以使用:- 使用串口调试,将调试信息输出到其他终端设备。
- 使用early_printk(),该函数在系统启动初期就有打印能力。但它只支持部分硬件体系。
3 LOG等级
printk和printf一个主要的区别就是前者可以指定一个LOG等级。内核根据这个等级来判断是否在终端上打印消息。内核把比指定等级高的所有消息显示在终端。 可以使用下面的方式指定一个LOG级别: printk(KERN_CRIT “Hello, world!\n”); 注意,第一个参数并不一个真正的参数,因为其中没有用于分隔级别(KERN_CRIT)和格式字符的逗号(,)。KERN_CRIT本身只是一个普通的字符串(事实上,它表示的是字符串 "<2>";表 1 列出了完整的日志级别清单)。作为预处理程序的一部分,C 会自动地使用一个名为 字符串串联 的功能将这两个字符串组合在一起。组合的结果是将日志级别和用户指定的格式字符串包含在一个字符串中。 内核使用这个指定LOG级别与当前终端LOG等级console_loglevel来决定是不是向终端打印。 下面是可使用的LOG等级:#define KERN_EMERG "<0>" /* system is unusable */#define KERN_ALERT "<1>" /* action must be taken immediately */ #define KERN_CRIT "<2>" /* critical conditions */#define KERN_ERR "<3>" /* error conditions */#define KERN_WARNING "<4>" /* warning conditions */#define KERN_NOTICE "<5>" /* normal but significant condition */#define KERN_INFO "<6>" /* informational */#define KERN_DEBUG "<7>" /* debug-level messages */#define KERN_DEFAULT "" /* Use the default kernel loglevel */
mtj@ubuntu :~$ cat /proc/sys/kernel/printk4 4 1 7mtj@ubuntu :~$ cat /proc/sys/kernel/printk_delay0mtj@ubuntu :~$ cat /proc/sys/kernel/printk_ratelimit5mtj@ubuntu :~$ cat /proc/sys/kernel/printk_ratelimit_burst10
4 记录缓冲区
内核消息都被保存在一个LOG_BUF_LEN大小的环形队列中。 关于LOG_BUF_LEN定义:#define __LOG_BUF_LEN (1 << CONFIG_LOG_BUF_SHIFT)
CONFIG_LOG_BUF_SHIFT=18
※ 这个纪录缓冲区之所以称为环形,是因为它的读写都是按照环形队列的方式进行操作的。
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中。nullarbor:~ # echo 'file svcsock.c line 1603 +p' >/dynamic_debug/control
六 内存调试工具
1 MEMWATCH
MEMWATCH 由 Johan Lindh 编写,是一个开放源代码 C 语言内存错误检测工具,您可以自己下载它。只要在代码中添加一个头文件并在 gcc 语句中定义了 MEMWATCH 之后,您就可以跟踪程序中的内存泄漏和错误了。MEMWATCH 支持ANSIC,它提供结果日志纪录,能检测双重释放(double-free)、错误释放(erroneous free)、没有释放的内存(unfreedmemory)、溢出和下溢等等。 清单 1. 内存样本(test1.c)#include#include #include "memwatch.h"int main(void){ char *ptr1; char *ptr2; ptr1 = malloc(512); ptr2 = malloc(512); ptr2 = ptr1; free(ptr2); free(ptr1);}
gcc -DMEMWATCH -DMW_STDIO test1.c memwatchc -o test1
清单 2. test1 memwatch.log 文件
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
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 的 test1gcc -g test1.c -o test1
YAMD version 0.32Executable: /usr/src/test/yamd-0.32/test1...INFO: Normal allocation of this blockAddress 0x40025e00, size 512...INFO: Normal allocation of this blockAddress 0x40028e00, size 512...INFO: Normal deallocation of this blockAddress 0x40025e00, size 512...ERROR: Multiple freeing Atfree of pointer already freedAddress 0x40025e00, size 512...WARNING: Memory leakAddress 0x40028e00, size 512WARNING: Total memory leaks:1 unfreed allocations totaling 512 bytes*** Finished at Tue ... 10:07:15 2002Allocated a grand total of 1024 bytes 2 allocationsAverage of 512 bytes per allocationMax bytes allocated at one time: 102424 K alloced internally / 12 K mapped now / 8 K maxVirtual program size is 1416 KEnd.
#include#include int main(void){ char *ptr1; char *ptr2; char *chptr; int i = 1; ptr1 = malloc(512); ptr2 = malloc(512); chptr = (char *)malloc(512); for (i; i <= 512; i++) { chptr[i] = 'S'; } ptr2 = ptr1; free(ptr2); free(ptr1); free(chptr);}
./run-yamd /usr/src/test/test2/test2
Running /usr/src/test/test2/test2Temp output to /tmp/yamd-out.1243*********./run-yamd: line 101: 1248 Segmentation fault (core dumped)YAMD version 0.32Starting run: /usr/src/test/test2/test2Executable: /usr/src/test/test2/test2Virtual program size is 1380 K...INFO: Normal allocation of this blockAddress 0x40025e00, size 512...INFO: Normal allocation of this blockAddress 0x40028e00, size 512...INFO: Normal allocation of this blockAddress 0x4002be00, size 512ERROR: Crash...Tried to write address 0x4002c000Seems 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.
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 的开头部分execve("/sbin/mkfs.jfs", ["mkfs.jfs", "-f", "/dev/test1"], &...open("/dev/test1", O_RDWR|O_LARGEFILE) = 4stat64("/dev/test1", {st_mode=&, st_rdev=makedev(63, 255), ...}) = 0ioctl(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 ) = 98stat64("/dev/test1", {st_mode=&, st_rdev=makedev(63, 255), ...}) = 0open("/dev/test1", O_RDONLY|O_LARGEFILE) = 5ioctl(5, 0x80041272, 0xbfffe124) = -1 EINVAL (Invalid argument)write(2, "mkfs.jfs: can\'t determine device"..., ..._exit(1) = ?
八 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 消息ksymoops 2.4.0 on i686 2.4.17. Options used... 15:59:37 sfb1 kernel: Unable to handle kernel NULL pointer dereference atvirtual address 0000000... 15:59:37 sfb1 kernel: c01588fc... 15:59:37 sfb1 kernel: *pde = 0000000... 15:59:37 sfb1 kernel: Oops: 0000... 15:59:37 sfb1 kernel: CPU: 0... 15:59:37 sfb1 kernel: EIP: 0010:[jfs_mount+60/704]... 15:59:37 sfb1 kernel: Call Trace: [jfs_read_super+287/688] [get_sb_bdev+563/736] [do_kern_mount+189/336] [do_add_mount+35/208][do_page_fault+0/1264]... 15:59:37 sfb1 kernel: Call Trace: []...... 15:59:37 sfb1 kernel: [ >EIP; c01588fc <=====...Trace; c0106cf3 Code; c01588fc 00000000 <_EIP>:Code; c01588fc <===== 0: 8b 2d 00 00 00 00 mov 0x0,%ebp <=====Code; c0158902 6: 55 push %ebp
109 printk("%d\n",*ptr);objdump jfs_mount.ojfs_mount.o: file format elf32-i386Disassembly of section .text:00000000: 0:55 push %ebp ... 2c: e8 cf 03 00 00 call 400 31: 89 c3 mov %eax,%ebx 33: 58 pop %eax 34: 85 db test %ebx,%ebx 36: 0f 85 55 02 00 00 jne 291 3c: 8b 2d 00 00 00 00 mov 0x0,%ebp << problem line above 42: 55 push %ebp
2 kallsyms
开发版2.5内核引入了kallsyms特性,它可以通过定义CONFIG_KALLSYMS编译选项启用。该选项可以载入内核镜像所对应的内存地址的符号名称(即函数名),所以内核可以打印解码之后的跟踪线索。相应,解码OOPS也不再需要System.map和ksymoops工具了。另外, 这样做,会使内核变大些,因为地址对应符号名称必须始终驻留在内核所在内存上。#cat /proc/kallsyms c0100240 T _stext c0100240 t run_init_process c0100240 T stext c0100269 t init …
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 执行流程
图 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
1) 修改内核引导参数,为启动捕获内核预留内存
通过下面的方法来配置 kdump 使用的内存大小。添加启动参数"crashkernel=Y@X",这里,Y 是为 kdump 捕捉内核保留的内存,X 是保留部分内存的开始位置。
- 对于 i386 和 x86_64, 编辑 /etc/grub.conf, 在内核行的最后添加"crashkernel=128M" 。
- 对于 ppc64,在 /etc/yaboot.conf 最后添加"crashkernel=128M"。
在 ia64, 编辑 /etc/elilo.conf,添加"crashkernel=256M"到内核行。
2) kdump 配置文件
kdump 的配置文件是 /etc/kdump.conf(RHEL6.2);/etc/sysconfig/kdump(SLES11 sp2)。每个文件头部都有选项说明,可以根据使用需求设置相应的选项。
3.3.3 启动 kdump 服务
在设置了预留内存后,需要重启机器,否则 kdump 是不可使用的。启动 kdump 服务:
Rhel6.2:
# chkconfig kdump on # service kdump status Kdump is operational # service kdump start
SLES11SP2:
# chkconfig boot.kdump on # service boot.kdump start
3.3.4 测试配置是否有效
可以通过 kexec 加载内核镜像,让系统准备好去捕获一个崩溃时产生的 vmcore。可以通过 sysrq 强制系统崩溃。
# echo c > /proc/sysrq-trigger
这造成内核崩溃,如配置有效,系统将重启进入 kdump 内核,当系统进程进入到启动 kdump 服务的点时,vmcore 将会拷贝到你在 kdump 配置文件中设置的位置。RHEL 的缺省目录是 : /var/crash;SLES 的缺省目录是 : /var/log/dump。然后系统重启进入到正常的内核。一旦回复到正常的内核,就可以在上述的目录下发现 vmcore 文件,即内存转储文件。可以使用之前安装的 kernel-debuginfo 中的 crash 工具来进行分析(crash 的更多详细用法将在本系列后面的文章中有介绍)。
# crash /usr/lib/debug/lib/modules/2.6.17-1.2621.el5/vmlinux /var/crash/2006-08-23-15:34/vmcore crash> bt
3.4 载入“转储捕获”内核
需要引导系统内核时,可使用如下步骤和命令载入“转储捕获”内核:
kexec -p\ --initrd= --args-linux \ --append="root= init 1 irqpoll"
装载转储捕捉内核的注意事项:
- 转储捕捉内核应当是一个 vmlinux 格式的映像(即是一个未压缩的 ELF 映像文件),而不能是 bzImage 格式;
- 默认情况下,ELF 文件头采用 ELF64 格式存储以支持那些拥有超过 4GB 内存的系统。但是可以指定“--elf32-core-headers”标志以强制使用 ELF32 格式的 ELF 文件头。这个标志是有必要注意的,一个重要的原因就是:当前版本的 GDB 不能在一个 32 位系统上打开一个使用 ELF64 格式的 vmcore 文件。ELF32 格式的文件头不能使用在一个“没有物理地址扩展”(non-PAE)的系统上(即:少于 4GB 内存的系统);
- 一个“irqpoll”的启动参数可以减低由于在“转储捕获内核”中使用了“共享中断”技术而导致出现驱动初始化失败这种情况发生的概率 ;
- 必须指定 <root-dev>,指定的格式是和要使用根设备的名字。具体可以查看 mount 命令的输出;“init 1”这个命令将启动“转储捕捉内核”到一个没有网络支持的单用户模式。如果你希望有网络支持,那么使用“init 3”。
3.5 后记
Kdump 是一个强大的、灵活的内核转储机制,能够在生产内核上下文中执行捕获内核是非常有价值的。本文仅介绍在 RHEL6.2 和 SLES11 中如何配置 kdump。望抛砖引玉,对阅读本文的读者有益。
参考:
1 2九 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的控制权重新交还给内核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机上执行:stty ispeed 115200 ospeed 115200 -F /dev/ttyS0
stty ispeed 115200 ospeed 115200 -F /dev/ttyS0
echo hello > /dev/ttyS0
cat /dev/ttyS0
2.2 安装与配置
下面我们需要应用kgdb补丁到Linux内核,设置内核选项并编译内核。这方面的资料相对较少,笔者这里给出详细的介绍。下面的工作在开发机(developement)上进行,以上面介绍的试验环境为例,某些具体步骤在实际的环境中可能要做适当的改动:I、内核的配置与编译
[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
[root@lisl tmp]#patch -p1 <../linux-2.6.7-kgdb-2.2/core-lite.patch
[root@lisl tmp]#make menuconfig
[*] 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
[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
[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
title 2.6.7 kgdbroot (hd0,0)kernel /boot/vmlinuz-2.6.7-kgdb ro root=/dev/hda1 kgdbwait kgdb8250=1,115200
image=/boot/vmlinuz-2.6.7-kgdblabel=kgdb read-only root=/dev/hda3append="gdb gdbttyS=1 gdbbaud=115200"
gdbfile vmlinuxset remotebaud 115200target remote /dev/ttyS0
[root@lisl boot]#gdbstart -s 115200 -t /dev/ttyS0
[*]KGDB: kernel debugging with remote gdbMethod for KGDB communication (KGDB: On ethernet) ---> ( ) KGDB: On generic serial port (8250)(X) KGDB: On ethernet
title 2.6.7 kgdbroot (hd0,0)kernel /boot/vmlinuz-2.6.7-kgdb ro root=/dev/hda1 kgdbwait kgdboe=@192.168.5.13/,@192.168. 6.13/
[root@lisl tmp]# insmod -m hello.ko >modaddr
.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……
(gdb) Add-symbol-file hello.o 0xc88d8060 -s .data 0xc88d80a0 -s .rodata 0xc88d80a0 -s .bss 0x c88d833c
…… ……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(只读段):该段保存着只读数据,在进程映象中构造不可写的段。 通过在模块初始化函数中放置一下代码,我们可以很容易地获得模块加载到内存中的地址。……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);
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是一个不错的内核调试工具,使用它可以进行对内核的全面调试,甚至可以调试内核的中断处理程序。如果在一些图形化的开发工具的帮助下,对内核的调试将更方便。 参考:十 使用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用户来执行。[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
[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
[root@lisl uClinux-dist]# gunzip linux-2.6.9-hsc0.patch.gz [root@lisl uClinux-dist]patch -p0 < linux-2.6.9-hsc0.patch
[root@lisl uClinux-dist]# rm -rf linux-2.6.x/[root@lisl uClinux-dist]# mv linux-2.6.9 linux-2.6.x
[root@lisl uClinux-dist]# cd linux-2.6.x[root@lisl linux-2.6.x]#make ARCH=armnommu CROSS_COMPILE=arm-uclinux- atmel_deconfig
[root@lisl linux-2.6.x]#make ARCH=armnommu CROSS_COMPILE=arm-uclinux- oldconfig
[root@lisl linux-2.6.x]# make ARCH=armnommu CROSS_COMPILE=arm-uclinux- v=1
CFLAGS += -g
Export PATH=$PATH:/root/bin/arm-linux-tool/bin
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结合,有了图形界面,能为调试和查看源码提供一些方便。 参考:十一 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 压缩的文件解压缩补丁文件:#bzip2 -d kdb-v4.2-2.4.20-common-1.bz2#bzip2 -d kdb-v4.2-2.4.20-i386-1.bz2
#patch -p1
2 初始化并设置环境变量
您可以定义将在 KDB 初始化期间执行的 KDB 命令。需要在纯文本文件 kdb_cmds 中定义这些命令,该文件位于 Linux 源代码树(当然是在打了补丁之后)的 KDB 目录中。该文件还可以用来定义设置显示和打印选项的环境变量。文件开头的注释提供了编辑文件方面的帮助。使用这个文件的缺点是,在您更改了文件之后需要重新构建并重新安装内核。3 激活 KDB
如果编译期间没有选中 CONFIG_KDB_OFF ,那么在缺省情况下 KDB 是活动的。否则,您需要显式地激活它 - 通过在引导期间将kdb=on 标志传递给内核或者通过在挂装了 /proc 之后执行该工作:#echo "1" >/proc/sys/kernel/kdb
#echo "0" >/proc/sys/kernel/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 行内存:[0]kdb> md 0xc000000 15
[0]kdb> mm 0xc000000 0x10
[0]kdb> rd[0]kdb> rm %ebx 0x25
[0]kdb> bp sys_write
[0]kdb> bl
[0]kdb> bc 1
- D:不可中断状态
- R:正运行
- S:可中断休眠
- T:已跟踪或已停止
- Z:僵死
- U:不可运行
[0]kdb> bt
[0]kdb> btp 575
[0]kdb> id schedule
[0]kdb> ssb0xc0105355 default_idle+0x25: cli0xc0105356 default_idle+0x26: mov 0x14(%edx),%eax0xc0105359 default_idle+0x29: test %eax, %eax0xc010535b default_idle+0x2b: jne 0xc0105361 default_idle+0x31
5 技巧和诀窍
调试一个问题涉及到:使用调试器(或任何其它工具)找到问题的根源以及使用源代码来跟踪导致问题的根源。单单使用源代码来确定问题是极其困难的,只有老练的内核黑客才有可能做得到。相反,大多数的新手往往要过多地依靠调试器来修正错误。这种方法可能会产生不正确的问题解决方案。我们担心的是这种方法只会修正表面症状而不能解决真正的问题。此类错误的典型示例是添加错误处理代码以处理 NULL 指针或错误的引用,却没有查出无效引用的真正原因。 结合研究代码和使用调试工具这两种方法是识别和修正问题的最佳方案。 调试器的主要用途是找到错误的位置、确认症状(在某些情况下还有起因)、确定变量的值,以及确定程序是如何出现这种情况的(即,建立调用堆栈)。有经验的黑客会知道对于某种特定的问题应使用哪一个调试器,并且能迅速地根据调试获取必要的信息,然后继续分析代码以识别起因。 因此,这里为您介绍了一些技巧,以便您能使用 KDB 快速地取得上述结果。当然,要记住,调试的速度和精确度来自经验、实践和良好的系统知识(硬件和内核内部机理等)。 5.1 技巧 #1 在 KDB 中,在提示处输入地址将返回与之最为匹配的符号。这在堆栈分析以及确定全局数据的地址/值和函数地址方面极其有用。同样,输入符号名则返回其虚拟地址。 示例 表明函数 sys_read 从地址 0xc013db4c 开始:[0]kdb> 0xc013db4c0xc013db4c = 0xc013db4c (sys_read)
[0]kdb> sys_writesys_write = 0xc013dcc8 (sys_write)
[0]kdb> md %ebp0xc74c9f38 c74c9f60 c0136c40 000001f0 000000000xc74c9f48 08053328 c0425238 c04253a8 000000000xc74c9f58 000001f0 00000246 c74c9f6c c0136a250xc74c9f68 c74c8000 c74c9f74 c0136d6d c74c9fbc0xc74c9f78 c014fe45 c74c8000 00000000 08053328[0]kdb> 0xc0136c400xc0136c40 = 0xc0136c40 (__alloc_pages +0x44)[0]kdb> 0xc0136a250xc0136a25 = 0xc0136a25 (_alloc_pages +0x19)[0]kdb> 0xc0136d6d0xc0136d6d = 0xc0136d6d (__get_free_pages +0xd)
[0]kdb> defcmd name "usage" "help"[0]kdb> [defcmd] type the commands here[0]kdb> [defcmd] endefcmd
[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
[0]kdb> hari[hari]kdb> md 0xc000000 10xc000000 00000001 f000e816 f000e2c3 f000e816[hari]kdb> rdeax = 0x00000000 ebx = 0xc0105330 ecx = 0xc0466000 edx = 0xc0466000.......[hari]kdb> md %ebp 10xc0467fbc c0467fd0 c01053d2 00000002 000a0200[0]kdb>
[0]kdb> bph 0xc0204060 dataw 4
[0]kdb> bph 0xc000000 datar 2
6 结束语
对于执行内核调试,KDB 是一个方便的且功能强大的工具。它提供了各种选项,并且使我们能够分析内存内容和数据结构。最妙的是,它不需要用另一台机器来执行调试。 参考:十二 Kprobes
Kprobes 是 Linux 中的一个简单的轻量级装置,让您可以将断点插入到正在运行的内核之中。 Kprobes 提供了一个强行进入任何内核例程并从中断处理器无干扰地收集信息的接口。使用 Kprobes 可以 轻松地收集处理器寄存器和全局数据结构等调试信息。开发者甚至可以使用 Kprobes 来修改 寄存器值和全局数据结构的值。 为完成这一任务,Kprobes 向运行的内核中给定地址写入断点指令,插入一个探测器。 执行被探测的指令会导致断点错误。Kprobes 钩住(hook in)断点处理器并收集调试信息。Kprobes 甚至可以单步执行被探测的指令。1 安装
要安装 Kprobes,需要从 Kprobes 主页下载最新的补丁。 打包的文件名称类似于 kprobes-2.6.8-rc1.tar.gz。解开补丁并将其安装到 Linux 内核:$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
$patch -p1 < ../kprobes-2.6.8-rc1-sysrq.patch
2 编写 Kprobes 模块
对于每一个探测器,您都要分配一个结构体 struct kprobe kp; (参考 include/linux/kprobes.h 以获得关于此数据结构的详细信息)。 清单 9. 定义 pre、post 和 fault 处理器/* 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;}
- 从 System.map 文件直接得到地址。
- 例如,要得到 do_fork 的地址,可以在命令行执行 $grep do_fork /usr/src/linux/System.map 。
- 使用 nm 命令。
- $nm vmlinuz |grep do_fork
- 从 /proc/kallsyms 文件获得地址。
- $cat /proc/kallsyms |grep do_fork
- 使用 kallsyms_lookup_name() 例程。
- 这个例程是在 kernel/kallsyms.c 文件中定义的,要使用它,必须启用 CONFIG_KALLSYMS 编译内核。kallsyms_lookup_name() 接受一个字符串格式内核例程名, 返回那个内核例程的地址。例如:kallsyms_lookup_name("do_fork");
/* 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 addressfor 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);
$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
$objdump -D /usr/src/linux/kernel/fork.o > fork.dis
000022b0: 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 22d6: b8 00 e0 ff ff mov $0xffffe000,%eax 22db: 21 e0 and %esp,%eax 22dd: 8b 00 mov (%eax),%eax
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);}
$tail -10 /var/log/messagesJun 22 18:14:25 llm05 kernel: thread-info element status=0,flags=0, cpu=1Jun 22 18:14:25 llm05 kernel: pid =5e4 task-info_ptr=f5948000Jun 22 18:14:25 llm05 kernel: thread-info element status=0,flags=8, cpu=0Jun 22 18:14:25 llm05 kernel: pid =5e5 task-info_ptr=f5eca000
$echo 1 > /proc/sys/kernel/sysrq
Jun 23 10:24:48 linux-udp4749545uds kernel: SysRq : Show kprobesJun 23 10:24:48 linux-udp4749545uds kernel:Jun 23 10:24:48 linux-udp4749545uds kernel: [] do_fork+0x0/0x1de