内存管理 · 2016-12-23 0

【Linux内存管理】内存破坏检测kmemcheck示例

前面分析了kmemcheck的实现,那么现在就针对其功能进行试验,鉴于kmemcheck支持KMEMCHECK_SHADOW_UNALLOCATED、KMEMCHECK_SHADOW_UNINITIALIZED及KMEMCHECK_SHADOW_FREED的检测且检测上报信息大同小异,本文中的实验主要针对于未初始化的内存进行。

如果需要使能kmemcheck,需要进行一系列的内核参数设置,具体的配置项可以参考内核源码Document目录下的kmemcheck.txt文档描述(不过文档更新的节奏似乎跟不上源码的更新速度,有部分存在偏差)。至于修改配置项make menuconfig,内核编译的设置选项中设置即可。

主要的配置项有:

CONFIG_CC_OPTIMIZE_FOR_SIZE=n

——该项修改为关闭, 设置路径:”General setup” / “Optimize for size”。如果没有关闭该项,Gcc编译器将会进行优化,这将会导致错误地触发kmemcheck上报非问题。比如代码中访问16bit的数据,Gcc则会先加载32bit的数据,然后丢弃高16bit的信息,而kmemcheck只会看到操作了32bit的数据,并且高16bit可能未初始化,那么将会触发问题上报;

CONFIG_SLAB=y or CONFIG_SLUB=y

——选择了slub算法,设置路径:”General setup” / “Choose SLAB Allocator”。

CONFIG_FUNCTION_TRACER=n

——该选项设置为关闭,设置路径:”Kernel hacking” / “Tracers” / “Kernel Function Tracer”。当函数轨迹编译进去的时候,在每个函数调用之前,Gcc都将会向其他函数发出一个调用。这将导致缺页异常处理被调用的时候,ftrace的框架将会在kmemcheck之前调动并获得机会去处理该异常。如果ftrace修改了kmemcheck所依赖的内存信息,将会导致无限的缺页异常循环。

CONFIG_DEBUG_PAGEALLOC= n

——配置项设置为关闭,似乎不太影响。文档路径: “Kernel hacking” / “Debug page memory allocations”。实际设置路径:”Kernel hacking”/”Memory Debugging”/”Debug page memory allocations”。

CONFIG_DEBUG_INFO=y

——该配置项设置为开启,设置路径:”Kernel hacking”/”Compile-time checks and compiler options”/”Compile the kernel with debug info”。

经过前面的设置,将可以看到kmemcheck调试功能开启选项。具体路径:”Kernel hacking” / “Memory Debugging” / “kmemcheck: trap use of uninitialized memory”)。在该设置路径里面有详细的子选项可供设置。

具体子选项有:

CONFIG_KMEMCHECK_[DISABLED | ENABLED | ONESHOT]_BY_DEFAULT

——该设置选项,用于设置Kmemcheck的状态,其中DISABLED表示kmemcheck功能关闭(值为0),而ENABLED表示系统引导之初就使能kmemcheck功能(其值为1),最后的是ONESHOT表示kmemcheck功能仅捕获一次而后自动关闭(其值为2)。如果把握不准的话,可以默认设置功能关闭,因为在系统启动后通过修改/proc/sys/kernel/kmemcheck的值来进行动态调整的,将文件中的修改为对应的状态值即可。该配置项建议修改为DISABLED,后期系统运行后再修改配置文件进行开启,否则系统启动的时候会很慢很慢。

CONFIG_KMEMCHECK_QUEUE_SIZE

——在设置选项中描述为 “kmemcheck: error queue size”。

CONFIG_KMEMCHECK_SHADOW_COPY_SHIFT

——在设置选项中描述为 “kmemcheck: shadow copy size (5 => 32 bytes, 6 => 64 bytes)”。

CONFIG_KMEMCHECK_PARTIAL_OK

——在设置选项中描述为“kmemcheck: allow partially uninitialized memory”。

CONFIG_KMEMCHECK_BITOPS_OK

——在设置选项中描述为“kmemcheck: allow bit-field manipulation”。

本文中的示例此几项的配置为:

其实这些配置项信息都无需记住路径在何处,只需要在make menuconfig的时候,使用右上至左下的斜杠 “/”进入搜索界面,直接搜索上面的配置项即可得到详细的路径信息,无需在乎内核版本如何更新。

将上述的配置项进行配置后,重新编译内核以及安装后,即可进行kmemcheck的测试实验了(由于pc的发展,32位环境已经无法接近于绝迹了,只好找了个64位的环境作为演示示例了)。具体的内核模块如何编写,这里就不赘述了,后期有机会再进行补充。示例实验测试代码:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/mm.h>
#include <asm/page.h>

struct page *pages;

void kmemchk_access_uninitialized(char *addr)
{
    int offset = 23;

    printk("[Kmemchk]: access mem page(%p) offset(%d) \n", addr, offset);
    if (*(addr + offset) == 'a')  /*此处尝试访问未被初始化的内存*/
    {
        printk("[Kmemchk]: Fail to hit a ramdon char \n");
    }
}

static int __init kmemchk_uninitialized_init(void)
{
    char *addr;
    
    printk("[Kmemchk]: kmemchk_uninitialized_init: \n");

    pages = alloc_pages(GFP_KERNEL, 1);
    if (!pages)
    {
        printk("[Kmemchk]: alloc_pages() allocation failed!\n");
    }
    else
    {
        addr = page_address(pages);

        kmemchk_access_uninitialized(addr);
    }

    return 0;
}

static void __exit kmemchk_uninitialized_exit(void)
{
    if (pages)
    {
        __free_pages(pages,1);
    }

    printk("[Kmemchk]: kmemchk_uninitialized_exit now \n");
}

