使用libumem定位memory leak和memory corruption(1)

时间:2022-11-21 10:42:27

今天新来的Manager让我最近给组里的同事分享一下最近我处理memory相关issue的经验。我想也好,干脆总结一下。先来介绍一个很基础但是很有用的工具libumem.so,libumem中的u指的是user space,是相对于kernal space来说的。读过linux的同学都知道linux kernal object内存管理的一个重要的概念就是slab。其实这个概念是solaris首先提出来的。

 

 

使用libumem配合mdb在 Solaris上进行内存问题的trouble shooting是非常有效的。但是有效的程度取决于经验,mdb是solaris的调试工具,非常强大,但是比较底层。最近开始读solaris internals这本书,觉得sun官方的mdb user guide不是很好,solaris internals反而更实用。libumem的核心思想就是把slab allocator的概念引入到user space来,使得用户态的程序也能借助kernal space中的多种技术帮助程序员发现定位问题。

 

简单说一下什么叫slab allocator,一个slab就是一类对象的cache,这个slab管理了多个buffer,其中每个buffer都能存放一个对象实例。当程序要一个新的该类对象的时候,slab就给程序一个已经初始化好的对象,当程序释放对象时,并不真正的释放,而是还给slab。slab最大的好处是效率,而且能够避免内存碎片,当然libumem中的每一个对象(内存对象)都有一些附属信息,方便调试。

在solaris中使用libumem非常方便,不需要重新编译代码,只要在启动程序设定LD_PRELOAD=libumem.so.1,系统就会把所有的malloc/free导向到libumem中,这一点在production中尤为重要,我们不可能要求在production环境中安装gcc或者gdb这样的工具的,或者想象一下你是在客户那里试图解决问题,难道还要带上源代码在客户的机器上编译不成。libumem.so是随着solaris安装的,每一台solaris 9以上版本的机器上都有。

 

就内存诊断工具我们常用的还有purify,purify也很强大,但是使用它需要重新编译代码。而且purify很慢。而libumem能和mdb配合使用。当然真正碰到问题,我常常是把手头所有的工具都用上-:)

 

网上比较好的文档有两篇,一是Sun的官方文档http://access1.sun.com/techarticles/libumem.html ,不过这个网址有时候不太稳定。还有一个是Judy的Bloghttp://developers.sun.com.cn/blog/judy/entry/200703091。这里我主要翻译一下Sun的官方文档,再结合一个Judy的文章。我希望我写的这篇文章能起到Hands On的效果,因为常常官方文档太学究,很多根本不是写给初学者看的,或者嘛翻译过来的东西不是出自技术人员之手,看的人中文是看懂了,看完了还是不知道什么意思。

 

 

先看这样一段很简单的代码Test.cpp:

int main()
{
    int *p = (int*)malloc(50);
    p = (int*)malloc(100);
    free(p);

    pause();
    return 0;
}

-bash-3.00$ g++ -o Test Test.cpp   //编译Test.cpp
-bash-3.00$ UMEM_DEBUG=default UMEM_LOGGING=transaction LD_PRELOAD=libumem.so.1 ./Test &
[1] 5503

UMEM_DEBUG=default UMEM_LOGGING=transaction LD_PRELOAD=libumem.so.1是送3个环境变量给Test。具体的解释我先不解释,可以使用 man umem_debug查看。

-bash-3.00$ mdb -p 5503   //使用mdb attach到Test进程,进程ID=5503
Loading modules: [ ld.so.1 libumem.so.1 libc.so.1 ]
>

> ::umalog //使用umalog打印所有的内存分配释放记录

