LinuxSir.cn,穿越时空的Linuxsir!

 找回密码
 注册
搜索
热搜: shell linux mysql
查看: 2586|回复: 2

Solaris中如何检测内存泄漏 zz

[复制链接]
发表于 2006-7-11 00:07:42 | 显示全部楼层 |阅读模式
转自:http://blog.gceclub.sun.com.cn/i ... 1099&blogId=640

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

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

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

/* memleak.c */
#include <stdio.h>
#include <stdlib.h>
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”也被覆盖了。
 楼主| 发表于 2006-7-11 00:09:20 | 显示全部楼层
本文介绍如何利用mdb的::findleaks检测内核代码的内存泄漏。

同前一篇文章一样,本文将以一个驱动程序(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]> $<systemdump
注意,"mdb -K"须在控制台上才能运行。另外,在控制台或终端运行"reboot -d"也可以让核心coredump。

等机器重新启动后,用mdb调试上一步生成的核心core文件
$ cd /var/crash/<hostname>/
$ 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 "<unknown>" 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$<bufctl_audit
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
回复 支持 反对

使用道具 举报

 楼主| 发表于 2006-7-11 00:11:11 | 显示全部楼层
这是这个系列的最后一篇文章。本文介绍了在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后面。

|<------------------------ buffer ----------------------->|<---------- buftag ---------->|
User Data                         bb                        Unallocated         RedZone                       Debugging Data              
|<------------------- cache_bufsize字节 ------------------->|<--- 64位 ---->|<--- 2个指针 -->|

RedZone:                       0xfeedface                       encoded index
                                   User data size = encoded_index / 251 字节

Debugging Data:            bcp 指针                              bxstat 指针
                                   bcp pointer ^ bxstat pointer = a110c8ed | f4eef4ee

以kmem_alloc_8中的一段内存为例打印其内容

> dec82b18,6/2Xna
0xdec82b18:
        75746572
        bb006e72
        -- User Data
0xdec82b20:
        feedface
        6de
        -- RedZone
0xdec82b28:
        decd3150
        7fddf9bd
        -- Debugging Data

       
       
       
0xdec82b30:
        73666e
        baddcabb
        -- User Data
0xdec82b38:
        feedface
        3ed
        -- RedZone
0xdec82b40:
        decd30d8
        7fddf835
        -- Debugging Data

RedZone的0xfeedface后面是经过编码的用户实际使用的缓冲大小,其计算方法是:

size = redzone_value / 251

则在上述例子中

size = 0x6de / 251 = 7 字节

注意,x86系统是little endian的。


bufctl 指针

Debugging Data中的两个指针,前一个是指向bufctl的bcp指针,后一个是bxstat指针。bxstat用于校验bcp指针的有效性。对于以分配的缓冲, bcp XOR bxstat = 0xa110c8ed(allocated);而对于已释放的缓冲,bcp XOR bxstat = 0xf4eef4ee(freefree)。同样在上面的例子中

decd3150 ^ 7fddf9bd = a110c8ed

当kmem_flags的KMF_AUDIT位被设置后,bcp指针指向一个kmem_bufctl_audit_t结构。该结构包含使该缓冲在 allocated和freed状态之间转换的操作的详细信息。

>decd3150$<bufctl_audit
ADDR
        BUFADDR
        TIMESTAMP
        THREAD

        CACHE
        LASTLOG
        CONTENTS
decd3150
        dec82b18
        8f2a260f73
        d5079980

        dac2c030
        db00cfc0
        0

        kmem_cache_alloc_debug+0x256

        kmem_cache_alloc+0x1ac

        kmem_zalloc+0x4b

        dtrace_strdup+0x21

        dtrace_probe_create+0x99

        fbt_provide_module+0x306

        dtrace_probe_provide+0x61

        dtrace_open+0x5d

        dev_open+0x27

        spec_open+0x3cc

        fop_open+0x6e

        vn_openat+0x42a

        copen+0x287

        open+0x1b



最后总结一下:

    * 从buffer所在cache可以获得buffer大小等信息(::kmem_cache, $<kmem_cache)
    * 0xdeadbeef是已释放缓冲的标志,可以帮助判断是否访问了已释放内存
    * 0xbaddcafe是缓冲的初始内容,可以帮助判断是否访问了未被初始化的数据
    * bb标识了缓冲区的边界,0xfeedface则是redzone的起始标志,它们都可用来判断是否有越界访问
    * bufctl指针中记录的事务信息,有助于定位引起内存异常的代码
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 返回顶部 返回列表