Chinaunix首页 | 论坛 | 博客
  • 博客访问: 612531
  • 博文数量: 90
  • 博客积分: 5111
  • 博客等级: 大校
  • 技术积分: 928
  • 用 户 组: 普通用户
  • 注册时间: 2007-11-29 16:56
文章存档

2011年(15)

2010年(34)

2009年(19)

2008年(22)

我的朋友

分类: LINUX

2011-01-10 23:23:03

Linux 内核内存检测工具 - Kmemcheck

Linux 内核内存检测工具 - Kmemcheck

本文是 Linux 内核内存检测工具系列中的一篇,主要分析了 Kmemcheck 的原理,配置以及它的典型应用。读者在阅读完本文之后,能轻松地学会怎样利用 Kmemcheck 来对内核程序进行检测,同时也能从 Kmemcheck 的设计原理中得到一些启发。

前言

访问非法的内存地址(如访问未初始化的内存,访问已经释放的内存)是一件很危险的事情,如果在内核程序中使用了非法内存中的内容,可能会导致系统崩溃,如何 发现并消灭这些潜在的风险,是在编写程序时都必须考虑的问题。在 Linux 系统中,gcc 会在编译的时候对内存未初始化的情况发出警告,但是它只能做一些静态的检查;另外如果系统安装了 Valgrind,也可以利用其提供的 memcheck 来动态地对内存进行检查,但是它只能检查出一些用户态程序的问题,对工作在内核态的程序无能为力。因此,从事内核开发(如设备驱动程序)工作的时候,我们 迫切需要一个能为内核程序提供动态内存检查的工具,所幸的是,在 Linux 2.6.31 的内核版本中,它提供了一个这样的内存检测功能 - Kmemcheck, 目前该功能只支持 x86 平台。


工作原理

Kmemcheck 工作在内核态,它使用了四个宏定义来标识内存的状态(以字节为单位来标识):

KMEMCHECK_SHADOW_UNALLOCATED

在 slab cache 中,如果没有设置构造函数,那么新分配的 slab 页面在还没有分配 object 之前,它都会被设置成此状态。

KMEMCHECK_SHADOW_UNINITIALIZED

一般情况下(不包含分配标志中置位了 __GFP_ZERO),新分配的页面都会被设置成这个状态。

KMEMCHECK_SHADOW_FREED

在 slab cache 中,当某一个 object 所占有的内存被释放后,该内存块会被设置成此状态。

KMEMCHECK_SHADOW_INITIALIZED

标识当前内存处于初始化状态(即对它的访问是正确的)

在以上四种内存状态中,对前三种状态的内存的访问都是非法的,kmemcheck 会给出相应的警告(本文中的访问都是指读操作,因为写入操作被认为是在初始化内存)。

为了使 kmemcheck 能够有效的工作,内核中修改(或新增)了一些数据结构,比较重要的有:

1. 在 struct page 中增加了一个 shadow 的指针,它指向该数据页面所对应的影子页面 ( 接下来会介绍),在影子页面中记录了数据页面中每个字节的状态。


清单 1. page 结构定义
在 include/linux/page.h 中:
 struct page { 
 ... 
 #ifdef CONFIG_KMEMCHECK       ( 宏定义在下文会介绍)
 /* 
 * kmemcheck wants to track the status of each byte in a page; this 
 * is a pointer to such a status block. NULL if not tracked. 
 */ 
 void *shadow; 
 #endif 
 ... 
 } 
2. 在页表项的页面属性域中新定义了一个 _PAGE_HIDDEN 的标志位,如果为 1,则说明该页面被 kmemcheck 跟踪。


清单 2. 页面属性定义
在 arch/x86/include/asm/pgtable_types.h 中:
 #define _PAGE_BIT_HIDDEN 11 /* hidden by kmemcheck */ 
 #ifdef CONFIG_KMEMCHECK 
 #define _PAGE_HIDDEN (_AT(pteval_t, 1) << _PAGE_BIT_HIDDEN) 
 #else 
 #define _PAGE_HIDDEN (_AT(pteval_t, 0)) 
 #endif 

