同前一篇文章一样,本文将以一个驱动程序(tleak.c tleak.conf)为例说明如何利用mdb的::findleaks命令检测内核代码是否存在内存泄漏。
请注意,上一篇文章给的示例应用程序其内存泄漏发生在堆(heap)上,当程序退出的时候,堆随之被释放掉,所以并不会对系统造成影响。而本文提供的示例驱动tleak将在内核产生内存泄漏,所以请谨慎使用,不熟悉内核的朋友请不要在自己的机器上运行该驱动及以下步骤。(USE AT YOUR OWN RISK)
tleak是一个伪字符设备,每打开一次,会进行一次内存分配,则当第二次打开该设备的时候就会产生内存泄漏,主要函数tleak_open()定义如下:
static int
tleak_open(dev_t *devp, int flag, int otyp, cred_t *credp)
{
if (otyp != OTYP_CHR)
return (EINVAL);
tleak_addr = kmem_zalloc(100, KM_SLEEP);return (0);
}
首先设置系统变量kmem_flags以使能核心内存分配(kernel memory allocator)的调试功能,这些功能在缺省情况下是被禁止的。为此在/etc/system中加入行:
set kmem_flags=0xf
重启机器,用mdb确认kmem_flag的值
$ mdb -k
Loading modules: [ unix krtld genunix specfs dtrace cpu.AuthenticAMD.15 uppc pcplusmp ufs ip sctp usba uhci s1394 nca fcp fctl lofs zfs random audiosup md cpc crypto fcip logindmux ptm sppp nfs ]
> kmem_flags/X
kmem_flags:
kmem_flags: f
其次编译、安装驱动程序tleak。
$ /usr/sfw/bin/gcc -D_KERNEL -c tleak.c
$ ld -dy -r -o tleak tleak.o
$ cp tleak /kernel/drv/
$ cp tleak.conf /kernel/drv/
$ add_drv tleak
add_drv将自动加载驱动程序,用modinfo检查一下
$ modinfo | grep tleak
194 fa15bb04 484 205 1 tleak (Test kernel memory leak v0.1)
在/devices下生成了设备文件/devices/pseudo/tleak@0:tleak。多次运行cat打开设备以产生内存泄漏
$ cat /devices/pseudo/tleak@0:tleak
强制系统coredump,同时重启机器
$ mdb -K
Loaded modules: [ audiosup crypto cpc uppc ptm ufs unix zfs krtld s1394 sppp ipcnca uhci lofs genunix ip logindmux usba specfs pcplusmp nfs md random sctp cpu.AuthenticAMD.15 ]
[0]> $
注意,"mdb -K"须在控制台上才能运行。另外,在控制台或终端运行"reboot -d"也可以让核心coredump。
等机器重新启动后,用mdb调试上一步生成的核心core文件
$ cd /var/crash/
/
$ ls
bounds unix.0 vmcore.0
$ mdb -k 0
Loading modules: [ unix krtld genunix specfs dtrace cpu.AuthenticAMD.15 uppc pcplusmp ufs ip sctp usba uhci s1394 nca fcp fctl lofs zfs random audiosup md cpc crypto fcip logindmux ptm sppp nfs ]
> ::status
debugging crash dump vmcore.0 (32-bit) from mars
operating system: 5.11 snv_34 (i86pc)
panic message:
BAD TRAP: type=e (#pf Page fault) rp=d4e7cdb8 addr=0 occurred in module "" due to a NULL pointer dereference
dump content: kernel pages only
> ::findleaks
CACHE
|
LEAKED
|
BUFCTL
|
CALLER
|
dac2e6f0
|
2 |
d3f14980 |
AcpiOsAllocate+0x15 |
dac2e6f0 |
5 |
d3f20c40 |
AcpiOsAllocate+0x15 |
dac2e6f0 |
1 |
d3f14ae8 |
AcpiOsAllocate+0x15 |
dac2e6f0 |
1 |
d3f1e618 |
AcpiOsAllocate+0x15 |
dac2e6f0 |
7 |
d3f20cb8 |
AcpiOsAllocate+0x15 |
dac2e6f0 |
2
|
d3f20b50 |
AcpiOsAllocate+0x15 |
dac32030 |
1
|
d4ec7748 |
tleak_open+0x35 |
---------
|
---------
|
---------------
|
-------------------------
|
Total
|
19
|
buffers,
|
976 bytes
|
> d4ec7748$
ADDR
|
BUFADDR
|
TIMESTAMP
|
THREAD
|
|
CACHE
|
LASTLOG
|
CONTENTS
|
d4ec7748
|
d4db0300
|
a1397b121b
|
d64db340
|
|
dac32030
|
db0f0628
|
dbb62e98
|
|
kmem_cache_alloc_debug+0x256
|
|
kmem_cache_alloc+0x97
|
|
kmem_zalloc+0x4b
|
|
tleak_open+0x35
|
|
dev_open+0x27
|
|
spec_open+0x3cc
|
|
fop_open+0x6e
|
|
vn_openat+0x42a
|
|
copen+0x287
|
|
open64+0x20
|
至此,我们已能识别出tleak产生内存泄漏的位置就是tleak_open()中的kmem_zalloc()。进一步看一下,驱动程序都分配/释放了哪些内存
>
::walk kmem_log | ::bufctl ! grep tleak
ADDR
|
BUFADDR
|
TIMESTAMP
|
THREAD
|
CALLER
|
----------
|
-----------
|
-------------
|
-----------
|
-------------------
|
db2bebf8
|
d4db0380
|
a49a0fccba
|
d64db340
|
tleak_open+0x35
|
db0f0628
|
d4db0300
|
a1397b121b
|
d64db340
|
tleak_open+0x35
|
db0bc394
|
d51b3380
|
9f58e81dab
|
d64db340
|
tleak_open+0x35
|
可以看出tleak_open()被调用了三次,也就意味着分配了三次内存。(或者说,cat被运行了三次)
另外mdb的
::kmem_verify可以用来检测内存异常(如越界访问)。这时mdb提供了丰富的命令和宏,使用户可以方便地得到坏内存被哪些线程访问过。如:
> d4db0300::whatis
d4db0300 is d4db0300+0, bufctl d4ec7748 allocated from kmem_alloc_112
::bufctl -a用buffer地址过滤内存分配日志。该例中此内存仅被tleak_open()访问过
> ::walk kmem_log | ::bufctl -a d4db0300
ADDR BUFADDR TIMESTAMP THREAD CALLER
db0f0628 d4db0300 a1397b121b d64db340 tleak_open+0x35
::kgrep搜索对指定buffer的引用
> d4db0300::kgrep | ::whatis -a
db0f062c is dac43000+4ad62c (vmem_seg dac11168) from kmem_log vmem arena
db0f062c is dac43000+4ad62c (vmem_seg dac11258) from heap vmem arena
d4ec774c is d4ec7748+4, allocated from kmem_bufctl_audit_cache
d4ec774c is d4ec7000+74c (vmem_seg d4ea9ac8) from kmem_msb vmem arena
d4ec774c is d4ec7000+74c (vmem_seg d4ea9bb8) from kmem_metadata vmem arena
d4ec774c is d4ec4000+374c (vmem_seg d4ea6d20) from heap vmem arena
d504693c is d5046920+1c, allocated from kmem_magazine_7
d504693c is d5046000+93c (vmem_seg d4eb98e8) from kmem_msb vmem arena
d504693c is d5046000+93c (vmem_seg d4eb99d8) from kmem_metadata vmem arena
d504693c is d5044000+293c (vmem_seg d4eb66f8) from heap vmem arena
这是这个系列的最后一篇文章。本文介绍了在solaris中如何利用核心内存分配的调试功能检测内存异常(corruption)。
引起内存异常的常见操作包括:
- 越界访问
- 访问未被初始化的数据
- 访问已被释放的内存
我们用前一篇文章《solaris中如何检测内存泄漏(二)》中生成的核心core文件为例,一步步进行分析。
核心缓存(Kernel Memory Cache)首先回忆一下,为了发现内存泄漏运行mdb的::findleaks其输出为:
> ::findleaks
CACHE
|
LEAKED
|
BUFCTL
|
CALLER
|
... ...
|
dac32030
|
1
|
d4ec7748 |
tleak_open+0x35
|
第一列是发生了内存泄漏的cache地址。solaris的核心内存分配机制把内存分成若干cache。每一cache由一组固定大小的buffer组成。kmem_alloc(9F)或kmem_zalloc(9F)将从cache中获得所需内存。cache由数据结构kmem_cache_t (kmem_impl.h)定义。拿前文中的核心core文件做例子,用mdb的::kmastat命令看一下核心中有哪些cache。
> ::kmastat
cache name
|
buf size
|
buf in use
|
buf total
|
memory in use
|
alloc succeed
|
alloc fail
|
----------
|
------
|
-------
|
-------
|
--------
|
------
|
----
|
... ... ...
|
kmem_alloc_8
|
8
|
110939
|
111010
|
2674688
|
205353
|
0
|
kmem_alloc_16
|
16
|
59421
|
59520
|
1904640
|
91402
|
0
|
kmem_alloc_24
|
24
|
25723
|
25806
|
1036288
|
79258
|
0
|
kmem_alloc_32
|
32
|
10552
|
10625
|
512000
|
28811
|
0
|
kmem_alloc_40
|
40
|
4288
|
4380
|
245760
|
17876
|
0
|
kmem_alloc_48
|
48
|
52219
|
52224
|
3342336
|
64754
|
0
|
kmem_alloc_56
|
56
|
653
|
672
|
49152
|
4127
|
0
|
kmem_alloc_64
|
64
|
337
|
352
|
45056
|
47603
|
0
|
kmem_alloc_80
|
80
|
50732
|
50736
|
4947968
|
60466
|
0
|
kmem_alloc_96
|
96
|
120
|
144
|
16384
|
1122
|
0
|
kmem_alloc_112
|
112
|
163
|
192
|
24576
|
1363
|
0
|
... ... ...
|
其中,cache的名字kmem_alloc_后面的数字是该cache中buffer的大小。如kmem_alloc_8表示这个cache中的 buffer大小是8个字节。接下来我们用::kmem_cache命令简要查看上文中产生了内存泄漏的cache(也可以用宏$< kmem_cache打印数据结构kmem_cache_t)。
> dac32030::kmem_cache
ADDR
|
NAME
|
FLAG
|
CFLAG
|
BUFSIZE
|
BUFTOTL
|
dac32030
|
kmem_alloc_112
|
020f
|
200000
|
112
|
192
|
其中重要的字段是name、bufsize和flag。从name和bufsize可以看出缓冲大小是112字节。flag的值定义在 kmem_impl.h中。0x20f表示(KMF_HASH | KMF_AUDIT | KMF_DEADBEEF | KMF_REDZONE | KMF_CONTENTS)。
mdb的::walk freemem和::walk kmem可分别用来查看chane的空闲和被占用的缓冲。
> dac32030::walk freemem
d7c91980
d7c91900
d4db0280
d4f05900
d4f05880
... ...
> dac32030::walk kmem
d4db0000
d4db0080
d4db0100
d4db0180
... ...
空闲缓冲(0xdeadbeef)随便查看一个空闲缓冲的内容
> d7c91980/32X
0xd7c91980: |
deadbeef |
deadbeef |
deadbeef |
deadbeef |
|
deadbeef |
deadbeef |
deadbeef |
deadbeef |
|
deadbeef |
deadbeef |
deadbeef |
deadbeef |
|
deadbeef |
deadbeef |
deadbeef |
deadbeef |
|
deadbeef |
deadbeef |
deadbeef |
deadbeef |
|
deadbeef |
deadbeef |
deadbeef |
deadbeef |
|
deadbeef |
deadbeef |
deadbeef |
deadbeef |
|
deadbeef |
deadbeef |
deadbeef |
deadbeef |
|
feedface |
feedface |
d7c9dbf8 |
23272f16 |
缓冲的内容并不是0,而是0xdeedbeef。当缓冲被释放后,其内容会被清成0xdeedbeef。这样,用户就可以很容易地判断出访问的是否是一个已经被释放的内存。
已分配缓冲(0xbaddcafe)
再随便查看一个被占用的缓冲
> d4db0000/32X
0xd4db0000: |
0 |
0 |
0 |
0 |
|
0 |
0 |
0 |
0 |
|
0 |
0 |
0 |
0 |
|
0 |
0 |
d5077bc0 |
0 |
|
0 |
0 |
d20c2df8 |
0 |
|
d20c2dd8 |
d20c2dd8 |
d20c2e00 |
f5f00 |
|
0 |
0 |
baddcabb |
baddcafe |
|
feedface |
65f9 |
d4ec7a18
|
75fcb2f5 |
缓冲的内容被初始化成0xbaddcafe。根据这个特殊的0xbaddcafe,用户可以判断出是否访问了未被初始化的内存。
一个特殊字段“bb”紧跟在实际要求分配的内存的后面。注意上文中的“baddcabb”而不是“bbddcafe”,这是由于x86系统是little endian的系统造成的。
Redzone (0xfeedface)
空闲缓冲和被占用缓冲有一个共同字段0xfeedface。0xfeedface是Redzone的标志。它标识了一个buffer的边界。这里所说的边界和上文bb标识的边界不同。bb表示的是用户请求分配的内存边界,而0xfeedface表示的是整个buffer的边界。0xfeedface和bb 都可用来判断是否有内存越界访问。紧跟Redzone的是一些调试数据,这些数据和redzone一起统称为buftag区(如下图所示)。当一个 cache的KMF_AUDIT、KMF_DEADBEEF或KMF_REDZONE标志位被设,buftag区就会被加到这个cache的每一 buffer后面。