Chinaunix首页 | 论坛 | 博客
  • 博客访问: 46755
  • 博文数量: 3
  • 博客积分: 850
  • 博客等级: 准尉
  • 技术积分: 210
  • 用 户 组: 普通用户
  • 注册时间: 2005-03-19 16:44
文章分类

全部博文(3)

文章存档

2009年(3)

我的朋友
最近访客

分类:

2009-02-26 10:35:10

在Solaris中如何监测应用程序和核心代码(驱动程序)是否存在内存泄漏?
内存泄漏(Memory Leak)通常是由应用程序没有释放其在堆(heap)上分配的内存而造成的。
对于由应用程序引起的内存泄漏,我们可以借助libumem来方便地进行调试。libumem是一个运行在用户模式的内存分配程序库。并已包含在 Solaris 9及以后的Solaris版本中。
对于内核代码,则可以利用Solaris调试工具mdb提供的"::findleaks"命令来进行检测。

一 利用libumem检测应用程序的内存泄漏

本文将以一个示例简单说明使用libumem的步骤。示例程序memleak.c如下:

/* memleak.c */
#include
#include
int main(void)
{
    char *p;
    p = malloc(50);
    p = malloc(100);
    while (1) {
        sleep(1);
    }
}

这个程序存在明显的内存泄漏。先用gcc编译该程序
$ /usr/sfw/bin/gcc -o memleak memleak.c

使能libumem,检测内存泄漏
$ export LD_PRELOAD=libumem.so
$ export UMEM_DEBUG=default
$ export UMEM_LOGGING=transaction
$ ./memleak &
[1] 1121
$ gcore 1121
gcore: core.1121 dumped
$ mdb core.1121
Loading modules: [ libumem.so.1 libc.so.1 ld.so.1 ]
> ::findleaks            -> 检测应用程序中发现的内存泄漏
CACHE LEAKED BUFCTL CALLER
0807c610 1 08088080 main+0x26
------------------------------------------------------------------------
Total 1 buffer, 64 bytes
> 08088080::bufctl_audit -> 打印最后对该内存进行操作的调用栈
ADDR    BUFADDR TIMESTAMP   THREAD
CACHE   LASTLOG CONTENTS
8088080 8086f80 2b73ee921ea 1
807c610 806b000 0
         libumem.so.1`umem_cache_alloc_debug+0x14f
         libumem.so.1`umem_cache_alloc+0x180
         libumem.so.1`umem_alloc+0xcc
         libumem.so.1`malloc+0x27
         main+0x26
         _start+0x80

进一步检查内存分配日志如下。就可以看出内存泄漏的原因是应用程序调用了两次malloc(),但没有调用free()。
> ::umalog

T-0.000000000 addr=8089f80 umem_alloc_112
         libumem.so.1`umem_cache_alloc_debug+0x14f
         libumem.so.1`umem_cache_alloc+0x180
         libumem.so.1`umem_alloc+0xcc
         libumem.so.1`malloc+0x27
         main+0x36
         _start+0x80

T-0.000052036 addr=8086f80 umem_alloc_64
         libumem.so.1`umem_cache_alloc_debug+0x14f
         libumem.so.1`umem_cache_alloc+0x180
         libumem.so.1`umem_alloc+0xcc
         libumem.so.1`malloc+0x27
         main+0x26
         _start+0x80
除了检测内存泄漏外,libumem还可用于检测内存异常,如内存越界访问。简单修改memleak.c,在第8行
         p = malloc(50);
后加入如下语句:
         memset(p, 60, 0);

重新编译、运行memleak,并生成core文件。
$ mdb core.1168
Loading modules: [ libumem.so.1 libc.so.1 ld.so.1 ]
> ::umem_verify         -> 检测是否存在内存异常
Cache Name Addr    Cache Integrity
umem_magazine_1    8077010 clean
umem_magazine_3    8077210 clean
umem_magazine_7    8077410 clean
umem_magazine_15   8077610 clean
umem_magazine_31   8077810 clean
umem_magazine_47   8077a10 clean
umem_magazine_63   8077c10 clean
umem_magazine_95   8077e10 clean
umem_magazine_143  8079010 clean
umem_slab_cache    8079210 clean
umem_bufctl_cache  8079410 clean
umem_bufctl_audit_cache 8079610 clean
umem_alloc_8       8079810 clean
umem_alloc_16      8079a10 clean
umem_alloc_24      8079c10 clean
umem_alloc_32      8079e10 clean
umem_alloc_40      807c010 clean
umem_alloc_48      807c210 clean
umem_alloc_56      807c410 clean
umem_alloc_64      807c610 1 corrupt buffer -> 发现内存异常
umem_alloc_80      807c810 clean
umem_alloc_96      807ca10 clean
> 807c610::umem_verify
Summary for cache 'umem_alloc_64'
buffer 8086f80 (allocated) has a corrupt redzone size encoding
> 8086f80::whatis       -> 找到bufctl指针
8086f80 is 8086f80+0, bufctl 8088080 allocated from umem_alloc_64
> 8088080::bufctl_audit -> 打印调用栈以确定异常所在的内存块
ADDR BUFADDR TIMESTAMP THREAD
CACHE LASTLOG CONTENTS
8088080 8086f80 422098b4141 1
807c610 806b000 0
         libumem.so.1`umem_cache_alloc_debug+0x14f
         libumem.so.1`umem_cache_alloc+0x180
         libumem.so.1`umem_alloc+0xcc
         libumem.so.1`malloc+0x27
         main+0x26
         _start+0x80
> 8086f80/20X           -> 打印buffer内容
0x8086f80: 3a 3a10bfc6 0       0
           0  0        0       0
           0  0        0       0
           0  0        0       0
           0  38df     8088080 a918486d
可以看出内存中有60个字节被设成了0,而该内存实际上只分配了50个有效字节。并且由于内存越界访问,redzone的标志字节“feedface”也被覆盖了。
 
 

同前一篇文章一样,本文将以一个驱动程序(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后面。
阅读(1215) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~