3. 增加了一个 slab cache 属性 SLAB_NOTRACK,当设置此属性时,cache 中的 slab 对象不会被 kmemcheck 跟踪。

清单 3. slab 分配标志定义

在 arch/x86/include/asm/slab.h 中:
 #ifdef CONFIG_KMEMCHECK 
 # define SLAB_NOTRACK 0x01000000UL 
 #else 
 # define SLAB_NOTRACK 0x00000000UL 
 #endif 

4. 增加了一个内存分配的 GFP 属性 __GFP_NOTRACK,当置位此标志位时,分配的内存不会被 kmemcheck 跟踪。


清单 5. GFP 分配标志定义
在 include/linux/gfp.h 中:
 #ifdef CONFIG_KMEMCHECK 
 #define __GFP_NOTRACK ((__force gfp_t)0x200000u)  /* Don't track with kmemcheck */ 
 #else 
 #define __GFP_NOTRACK ((__force gfp_t)0) 
 #endif 
kmemcheck 究竟是怎么工作的呢? 下面从四个方面详细介绍了 kmemcheck 的工作原理 ( 假设 kmemcheck 功能被打开):

分配内存

对 分配到的内存数据页面(分配标志中不包含 __GFP_NOTRACK,__GFP_HIGHMEM,对于 slab cache 的内存,cache 创建时标志中不包含 SLAB_NOTRACK),kmemcheck 会为其分配相同数量的影子页面(在分配影子页面时,置位了 __GFP_NOTRACK 标志位,所以它自己不会被 kmemcheck 跟踪),数据页面通过其 page 结构体中的 shadow 指针和影子页面联系起来。然后影子页面中的每个字节会标志为未初始化状态,同时将数据页面对应的页表项中 _PAGE_PRESENT 标志位清零(这样访问该数据页面时会引发页面异常),并置位 _PAGE_HIDDEN 标志位来表明该页面是被 kmemcheck 跟踪的。

访问内存

由 于在分配过程中将数据页面对应的页表项中的 _PAGE_PRESENT 清零了,因此对该数据页面的访问会引发一次页面异常,在 do_page_fault 函数处理过程中,如果它发现页表项属性中的 _PAGE_HIDDEN 置位了,那么说明该页面是被 Kmemcheck 跟踪的,接下来就会进入 kmemcheck 的处理流程,其中会根据该次内存访问地址所对应的影子页面中的内容来检查这次访问是否是合法的,如果是非法的那么它就会将预先设置好的一个 tasklet(该 tasklet 负责错误处理)插入到当前 CPU 的 tasklet 队列中,然后去触发一个软中断,这样在中断的下半部分就会执行这个 tasklet。接下来 kmemcheck 会将影子页面中对应本次内存访问地址的内存区域标识为初始化状态(防止同一个地址警告两次),同时将数据页面页表项中的 _PAGE_PRESENT 置位,并将 CPU 标志寄存器 TF 置位开启单步调试功能,这样当页面异常处理返回后,CPU 会重新执行触发异常的指令,而这次是可以正确执行的。但是执行该指令完毕后,由于 TF 标志位置位了,所以在执行下一条指令之前,系统会进入调试陷阱(debug trap),在其处理函数 do_trap 中,kmemcheck 又会清零该数据页面页表项中的 _PAGE_PRESENT 属性标志位(并且清零标志寄存器中的 TF 位),从而当下次再访问到这个页面时,又会引发一次页面异常。

释放内存

影子页面会随着数据页面的释放而被释放,因此当数据页面被释放之后,如果再去访问该页面,不会出现 kmemcheck 报警。

错误处理

