malloc崩溃
1. 概述
最近 merger 偶发 core dump,位置不定,但有一定规律:
- 一般是在 RequestManager::MergeResult() 函数或其前后出现。
- core dump 前超时较多。
core dump 的直接原因,一般是 malloc 或 free函数检测到其内部链表结构被破坏后,报错并主动 abort 进程。
这类问题一般是越界读写内存或者线程不安全导致的。
尝试过使用 valgrind 或者 tcmalloc 检测内存越界,以及代码 review,但都没有解决问题。
进一步分析 core 文件之后,定位到了问题原因。大体步骤如下:
- 安装 glibc 的调试信息,使我们能够查看 malloc 或者 free 函数内部的变量
- 了解 ptmalloc2 数据结构,找出被破坏的数据(的内存地址)
- 通过查看每个线程的寄存器,找出哪个线程在往上一步找到的地址上写数据
最终我们发现有一个线程在向一个已被释放的对象上写数据。
根据此线程的调用栈,就能很快定位到有问题的代码(线程不安全)。
下面是详细分析过程,供以后排查类似问题时参考。
2. core 文件分析
2.1. glibc的日志
merger core dump之后,一般可以在nohup.out里找出glibc输出的信息,如:
*** glibc detected *** /opt/esearch/0/version-138/merger05/package/merger: free(): corrupted unsorted chunks: 0x00007fb8f4009520 ***
这行日志表示当程序在释放一个值为 0x00007fb8f4009520 的指针时,free()函数检测到 ptmalloc2 内部的 unsorted chunks 数据结构已被破坏。
单纯这行日志对我们定位原因帮助不大,因为具体哪个地址上的数据被破坏,它并未给出来。
2.2. 准备调试
打开core文件查看调用栈时,会输出以下内容:
(gdb) bt
#0 0x0000003fa8032925 in raise () from /lib64/libc.so.6
#1 0x0000003fa8034105 in abort () from /lib64/libc.so.6
#2 0x0000003fa8070837 in __libc_message () from /lib64/libc.so.6
#3 0x0000003fa8076166 in malloc_printerr () from /lib64/libc.so.6
#4 0x0000003fa8078c93 in _int_free () from /lib64/libc.so.6
#5 0x00007fb94bb1a394 in deallocate (__p=<optimized out>, this=<optimized out>)
at /usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../include/c++/4.1.2/ext/new_allocator.h:94
#6 _M_deallocate (__n=<optimized out>, __p=<optimized out>, this=<optimized out>)
at /usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../include/c++/4.1.2/bits/stl_vector.h:133
#7 ~_Vector_base (this=<optimized out>, __in_chrg=<optimized out>) at /usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../include/c++/4.1.2/bits/stl_vector.h:119
#8 ~vector (this=<optimized out>, __in_chrg=<optimized out>) at /usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../include/c++/4.1.2/bits/stl_vector.h:272
#9 netter::RequestManager::~RequestManager (this=0x7fb85c8cd9e0, __in_chrg=<optimized out>) at build/release64/netter/arbiter/request_manager.cpp:36
#10 0x00007fb94bb16c30 in netter::RequestManager::TimeCheck (this=0x7fb85c8cd9e0, now=1429684563654, toDele=@0x7fb93d679e57: true)
at build/release64/netter/arbiter/request_manager.cpp:132
...
可以看出来,_int_free() 函数在执行过程中检测到了错误,调用 malloc_printerr() 打印错误并终止进程。
为了定位错误数据的位置,需要跟踪进 _int_free() 这个函数去分析。但机器上一般没有装 glibc 的调试信息,看不到 _int_free() 的源码。
gdb 加载 core 文件时会给出提示,告诉你该用什么命令、安装哪些调试信息:
Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.132.el6.x86_64 libgcc-4.4.7-4.el6.x86_64 libstdc++-4.4.7-4.el6.x86_64 zlib-1.2.3-29.el6.x86_64
如果没有 root 权限,则无法执行这个命令。变通的方法见[_安装glibc调试信息]。
2.3. 开始调试
安装了调试信息后,gdb给出的调用栈如下:
(gdb) bt
#0 0x0000003fa8032925 in *__GI_raise (sig=6) at ../nptl/sysdeps/unix/sysv/linux/raise.c:64
#1 0x0000003fa8034105 in *__GI_abort () at abort.c:92
#2 0x0000003fa8070837 in __libc_message (do_abort=2, fmt=0x3fa8158ac0 "*** glibc detected *** %s: %s: 0x%s ***\n") at ../sysdeps/unix/sysv/linux/libc_fatal.c:198
#3 0x0000003fa8076166 in malloc_printerr (action=3, str=0x3fa8158e48 "free(): corrupted unsorted chunks", ptr=<optimized out>) at malloc.c:6332
#4 0x0000003fa8078c93 in _int_free (av=0x7fb8f4000020, p=0x7fb8f4009510, have_lock=0) at malloc.c:4832
#5 0x00007fb94bb1a394 in deallocate (__p=<optimized out>, this=<optimized out>)
at /usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../include/c++/4.1.2/ext/new_allocator.h:94
#6 _M_deallocate (__n=<optimized out>, __p=<optimized out>, this=<optimized out>) at /usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../include/c++/4.1.2/bits/stl_vector.h:133
#7 ~_Vector_base (this=<optimized out>, __in_chrg=<optimized out>) at /usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../include/c++/4.1.2/bits/stl_vector.h:119
#8 ~vector (this=<optimized out>, __in_chrg=<optimized out>) at /usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../include/c++/4.1.2/bits/stl_vector.h:272
#9 netter::RequestManager::~RequestManager (this=0x7fb85c8cd9e0, __in_chrg=<optimized out>) at build/release64/netter/arbiter/request_manager.cpp:36
#10 0x00007fb94bb16c30 in netter::RequestManager::TimeCheck (this=0x7fb85c8cd9e0, now=1429684563654, toDele=@0x7fb93d679e57: true) at build/release64/netter/arbiter/request_manager.cpp:132
...
来看看 _int_free() 中 4832 行的代码:
4819 /* Little security check which won't hurt performance: the 4820 allocator never wrapps around at the end of the address space. 4821 Therefore we can exclude some size values which might appear 4822 here by accident or by "design" from some intruder. */ 4823 if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0) 4824 || __builtin_expect (misaligned_chunk (p), 0)) 4825 { 4826 errstr = "free(): invalid pointer"; 4827 errout: 4828 #ifdef ATOMIC_FASTBINS 4829 if (have_lock || locked) 4830 (void)mutex_unlock(&av->mutex); 4831 #endif 4832 malloc_printerr (check_action, errstr, chunk2mem(p)); 4833 #ifdef ATOMIC_FASTBINS 4834 if (have_lock) 4835 mutex_lock(&av->mutex); 4836 #endif 4837 return; 4838 }
结合整个 _int_free() 函数的源码分析后发现,这里是 _int_free() 函数中通用的错误处理逻辑,而不是实际检测到错误的地方。
_int_free() 中有多处错误检测,一旦检测出错误之后,就会设置 errstr 的值然后 goto errout。
从调用栈中可以看到 errstr 的值(即 malloc_printerr() 的参数 str)实为”free(): corrupted unsorted chunks”,据此可以在源码中找到实际报错的地方:
5019 /* 5020 Place the chunk in unsorted chunk list. Chunks are 5021 not placed into regular bins until after they have 5022 been given one chance to be used in malloc. 5023 */ 5024 5025 bck = unsorted_chunks(av); 5026 fwd = bck->fd; 5027 if (__builtin_expect (fwd->bk != bck, 0)) 5028 { 5029 errstr = "free(): corrupted unsorted chunks"; 5030 goto errout; 5031 }
可见,错误的原因是:
fwd->bk != bck
其中,
- fwd 和 bck 为指向 struct malloc_chunk 的指针,这个类型用于表示单个内存块。
- av 为指向 struct mstate 的指针,这个类型用于管理内存块。
这块代码的含义:
- 经过大小判断、安全检测后,_int_free()决定把释放的内存块放入一个称为 unsorted chunks 的双向链表中。
- 从 av 中取出 unsorted chunks 双向链表,发现链表头部的前后两个节点衔接不上(后一个节点 fwd 的 bk 指针没有指向前一个节点 bck)。
变量具体的值为:
bck = (struct malloc_chunk *) 0x7fb8f4000078 fwd = (struct malloc_chunk *) 0x7fb8f40010c0 *fwd = { prev_size = 140432361410256, size = 449, fd = (struct malloc_chunk *) 0x7fb8f6275380, bk = (struct malloc_chunk *) 0x7fb800000002, fd_nextsize = 0x0, bk_nextsize = 0xffffffff }
(实际上 fwd 和 bck 都已被优化掉了,无法直接在 gdb 中查看它们的值。取回它们的值的方法,见[_查看被优化后的变量值])
更深层的原因可能是:
- fwd→bk 被篡改
- fwd 被篡改,所以 fwd→bk 也不正确
- bck 被篡改
首先可以排除(3),了解 unsorted_chunks 的逻辑之后可知,bck 指针是按一定规则通过 av 指针计算出来的,只要 av 指针正确,bck 就正确。
观察 av 指向的对象,其每个成员的值都是有意义的,比如其 bins 数组里每个指针都指向一个有效的 chunk,基本上可以据此判断av指向的是一个有效的对象,应未被篡改。
然后也可以排除(2),因为 fwd 指向的是一个有效的 chunk,因此 fwd 指针也不太可能已被篡改。
上面两次提到了“指向一个有效的chunk”,关于如何判断一个 chunk 有效,见[_chunk_的特性]。
2.4. 跟踪 fwd→bk
尝试访问 fwd→bk 指向的对象,发现不可访问:
(gdb) p *(struct malloc_chunk *) 0x7fb800000002
Cannot access memory at address 0x7fb800000002
而 fwd→fd 指向的对象是可访问的,并且也是一个有效的 chunk。这说明 fwd→fd 的值是正常的。
fwd 对象的内存布局如下:
(gdb) x /10wx 0x7fb8f40010c0
0x7fb8f40010c0: 0xf5044ed0 0x00007fb8 0x000001c1 0x00000000
|<----- prev_size ------>| |<----- size ----->|
0x7fb8f40010d0: 0xf6275380 0x00007fb8 0x00000002 0x00007fb8
|<----- fd ------>| |<----- bk ----->|
0x7fb8f40010e0: 0x00000000 0x00000000
可以发现,bk 的高4个字节 0x00007fb8 与 fd 的高4个字节相同,应为正常值。
唯独其低四个字节 0x00000002 (地址为 0x7fb8f40010d8) 比较异常,不像指针,而像计数或者某种表示状态的值,故怀疑程序中有某些地方修改了这个位置的值。
程序要读写某个内存地址中的值时,一般会先将其地址装入寄存器中。
不过,在访问对象成员的时候可能会用”对象地址(寄存器)+成员偏移量(立即数)”的寻址方式。而在访问数组元素的时候可能用”数组首地址(寄存器)+偏移量(寄存器) * 元素大小(立即数)”的寻址方式。这两种方式下,寄存器里的地址与实际被访问的地址有或大或小的偏差。
不论如何,我们先尝试观察每个线程的寄存器,看看是否有与 0x7fb8f40010d8 相等或相近的值。
幸运的是,我们在一个线程里发现有一个寄存器 r8 的值恰好等于 0x7fb8f40010d8:
(gdb) i registers
...
rsp 0x7fb93ea7bad0 0x7fb93ea7bad0
r8 0x7fb8f40010d8 140432344355032
r9 0x550b 21771
...
该线程调用栈如下:
(gdb) bt
#0 __lll_lock_wait () at ../nptl/sysdeps/unix/sysv/linux/x86_64/lowlevellock.S:136
#1 0x0000003fa8409508 in _L_lock_854 () from /lib64/libpthread.so.0
#2 0x0000003fa84093d7 in __pthread_mutex_lock (mutex=0x7fb8f40010d8) at pthread_mutex_lock.c:61
#3 0x00007fb94bb16d54 in Lock (this=<optimized out>) at /home/esearchQa/sourceCode/trunk/sharelib/build/release64/sdk/sharelib_sdk-0.1.0-release64/include/sharelib/util/lock.h:22
#4 netter::RequestManager::HandleSubRequest (this=0x7fb8f40010d0, partitionId=...) at build/release64/netter/arbiter/request_manager.cpp:86
#5 0x00007fb94bb04232 in netter::ArbiterTcpSubRequest::handlePacket (this=0x7fb8f4006ef0, packet=0x7fb905006280, args=<optimized out>) at build/release64/netter/arbiter/arbiter_tcp_sub_request.cpp:47
#6 0x00007fb94c4a07fb in enet::Connection::handlePacket (this=0x7fb9280467e0, packet=0x7fb905006280) at build/release64/enet/connection.cpp:194
...
可以看到 #2 的参数 mutex 正好是我们要找的地址,而 #4 的参数 this 恰好是 fwd 这个 chunk 的 user memory 地址。
于是我们推断程序执行过程如下:
- 程序中申请了一个 RequestManager 对象,地址为 0x7fb8f40010d0
- 某个时刻,程序删除了这个对象。ptmalloc2 把这块内存对应的 chunk 放入它的 unsorted chunks 链表中,把 chunk 的 fd 和 bk 指针都设为有效的值。
- 但仍有一个线程在对这个对象进行修改(对这个对象里的一个互斥量进行加锁),从而覆盖了bk指针的一部分。
- 另一个线程(即报错的线程)删除另一个 RequestManager 对象,此时正在执行 RequestManager 的析构函数,尝试释放其中一个 vector 型成员的内存。
- ptmalloc2 尝试把这块内存放入它的 unsorted chunks 链表中,发现链表第一个节点的 bk 指针有问题,于是报错并终止了程序。
随后我们检查了 RequestManager 在什么条件下可能被删除,发现 RequestManager::HandleSubRequest() 函数在修改了 RequestManager 某个成员之后继续对该对象进行加锁以及后续操作,而该成员的改变可能导致超时检测线程删除 RequestManager 对象。
我们尝试在”修改 RequestManager 某个成员”和”继续对该对象进行加锁”这两个步骤间插入了3秒的sleep时间,重新编译并测试,每次运行单测必然会异常终止,于是基本确认了问题。
2.4.1. 内存布局示意图
+-----------------------------------+ <- fwd (0x7fb8f40010c0)
| prev_size |
+-----------------------------------+
| size |N|M|P|
+-----------------------------------+ <- mem -> +-----------------------------------+ <- RequestManager
| fd | | vtable | (0x7fb8f40010d0)
+-----------------------------------+ +-----------------------------------+ <- 0x7fb8f40010d8
| bk | | int __lock | unsigned __count |\
+-----------------------------------+ |----------------+------------------+ \
| | | int __owner | unsigned __nusers| \
| unused memory | |----------------+------------------+ | mutexForEnqueue
| | | ... | ... | /
+-----------------------------------+ +-----------------------------------+ /
当程序尝试对已释放的 RequestManager 对象加锁时,会把 __lock 设置为 2,也就导致 bk 的低四个字节被覆盖。
3. 附录
3.1. 安装glibc调试信息
3.1.1. 下载两个rpm包,放到 ~/glibc 目录下:
http://debuginfo.centos.org/6/x86_64/glibc-debuginfo-common-2.12-1.132.el6.x86_64.rpm
http://debuginfo.centos.org/6/x86_64/glibc-debuginfo-2.12-1.132.el6.x86_64.rpm
3.1.2. 安装到 ~/local 目录下:
mkdir -p ~/local
cd ~/local
rpm2cpio ~/glibc/glibc-debuginfo-common-2.12-1.132.el6.x86_64.rpm | cpio -idmv
rpm2cpio ~/glibc/glibc-debuginfo-2.12-1.132.el6.x86_64.rpm | cpio -idmv
3.1.3. 修改gdb配置
在文件 ~/.gdbinit 中增加两行,告诉gdb从哪里找到调试符号和源码文件(假设用户名为work):
set debug-file-directory /home/work/local/usr/lib/debug
dir ~/local/usr/src/debug/glibc-2.12-2-gc4ccff1/malloc
参考:
3.2. 查看被优化后的变量值
3.2.1. 查看寄存器
在gdb中,如果遇到被优化掉的变量,无法用print命令查看其值,可以首先尝试查看变量所在的寄存器。
首先用以下命令,输出当前函数的反汇编代码:
disassemble /m
输出结果可能很长,为了便于查看,可以让 gdb 输出到文件里(例如输出到 _int_free.c):
set logging file _int_free.c
set logging on
set pagination off
disassemble /m
输出结果如下(仅截取相关的一小段)
5019 /* 5020 Place the chunk in unsorted chunk list. Chunks are 5021 not placed into regular bins until after they have 5022 been given one chance to be used in malloc. 5023 */ 5024 5025 bck = unsorted_chunks(av); 0x0000003fa8078815 <+549>: lea 0x58(%r13),%rax 5026 fwd = bck->fd; 0x0000003fa8078819 <+553>: mov 0x10(%rax),%rdx 5027 if (__builtin_expect (fwd->bk != bck, 0)) 0x0000003fa807881d <+557>: cmp 0x18(%rdx),%rax 0x0000003fa8078821 <+561>: jne 0x3fa8078c58 <_int_free+1640> 0x0000003fa8078c58 <+1640>: lea 0xe01e9(%rip),%rsi # 0x3fa8158e48 5028 { 5029 errstr = "free(): corrupted unsorted chunks"; 5030 goto errout; 5031 }
可知 bck 寄存器为 rax,fwd 寄存器为 rdx。
查看其值:
(gdb) p $rax
$2 = 0
(gdb) p $rdx
$3 = 6
两个寄存器的值都不像是正常的指针,应该是被别的值覆盖了。
3.2.2. 重新计算
查看寄存器的方法行不通,再尝试另一种方法:
bck 和 fwd 的值都是从 av 这个变量里取出来的,av 的值没有被优化掉,因此我们可以在 gdb 中模拟 unsorted_chunks 的逻辑,重新计算出 bck 和 fwd。
unsorted_chunks 在源码中是一个宏:
2099 /* addressing -- note that bin_at(0) does not exist */ 2100 #define bin_at(m, i) \ 2101 (mbinptr) (((char *) &((m)->bins[((i) - 1) * 2])) \ 2102 - offsetof (struct malloc_chunk, fd)) ...... 2201 /* 2202 Unsorted chunks 2203 2204 All remainders from chunk splits, as well as all returned chunks, 2205 are first placed in the "unsorted" bin. They are then placed 2206 in regular bins after malloc gives them ONE chance to be used before 2207 binning. So, basically, the unsorted_chunks list acts as a queue, 2208 with chunks being placed on it in free (and malloc_consolidate), 2209 and taken off (to be either used or placed in bins) in malloc. 2210 2211 The NON_MAIN_ARENA flag is never set for unsorted chunks, so it 2212 does not have to be taken into account in size comparisons. 2213 */ 2214 2215 /* The otherwise unindexable 1-bin is used to hold unsorted chunks. */ 2216 #define unsorted_chunks(M) (bin_at(M, 1))
我们可以写一些 gdb 命令模拟这个宏:
define bin_at
set $retval = (struct malloc_chunk*) (((char *) &($arg0->bins[(($arg1) - 1) * 2])) - (int)&((struct malloc_chunk*)0)->fd)
p $retval
end
define unsorted_chunk
bin_at $arg0 1
end
这些命令可以写在当前目录的 .gdbinit 文件里,当 gdb 启动时会自动加载它。
执行命令后,即可得到 bck 和 fwd 的值:
(gdb) unsorted_chunk av
$1 = (struct malloc_chunk *) 0x7fb8f4000078
(gdb) p $1->fd
$2 = (struct malloc_chunk *) 0x7fb8f40010c0
3.3. chunk 的特性
根据内存块大小的不同,ptmalloc2 会用不同的策略来进行管理,但都统一使用 struct malloc_chunk 这种结构来描述内存块。
这里不详述具体的管理策略,仅分析 chunk 的一些特性。
chunk 主要包含了指向前后节点的两个指针,以便构成双向链表,以及供用户使用的内存。
具体定义:
struct malloc_chunk { INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */ INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */ struct malloc_chunk* fd; /* double links -- used only if free. */ struct malloc_chunk* bk; /* Only used for large blocks: pointer to next larger size. */ struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */ struct malloc_chunk* bk_nextsize; };
- prev_size: 前一个chunk的大小
- 前一个 chunk 指的是内存地址上相邻的前一个 chunk,并非链表中的上一个节点。
- 有了这个 prev_size,可以快速定位到前一个 chunk 的头部,从而便于合并内存地址相邻的两个 chunk。
- 若前一个 chunk 已被分配出去,那么这个 chunk 的 prev_size 也会被其占用,此时 prev_size 是无效的。
- size: chunk 的大小。由于 chunk 大小是对齐的,所以 size 的最低3个 bit 都是0,可以用于存储一些信息:
- prev_inuse = chunk→size & 1, 是否前一个 chunk 正在使用中(已被分配出去)
- is_mmapped = chunk→size & 2, 此 chunk 是否是用 mmap 分配出来的
- non_main_arena = chunk→size & 4, 此 chunk 是否不属于 main arena
- 因此,计算 chunk 的实际大小的方法为:chunksize(p) = p→size & ~(1|2|4)
- fd: 指向链表中的下一个节点
- bk: 指向链表中的上一个节点
fd 和 bk 等成员,仅在 此 chunk 为空闲时有效。
当 chunk 被分配之后,fd 和 fd 之后的域,以及下一个chunk 的 prev_size 域,都会成为本 chunk 中用户可用内存的一部分。
当 malloc 内部分配出适当大小的chunk之后,返回的指针实际等于 &chunk→fd。
未分配的chunk:
+-----------------------------------+ <- chunk
| prev_size |
+-----------------------------------+
| size |N|M|P|
+-----------------------------------+
| fd |
+-----------------------------------+
| bk |
+-----------------------------------+
| |
| unused memory |
| |
+-----------------------------------+ <- next chunk
| prev_size |
+-----------------------------------+
| size |N|M|P| (P = 0)
+-----------------------------------+
| ... |
+-----------------------------------+
已分配的chunk:
+-----------------------------------+ <- chunk
| prev_size |
+-----------------------------------+
| size |N|M|P|
+-----------------------------------+ <- mem
|/fd|
+-----------------------------------+
|/bk|
+-----------------------------------+
|///|
|///|
|///|
+-----------------------------------+ <- next chunk
|/prev_size/|
+-----------------------------------+
| size |N|M|P| (P = 1)
+-----------------------------------+
| ... |
+-----------------------------------+
一些有用的结论:
- 要判断一个空闲 chunk 是否有效,可以看其 size 是否与 next chunk 的 prev_size 相等。(nextchunk = (struct malloc_chunk *) ( ( (char *) chunk ) + chunksize(chunk) ) )
- 因为一个 chunk 的已分配状态下的用户可用内存与未分配状态下的 fd 和 bk 指针有所重叠,当程序在修改一个已释放的对象时有可能覆盖掉 fd 或 bk 指针。
更多推荐
所有评论(0)