T-0.000000000  addr=80a0f80  umem_alloc_112
         libumem.so.1`umem_cache_free_debug+0×135
         libumem.so.1`umem_cache_free+0x3f
         libumem.so.1`umem_free+0xd5
         libumem.so.1`process_free+0xfd
         libumem.so.1`free+0×14
         main+0×47
         _start+0×80

T-0.000003228  addr=80a0f80  umem_alloc_112
         libumem.so.1`umem_cache_alloc_debug+0x16c
         libumem.so.1`umem_cache_alloc+0x15c
         libumem.so.1`umem_alloc+0x3f
         libumem.so.1`malloc+0×23
         main+0×36
         _start+0×80

T-0.000067020  addr=809cf80  umem_alloc_64
         libumem.so.1`umem_cache_alloc_debug+0x16c
         libumem.so.1`umem_cache_alloc+0x15c
         libumem.so.1`umem_alloc+0x3f
         libumem.so.1`malloc+0×23
         main+0×26
         _start+0×80

 

 

可以看到,Test程序一共有两次内存分配,1次内存释放。log是按照时间顺序的,排在前面的记录是最近的内存操作。umem_alloc_112是cache的名字,umem_alloc_112是专门用来管理size=112字节的内存的。addr=80a0f80表示内存buffer的起始地址。umalog还包含了调用栈,这个信息很有用。其实在实际使用中直接输入::umalog你会头昏的,因为一般有一点规模的程序会有非常多的内存操作,你就像大海捞针,所以我一般会这样:

> ::umalog ! less   // ! 的作用就是管道,意思是把umalog的结果输出给less,这样我就可以在mdb中使用less的功能进行查找翻页

在打印log方面还可以这样

> ::umem_log
CPU ADDR     BUFADDR      TIMESTAMP THREAD
  1 0806f0c8 080a0f80    c6bb095680bec 00000001
  1 0806f064 080a0f80    c6bb09567ff50  00000001
  1 0806f000 0809cf80    c6bb095670620 00000001

 

 

这样也打印了内存操作,CPU=1表示代码运行在CPU1上,ADDR其实是LOG的地址,我们先不关心。BUFADDR和::umalog中的addr是吻合的,THREAD表示该内存操作在程序的哪个线程中被调用的,这个信息对于分析多线程并发情况下的内存corruption有用。但是和::umalog一样,这样打印信息实在太多。比如我想知道曾经访问过addr=80a0f80这块buffer的记录有哪些:

> ::umem_log ! grep 80a0f80
  1 0806f0c8 080a0f80    c6bb095680bec 00000001
  1 0806f064 080a0f80    c6bb09567ff50 00000001

 

 

前面说到每一块buffer都有一些meta信息用于管理或者debug。

 

 

> 080a0f80::whatis
80a0f80 is 80a0f80+0, bufctl 80a1100 freed from umem_alloc_112

通过whatis,我要求mdb告诉我0x080a0f80所在的buffer是什么样子的buffer,于是mdb告诉我,这块buffer的起始地址是0x080a0f80,管理这块buffer的控制结构所在的地址是0x80a1100,发生在这块buffer上最近的操作是free,用于管理这一类buffer的cache的名字是umem_alloc_112。

> 80a1100::bufctl      //已知buffer ctl structure的地址,要求打印bufctl的信息
ADDR     BUFADDR  TIMESTAMP     THRD CALLER
080a1100 080a0f80 c6bb095680bec     1 libumem.so.1`process_free+0xfd

如果要更详细的信息

> 80a1100::bufctl_audit
            ADDR          BUFADDR        TIMESTAMP           THREAD
                             CACHE           LASTLOG           CONTENTS
         80a1100          80a0f80     c6bb095680bec                1
                              8090710          806f0c8                     0
                 libumem.so.1`umem_cache_free_debug+0×135
                 libumem.so.1`umem_cache_free+0x3f
                 libumem.so.1`umem_free+0xd5
                 libumem.so.1`process_free+0xfd
                 libumem.so.1`free+0×14
                 main+0×47
                 _start+0×80

 

 

可见ADDR=80a1100是bufctl_audit的起始地址,BUFFADDR=80a0f80是buffer的起始地址,CACHE=8090710是cache的起始地址,已知buffer addr可以通过whatis获取bufctl的地址,已知bufctl可以通过bufctl或者bufctl_audit获取buffer的地址。

 

 

> 8090710::umem_cache //已知cache的指针,打印cache的数据结构,BUFTOTL=32表示该cache一共管理32块buffer。
ADDR     NAME                      FLAG    CFLAG  BUFSIZE  BUFTOTL
08090710 umem_alloc_112      020f   80000000      112       32

所以我们推断,buffer的meta信息中肯定有指向bufctl的指针,bufctl中一定有指向buffer的指针,bufctl_audit里面一定含有cache的指针,call stack的指针…下一篇文章我来详细的介绍这些数据结构。