kmemcheck 用了一个循环缓冲区(包含了 CONFIG_KMEMCHECK_QUEUE_SIZE 个元素)来记录每次的警告信息,包括警告类型,引发警告的内存地址及其访问长度,各寄存器的值和 stack trace,同时还将访问地址附近(起始地址:以 2 的 CONFIG_KMEMCHECK_SHADOW_COPY_SHIFT 次幂大小对该地址进行圆整后的值;大小:2 的 CONFIG_KMEMCHECK_SHADOW_COPY_SHIFT 次幂)的数据页面和其对应影子页面中的内容保存在记录中(由同一指令地址引发的相邻的两次警告不会被重复记录)。当前文中注册的 tasklet 被调度执行时,会将循环缓冲区中所有的记录都打印出来。



Kmemcheck 的配置

为了在内核中使用 kmemcheck 功能,需要进行如下设置:

重新编译内核

下面例举了内核选项中针对 Kmemcheck 的配置选项,以及它们应该被设置的值(或推荐值):

CONFIG_CC_OPTIMIZE_FOR_SIZE=n

禁 止 gcc 对数据长度进行优化,例如在 32 位的机器中,为了提高内存访问速度,gcc 可能会将一些 16 位的数据访问提升至 32 位(真正使用时会舍弃高 16 位),这样 kmemcheck 可能就会对高 16 位中数据内容访问发出警告(这种警告成为伪警告)。这个选项是配置 kmemcheck 的前提,否则 kmemcheck 不会出现在配置选项中。默认是 y,在选项"General setup" 中。

CONFIG_SLAB=y or CONFIG_SLUB=y

使用 slab 或者 slub 机制,默认是 CONFIG_SLUB=y,在选项"General setup" 中。

CONFIG_FUNCTION_TRACER=n

防止嵌套的页面异常,默认是 n,在选项"General setup" 中。

CONFIG_DEBUG_PAGEALLOC=n

关闭页面分配调试功能,默认是 n,在选项"Kernel hacking" 中。

CONFIG_DEBUG_INFO=y (推荐值)

打开内核调试信息,方便内核调试,在选项"Kernel hacking" 中。

CONFIG_KMEMCHECK=y

决定内核是否包含 kmemcheck 功能,在选项"Kernel hacking" 中

CONFIG_KMEMCHECK_[DISABLED|ENABLED| ONESHOT]_BY_DEFAULT

定 义 Kmemcheck 在机器启动时的状态,默认是 ENABLED,在选项"Kernel hacking" 中。DISABLED 为不启动,ENABLED 为启动但它会降低启动的速度,ONESHOT 将在第一次警告之后关闭 Kmemcheck 功能。kmemcheck 的状态是可以在系统启动后通过修改 /proc/sys/kernel/kmemcheck 的值来进行动态调整的。

CONFIG_KMEMCHECK_QUEUE_SIZE

出错循环缓冲区大小,默认是 64,即最多一次可以保存 64 条警告记录,推荐保留默认值。

CONFIG_KMEMCHECK_SHADOW_COPY_SHIFT

当发生警告时,保存下来的内存数据大小,默认是 5,即可以保存 32 字节的数据,推荐保留默认值。

CONFIG_KMEMCHECK_PARTIAL_OK

为了解决 gcc 对数据长度的优化,默认是 y,推荐保留默认值。

CONFIG_KMEMCHECK_BITOPS_OK

针对位域的访问,默认是 n,推荐保留默认值(如果需要用到 kmemcheck 来对位域的访问进行跟踪,推荐使用其提供的 Bitfield annotations)。

重启机器

选用新内核来启动系统,此时系统会根据 CONFIG_KMEMCHECK_[DISABLED|ENABLED|ONESHOT]_BY_DEFAULT 来决定 kmemcheck 在启动时的状态,如果需要动态修改,可以在引导程序的内核启动选项中加入 kmemcheck=x 参数(x 为 0 对应 CONFIG_KMEMCHECK_DISABLED_BY_DEFAULT,x 为 1 对应 CONFIG_KMEMCHECK_ENABLED_BY_DEFAULT,x 为 2 时对应 CONFIG_KMEMCHECK_ONESHOT_BY_DEFAULT)。例如可以设置如下 grub 参数选项来禁止 kmemcheck 在系统启动时启动:

 title Fedora (2.6.31.1 with kmemcheck) 
 root (hd0, 5) 
 kernel /boot/vmlinuz-2.6.31.1 ro root=UUID=6f6907e3-2dc6-4d4f-bd05-0e53111ad969 \ 
     kmemcheck=0 
 initrd /boot/initrd-2.6.31.1 

