本文是 Linux 内核内存检测工具系列中的一篇,主要分析了 Kmemcheck 的原理,配置以及它的典型应用。读者在阅读完本文之后,能轻松地学会怎样利用 Kmemcheck 来对内核程序进行检测,同时也能从 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 的配置选项,以及它们应该被设置的值(或推荐值):
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 在系统启动时启动: