碰到一个内存越界,设备起来后不久报错并当机,串口打印为*** glibc detected *** ./server: double free or corruption (!prev): 0x08a03b88 ***
这个头疼的问题,和同事跟踪定位了三天,终于得到解决,下面分析下定位堆越界的过程,由于不在公司,不方便贴出源码及数据,只大概给出粗略的数据和过程:
这个内存越界死机问题,其实已经埋伏了很久,但太过偶现,大家也没注意,最近突然频繁死机,也给了我们定位的契机。double free or corruption (!prev): 0x08a03b88,glibc的这个报错,表示glibc检测到自己的内存头信息已经被破坏。
下面简单描述下ptmalloc的内存模型:
malloc_chunk结构的前两个成员(8字节).一段已分配的内存结构如下图所示:
0 16 32
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 上一个块的字节数(如果上一个块空闲的话) | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 当前块的字节数 (size) |M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 用户数据开始... .
. .
. (用户可以用空间大小) .
. |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
当用户要去free 0x08a03b88这个地址时,glibc需要在这个地址的前8个字节找这个内存块的信息,当这个内存块被上个内存块越界时,头信息被破坏,于是报错,我们要做的,就是要找到谁在用上一个内存块。
调gdb分析0x08a03b88地址附近的内存信息,可以一窥上个内存块的信息,分析上个内存块的信息,或许能找到是谁在用上个内存块:
gdb x/128wx 0x08a03b88-64
0x08a03b58:0x00000001 0x00000001 0x00000001 0x0000004d
0x08a03b58:0x12345678 0x00000029 0x00000001 0x00000001
0x08a03b68:0x00000001 0x00000001 0x00000001 0x00000001
0x08a03b78:0x00000001 0x00000001 0x00000001 0x00000001
0x08a03b88:0x00000001 0x00000001 0x00000001 0x00000001
根据ptmalloc的结构,上述着红色部分,就是当前要释放内存块的内存头,于是需要分析的是上个内存块,分析一段堆上的内存数据,上个内存是完整的,但对他的读写越界了。但分析了这个数据,并不是一个马上能分析的字符等数据。虽然很有规律,而且改内存块里的第一个四字节,总是填0x12345678;但翻遍代码,没有这种赋值过程,可见0x12345678并不是一个常量。
虽然分析内容没有直接给出有用的线索,但还是给了我们非常有用的东西,聪明的同事马上想到了,在这种内存free的时候,打印堆栈,看是谁在申请和释放这个内存块。
重定向malloc和free到我们自己的__wrap_malloc_和__wrap_free_函数,
void __wrap_free(void *ptr)
{
if(*ptr == 0x12345678)
{
assert(0);
}
__real_free(ptr);
}
原以为我们很快会找到真凶时,却怎么也assert不掉,原来这家伙还只malloc,不free的。
于是只能想其他办法。
下面就从malloc入手了。首先想到的是,在malloc的时候记录申请的线程号,如果是特殊线程,那也能大大缩小查找的范围,我们把附加的信息,记录到内存头的下一个4字节中。
#define EXTERN (8)
void __wrap_malloc(int c)
{
void *ptr = __real_malloc(c+EXTERN);
*ptr = getthreadID();
return (void*)(ptr+EXTERN)
}
死机的时候,分析堆上数据,查找到的线程号是主线程号,这个在茫茫代码中,就无法查找了。
但是,一步步接近曙光了。下一步,记录调用链到我们开辟的附加内存单元中,(不得不佩服下同事的渊博和睿智);
原理是,但函数调用时,他的调用链和参数等会保存到栈上,而调用的指令,他的数据有一定的特点,可以通过查看汇编代码(readelf)或这/proc/tid/maps查看代码段的
范围
代码段的范围一般是
0x00008000 ~0x010fb00000之类的;
于是,在malloc的时候,从栈顶搜索可能的指令地址,保存到预留的内存空间中,等死机的时候,打印内存信息,对照反汇编的指令,看调用链关系。
#define EXTERN (24)
void __wrap_malloc(int c)
{
void *ptr = __real_malloc(c+EXTERN);
void *a = &ptr;//此处获取当前栈的地址
for(int i = 0; i < 1024; i++) //有于栈是从高地址往低地址生长,所以往高地址遍历可能的指令地址。
{
int j = 0
if(a[i] > 0x00080000 && a[i] < 0x10fb00000)
*ptr+ 4*j++ = a[i];
if(j==3) break;
}
}
下面坐等死机,打印堆栈,分析内存内容信息。
果然,存在预留内存空间的数据,从反汇编的代码里,搜到的指令地址,并找到完整的调用链,找到申请这块内存的罪魁祸首。