另外还可以在系统启动完毕之后,动态启用 kmemcheck 功能,值得注意的是 kmemcheck 的该功能仍处在试验阶段,可能会产生一些伪警告信息。打开 kmemcheck 功能启动命令为:

 echo 1  > /proc/sys/kernel/kmemcheck  #0:disable 1:enable 2:one-shot 
Kmemcheck 使用示例

下面通过三个例子分别展示了 kmemcheck 所能检测出的三种内存访问错误:

1. 在本例中(完整代码请参阅附件 1 中的 kmemchk_ uninitialized.c),我们先用 alloc_pages 分配了两个页面大小的内存,然后在未初始化的情况下对其中的内容进行访问,我们会发现 kmemcheck 会发出内存未初始化警告信息(即 KMEMCHECK_SHADOW_UNINITIALIZED 类型的错误信息)。


清单 6: kmemchk_uninitialized.c 部分代码示例
 static int __init kmemchk_uninitialized_init(void) 
 { 
        char * addr;  /* used to store page struct addresses */ 
        int offset;   /* offset to the page */ 
        pages = alloc_pages(GFP_KERNEL,1);     /* allocate 2 pages,  \ 
                 if __GFP_NOTRACK is specified, no kmemcheck warnings would be issued */ 

     if(!pages) 
          printk("alloc_pages: allocation failed !\n"); 
        else { 
          addr = page_address(pages); /* convert to virt addr */ 
         
          offset = 43; 
          printk("checkpoint: access mem page: %p offset: %d \n",addr,offset); 
          if(*(addr + offset) == 'a' )  /* access uninitialized memory */
            printk("You hit a ramdon char \n"); 
        } 
 } 

加载模块后终端会显示 kmemcheck 打印的警告信息(完整 log 信息请参阅附件 2):


清单 6. Unintialized 警告日志
 checkpoint: access mem page: cef52000 offset: 43 
 WARNING: kmemcheck: Caught 8-bit read from uninitialized memory (cef5202b)   --> a 
 0000000000000000000000000000000000000000000000000000000000000000     --> b 
 u u u u u u u u u u u u u u u u u u u u u u u u u u u u u u u u        --> c 
                          ^                                                        --> d 
 Pid: 13017, comm: insmod Tainted: G      D W  (2.6.31.1 #2) V71         --> e 
 EIP: 0060:[] EFLAGS: 00010286 CPU: 0 
 EIP is at 0xd09d306a 
 EAX: 00000035 EBX: cef52000 ECX: 00000092 EDX: 00885000 
 ESI: 00000000 EDI: b8018fc0 EBP: cef25f5c ESP: c09e2898 
 DS: 007b ES: 007b FS: 00d8 GS: 0000 SS: 0068 
 CR0: 8005003b CR2: cfbd92e0 CR3: 0ef2c000 CR4: 000006d0 
 DR0: 00000000 DR1: 00000000 DR2: 00000000 DR3: 00000000 
 DR6: ffff4ff0 DR7: 00000400 
 [] do_one_initcall+0x23/0x180 
 [] sys_init_module+0xb1/0x1f0 
 [] sysenter_do_call+0x12/0x28 
 [] 0xffffffff 

下面对清单 6 中的主要内容进行分析:
































阅读(1674) | 评论(1) | 转发(1) |
给主人留下些什么吧!~~

chinaunix网友2011-03-07 14:36:32

很好的, 收藏了 推荐一个博客,提供很多免费软件编程电子书下载: http://free-ebooks.appspot.com