module_init(kmemchk_uninitialized_init)
module_exit(kmemchk_uninitialized_exit)

MODULE_LICENSE("GPL");

 

Makefile编译脚本:

obj-m = kmemcheck_test.o

all:
    make -C /lib/modules/`uname -r`/build M=`pwd`

clean:
    rm -f *.o *.ko *.mod.c modules.order Module.symvers

 

通过Makefile编译出kmemcheck_test.ko后,通过insmod命令即可将该模块加载到环境中,然后通过dmesg即可看到执行结果:

详细的错误信息解析如下:

[ 2499.928090] [Kmemchk]: kmemchk_uninitialized_init: 
[ 2499.928094] [Kmemchk]: access mem page(ffff88036a4ac000) offset(23) 
[ 2499.929842] WARNING: kmemcheck: Caught 8-bit read from uninitialized memory (ffff88036a4ac017)
#此行记录了告警类型,以及访问的未初始化内存空间地址0xffff88036a4ac017
[ 2499.929843] 80655b6b0388ffff0200000000000000000000000000000000f0ffffff7f0000
#此处是dump出来的内存数据,具体长度取决于CONFIG_KMEMCHECK_SHADOW_COPY_SHIFT配置项
[ 2499.929851]  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
[ 2499.929858]                                                ^
#此处的u表示KMEMCHECK_SHADOW_UNINITIALIZED类型的错误,此外KMEMCHECK_SHADOW_UNALLOCATED和
#KMEMCHECK_SHADOW_FREED类型的错误则分别由a和f表示;而^则指出了引致错误的内存访问位置。
[ 2499.929859] RIP: 0010:[<ffffffffc078201c>]  [<ffffffffc078201c>] kmemchk_access_uninitialized+0x1c/0x33 [kmemcheck_test]
#这里指出了报出告警的指令地址以及函数和其函数内指令偏移。
[ 2499.929862] RSP: 0018:ffff88036a4cfc60  EFLAGS: 00010286
[ 2499.929862] RAX: 0000000000000038 RBX: ffff88036a4ac000 RCX: 0000000000000006
[ 2499.929863] RDX: 0000000000000000 RSI: 0000000000000246 RDI: ffff88037020dc30
[ 2499.929864] RBP: ffff88036a4cfc68 R08: 0000000000000002 R09: 00000000000003c8
[ 2499.929865] R10: 00003ffffffff000 R11: 00000000000003c8 R12: ffff88036a455a80
[ 2499.929865] R13: 0000000000000000 R14: ffffffffc0006000 R15: ffffffffc0784000
[ 2499.929866] FS:  0000000000000000(0000) GS:ffff880370200000(0000) knlGS:0000000000000000
[ 2499.929867] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[ 2499.929868] CR2: ffff880368e561d8 CR3: 0000000002c0a000 CR4: 00000000003406f0
[ 2499.929869] DR0: 0000000000000000 DR1: 0000000000000000 DR2: 0000000000000000
[ 2499.929870] DR3: 0000000000000000 DR6: 00000000ffff4ff0 DR7: 0000000000000400
[ 2499.929870]  [<ffffffffc000606f>] 0xffffffffc000606f
[ 2499.929872]  [<ffffffff8100211e>] do_one_initcall+0xae/0x1f0
[ 2499.929874]  [<ffffffff8113b6e3>] do_init_module+0x55/0x1c1
[ 2499.929876]  [<ffffffff810eba2a>] load_module+0x208a/0x26a0
[ 2499.929878]  [<ffffffff810ec284>] SYSC_finit_module+0xb4/0xe0
[ 2499.929880]  [<ffffffff810ec2c9>] SyS_finit_module+0x9/0x10
[ 2499.929882]  [<ffffffff81755936>] entry_SYSCALL_64_fastpath+0x16/0x75
[ 2499.929884]  [<ffffffffffffffff>] 0xffffffffffffffff

 

根据错误信息可以看到此处报出来的告警在kmemchk_access_uninitialized(),位于偏移0x1c的指令访问了未被初始化的内存空间。继而将编译生成的kmemcheck_test.ko进行反汇编,找到kmemchk_access_uninitialized()的该指令。

0000000000000000 <kmemchk_access_uninitialized>:
   0:	55                   	push   %rbp
   1:	48 89 fe             	mov    %rdi,%rsi
   4:	ba 17 00 00 00       	mov    $0x17,%edx
   9:	48 89 e5             	mov    %rsp,%rbp
   c:	53                   	push   %rbx
   d:	48 89 fb             	mov    %rdi,%rbx
  10:	48 c7 c7 00 00 00 00 	mov    $0x0,%rdi
  17:	e8 00 00 00 00       	callq  1c <kmemchk_access_uninitialized+0x1c>
  1c:	80 7b 17 61          	cmpb   $0x61,0x17(%rbx)
  20:	74 03                	je     25 <kmemchk_access_uninitialized+0x25>
  22:	5b                   	pop    %rbx
  23:	5d                   	pop    %rbp
  24:	c3                   	retq   
  25:	48 c7 c7 00 00 00 00 	mov    $0x0,%rdi
  2c:	e8 00 00 00 00       	callq  31 <kmemchk_access_uninitialized+0x31>
  31:	eb ef                	jmp    22 <kmemchk_access_uninitialized+0x22>

 

即cmpb $0x61,0x17(%rbx)指令,可以分析得出该比较指令对应的即为函数中的if (*(addr + offset) == ‘a’)该条件判断操作。

由此一来便可以定位此类对未初始化内存进行访问的不合法的信息。