kmemleak工具

前言

内存泄漏分为用户态的内存泄漏和内核态的内存泄漏,我们本文主要关注的是内核态的内存泄漏。工作中比较常见的内存泄漏按照发生泄漏的频率可以划分以下几种类型:
1、一次性内存泄漏,只在初始化过程中或某一次条件触发产生的内存泄漏。
2、偶发性内存泄漏,在某种条件下偶尔触发产生的内存泄漏。
3、频发性内存泄漏,内存泄漏点被频繁的触发。

对于频发性内存泄漏我们有比较多的调试手段去定位,比如我们可以先通过/proc/meminfo信息大致确定下内存泄漏发生在哪个模块中,再通过其他手段进一步定位。如果观察到vmalloc异常,可以通过/proc/vmallocinfo信息分析定位。如果观察到slab内存异常,可以通过slabinfo和/sys/kernel/slab/*/alloc_calls或free_calls去辅助定位问题。而对于一次性的或者偶发性的内存泄漏确很难去通过/proc/meminfo信息快速分析定位,且大量的一次性或偶发性内存泄漏,同样给系统造成额外的内存压力。而本文介绍的kmemleak工具为各种类型的内存泄漏提供了一种检测方法。

KMEMLEAK原理

kmemleak(kernel memory leak detector)是检测内核空间的内存泄漏的调试工具。检测对象是memblock_alloc、kmalloc、vmalloc、kmem_cache_alloc等函数分配的内存块,该内存块由struct kmemleak_object来描述(简称为object)。kmemleak的实现原理非常简单,通过暴力扫描内存(假定内存中存放的都是指针,以ARM64为例,每次扫描8个字节),如果找不到指向起始地址或者内存块任何位置的指针,则分配的内存块被认为是孤立的。这意味着内核可能无法将分配内存块的地址传递给释放函数,因此该内存块被视为内存泄漏。内存块(object)有3种颜色,分别为黑色、白色、灰色, 通过count和min_count区分不同颜色的object。

  • 黑色: min_count = -1,表示被忽略的object,此object不包含对别人的引用,也不会存在内存泄漏,比如代码段会标记为黑色。

  • 白色: count < min_count,孤立的object,没有足够的引用指向这个object,一轮扫描结束后被认为泄漏的内存块。

  • 灰色: min_count = 0,表示不是孤立的object,即不存在内存泄漏的object,如代码中主动标记object为灰色,防止误报(如data、bss、ro_after_init)。或者count >= min_count,对该object有足够的指针引用,认为不存在内存泄漏的内存块。

具体检测步骤如下:

1、通过struct kmemleak_object(简称为object)描述kmalloc、vmalloc、kmem_cache_alloc等函数申请的内存块,记录申请内存的起始地址,大小、call trace等信息。同时把object加入到红黑树object_tree_root和双向链表object_list中,红黑树中的key值为内存块的起始地址。

2、遍历双向链表object_list,把所有的object的count计数清0,即在新的一轮扫描前,尽可能的把能复位成白色的object标记为白色。然后判断object是否是灰色(默认data、bss、ro_after_init段会被标记为灰色),如果是灰色的object则把object加入到灰色链表gray_list中。

3、扫描内存中可能存放指针的内存区域(per-cpu段、struct page的内容、内核栈、灰色链表),根据挂在红黑树中所有的object的地址范围进行对比。如果有指针指向某一个object(指向该object的起始地址或者指向object地址范围内),会把object对应的count字段增加1,如果object变成灰色,则会把object加入到灰色链表中。

4、扫描object_list中的白色对象的object,判断object所描述的地址范围的内容的crc值是否发生变化,如果发生变化,则同样把object加入到灰色链表gray_list中。说明通过间接的方式访问了object描述的地址范围,不是内存泄漏,减少误报。

5、重新扫描灰色链表,因为步骤4中,可能有些白色的object加入到了灰色链表中,需要重新扫描。

6、经过上述一系列的扫描,剩余白色的object就是可疑的内存泄漏点。

开启KMEMLEAK功能

如果定义了CONFIG_DEBUG_KMEMLEAK_DEFAULT_OFF宏或者通过cmdline设置为kmemleak=off,则默认关闭kmemleak。如果cmdline中设置kmemleak=on,则表示默认开启kmemleak功能。如果没有定义CONFIG_DEBUG_KMEMLEAK_DEFAULT_OFF宏,则默认是开启kmemleak功能

9155 CONFIG_HAVE_DEBUG_KMEMLEAK=y
9156 CONFIG_DEBUG_KMEMLEAK=y
9157 CONFIG_DEBUG_KMEMLEAK_MEM_POOL_SIZE=16000
9158 # CONFIG_DEBUG_KMEMLEAK_TEST is not set
9159 # CONFIG_DEBUG_KMEMLEAK_DEFAULT_OFF is not set
9160 # CONFIG_DEBUG_KMEMLEAK_AUTO_SCAN is not set

测试用例

kmemleak-test.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 * samples/kmemleak/kmemleak-test.c
 *
 * Copyright (C) 2008 ARM Limited
 * Written by Catalin Marinas <catalin.marinas@arm.com>
 */

#define pr_fmt(fmt) "kmemleak: " fmt

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>
#include <linux/list.h>
#include <linux/percpu.h>
#include <linux/fdtable.h>

#include <linux/kmemleak.h>

struct test_node {
	long header[25];
	struct list_head list;
	long footer[25];
};

static LIST_HEAD(test_list);
static DEFINE_PER_CPU(void *, kmemleak_test_pointer);

/*
 * Some very simple testing. This function needs to be extended for
 * proper testing.
 */
static int __init kmemleak_test_init(void)
{
	struct test_node *elem;
	int i;

	pr_info("Kmemleak testing\n");

	/* make some orphan objects */
	pr_info("kmalloc(32) = %p\n", kmalloc(32, GFP_KERNEL));
	pr_info("kmalloc(32) = %p\n", kmalloc(32, GFP_KERNEL));
	pr_info("kmalloc(1024) = %p\n", kmalloc(1024, GFP_KERNEL));
	pr_info("kmalloc(1024) = %p\n", kmalloc(1024, GFP_KERNEL));
	pr_info("kmalloc(2048) = %p\n", kmalloc(2048, GFP_KERNEL));
	pr_info("kmalloc(2048) = %p\n", kmalloc(2048, GFP_KERNEL));
	pr_info("kmalloc(4096) = %p\n", kmalloc(4096, GFP_KERNEL));
	pr_info("kmalloc(4096) = %p\n", kmalloc(4096, GFP_KERNEL));
#ifndef CONFIG_MODULES
	pr_info("kmem_cache_alloc(files_cachep) = %p\n",
		kmem_cache_alloc(files_cachep, GFP_KERNEL));
	pr_info("kmem_cache_alloc(files_cachep) = %p\n",
		kmem_cache_alloc(files_cachep, GFP_KERNEL));
#endif
	pr_info("vmalloc(64) = %p\n", vmalloc(64));
	pr_info("vmalloc(64) = %p\n", vmalloc(64));
	pr_info("vmalloc(64) = %p\n", vmalloc(64));
	pr_info("vmalloc(64) = %p\n", vmalloc(64));
	pr_info("vmalloc(64) = %p\n", vmalloc(64));

	/*
	 * Add elements to a list. They should only appear as orphan
	 * after the module is removed.
	 */
	for (i = 0; i < 10; i++) {
		elem = kzalloc(sizeof(*elem), GFP_KERNEL);
		pr_info("kzalloc(sizeof(*elem)) = %p\n", elem);
		if (!elem)
			return -ENOMEM;
		INIT_LIST_HEAD(&elem->list);
		list_add_tail(&elem->list, &test_list);
	}

	for_each_possible_cpu(i) {
		per_cpu(kmemleak_test_pointer, i) = kmalloc(129, GFP_KERNEL);
		pr_info("kmalloc(129) = %p\n",
			per_cpu(kmemleak_test_pointer, i));
	}

	return 0;
}
module_init(kmemleak_test_init);

static void __exit kmemleak_test_exit(void)
{
	struct test_node *elem, *tmp;

	/*
	 * Remove the list elements without actually freeing the
	 * memory.
	 */
	list_for_each_entry_safe(elem, tmp, &test_list, list)
		list_del(&elem->list);
}
module_exit(kmemleak_test_exit);

MODULE_LICENSE("GPL");

Makefile

# SPDX-License-Identifier: GPL-2.0-only
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-

KERNEL_DIR ?=~/linux_rt/linux-rt-5.15/
obj-m := kmemleak-test.o

modules:
	$(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean

install:
	cp *.ko $(KERNEL_DIR)/kmodules

运行结果

加载模块,触发内存泄漏检测:

insmod kmemleak-test.ko
echo scan > /sys/kernel/debug/kmemleak

查看结果如下:

[root@liebao kmemleak]# cat /sys/kernel/debug/kmemleak
unreferenced object 0xffff000006d3a400 (size 1024):
  comm "insmod", pid 1040, jiffies 4295348039 (age 1046.512s)
  hex dump (first 32 bytes):
    00 e0 b2 45 00 00 00 00 17 00 00 00 01 00 01 00  ...E............
    00 90 b2 45 00 00 00 00 0b 00 00 00 03 00 02 00  ...E............
  backtrace:
    [<(____ptrval____)>] slab_post_alloc_hook+0x7c/0x260
    [<(____ptrval____)>] kmem_cache_alloc_trace+0x188/0x358
    [<(____ptrval____)>] 0xffff8000010950a0
    [<(____ptrval____)>] do_one_initcall+0x50/0x2b0
    [<(____ptrval____)>] do_init_module+0x50/0x1e8
    [<(____ptrval____)>] load_module+0x2184/0x2708
    [<(____ptrval____)>] __do_sys_init_module+0x1e8/0x228
    [<(____ptrval____)>] __arm64_sys_init_module+0x24/0x30
    [<(____ptrval____)>] invoke_syscall+0x54/0x118
    [<(____ptrval____)>] el0_svc_common.constprop.3+0x90/0x118
    [<(____ptrval____)>] do_el0_svc+0x34/0xa0
    [<(____ptrval____)>] el0_svc+0x20/0x60
    [<(____ptrval____)>] el0t_64_sync_handler+0x88/0xb0
    [<(____ptrval____)>] el0t_64_sync+0x16c/0x170
unreferenced object 0xffff000006d3a800 (size 1024):
  comm "insmod", pid 1040, jiffies 4295348039 (age 1046.512s)
  hex dump (first 32 bytes):
    00 00 e0 48 00 00 00 00 0b 00 00 00 01 00 01 00  ...H............
    00 00 e6 48 00 00 00 00 00 10 00 00 03 00 02 00  ...H............
  backtrace:
    [<(____ptrval____)>] slab_post_alloc_hook+0x7c/0x260
    [<(____ptrval____)>] kmem_cache_alloc_trace+0x188/0x358
    [<(____ptrval____)>] 0xffff8000010950bc
    [<(____ptrval____)>] do_one_initcall+0x50/0x2b0
    [<(____ptrval____)>] do_init_module+0x50/0x1e8
    [<(____ptrval____)>] load_module+0x2184/0x2708
    [<(____ptrval____)>] __do_sys_init_module+0x1e8/0x228
    [<(____ptrval____)>] __arm64_sys_init_module+0x24/0x30
    [<(____ptrval____)>] invoke_syscall+0x54/0x118
    [<(____ptrval____)>] el0_svc_common.constprop.3+0x90/0x118
    [<(____ptrval____)>] do_el0_svc+0x34/0xa0
    [<(____ptrval____)>] el0_svc+0x20/0x60
    [<(____ptrval____)>] el0t_64_sync_handler+0x88/0xb0
    [<(____ptrval____)>] el0t_64_sync+0x16c/0x170
。。。

通过kmemleak report的输出信息,我们可以获取如下信息:
1、产生泄漏内存块对应的object的起始地址为0xffff000006d3a400 ,泄漏的大小为1024个字节。
2、进程名为insmod,pid为1040,创建object时的jiffies为1046.512s。
3、泄漏内存块的前32字节数据。
4、泄漏点的backtrace信息。

卸载模块,因没有free内存,会有新的内存泄漏

[root@liebao kmemleak]# rmmod kmemleak_test
[root@liebao kmemleak]# echo scan > /sys/kernel/debug/kmemleak
[10015.161611] kmemleak: 14 new suspected memory leaks (see /sys/kernel/debug/kmemleak)
[root@liebao kmemleak]# cat /sys/kernel/debug/kmemleak
unreferenced object 0xffff000008c72c00 (size 256):
  comm "insmod", pid 1040, jiffies 4295348048 (age 8201.876s)
  hex dump (first 32 bytes):
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  backtrace:
    [<(____ptrval____)>] slab_post_alloc_hook+0x7c/0x260
    [<(____ptrval____)>] kmem_cache_alloc_trace+0x188/0x358
    [<(____ptrval____)>] 0xffff800001095268
    [<(____ptrval____)>] do_one_initcall+0x50/0x2b0
    [<(____ptrval____)>] do_init_module+0x50/0x1e8
    [<(____ptrval____)>] load_module+0x2184/0x2708
    [<(____ptrval____)>] __do_sys_init_module+0x1e8/0x228
    [<(____ptrval____)>] __arm64_sys_init_module+0x24/0x30
    [<(____ptrval____)>] invoke_syscall+0x54/0x118
    [<(____ptrval____)>] el0_svc_common.constprop.3+0x90/0x118
    [<(____ptrval____)>] do_el0_svc+0x34/0xa0
    [<(____ptrval____)>] el0_svc+0x20/0x60
    [<(____ptrval____)>] el0t_64_sync_handler+0x88/0xb0
    [<(____ptrval____)>] el0t_64_sync+0x16c/0x170
unreferenced object 0xffff000008c72d00 (size 256):
  comm "insmod", pid 1040, jiffies 4295348048 (age 8201.924s)
  hex dump (first 32 bytes):
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  backtrace:
    [<(____ptrval____)>] slab_post_alloc_hook+0x7c/0x260
    [<(____ptrval____)>] kmem_cache_alloc_trace+0x188/0x358
    [<(____ptrval____)>] 0xffff800001095268
    [<(____ptrval____)>] do_one_initcall+0x50/0x2b0
    [<(____ptrval____)>] do_init_module+0x50/0x1e8
    [<(____ptrval____)>] load_module+0x2184/0x2708
    [<(____ptrval____)>] __do_sys_init_module+0x1e8/0x228
    [<(____ptrval____)>] __arm64_sys_init_module+0x24/0x30
    [<(____ptrval____)>] invoke_syscall+0x54/0x118
    [<(____ptrval____)>] el0_svc_common.constprop.3+0x90/0x118
    [<(____ptrval____)>] do_el0_svc+0x34/0xa0
    [<(____ptrval____)>] el0_svc+0x20/0x60
    [<(____ptrval____)>] el0t_64_sync_handler+0x88/0xb0
    [<(____ptrval____)>] el0t_64_sync+0x16c/0x170
。。。

从调用栈看不到[<(ptrval)>] 0xffff800001095268这些地址对应的源代码,在kallsyms 搜索地址,发现在kmemleak_test模块里。另外可以借助objdump和addr2line工具看内存泄漏的代码行。
使用objdump时,发现pc值和实际代码有0x5000的偏移,不清楚是不是linux模块链接时,有没有相关操作,待确认。

[root@liebao kmemleak]# cat /proc/kallsyms | grep ffff8000010
ffff800001090000 t $x	[kmemleak_test]
ffff800001090000 t kmemleak_test_exit	[kmemleak_test]
ffff800001092000 d $d	[kmemleak_test]
ffff800001092000 d test_list	[kmemleak_test]
ffff800001091028 r $d	[kmemleak_test]
ffff800001092040 d $d	[kmemleak_test]
ffff800001091140 r $d	[kmemleak_test]
ffff800001091140 r _note_9	[kmemleak_test]
ffff800001091158 r _note_8	[kmemleak_test]
ffff800001092040 d __this_module	[kmemleak_test]
ffff800001090000 t cleanup_module	[kmemleak_test]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值