关于malloc实际分配内存的探讨
在最近的项目中,发现了一个比较奇怪的bug,bug的最终结果引发了free(): invalid next size (fast)错误,导致程序崩溃。 通常这个问题很常见,就是由于实际写入的内存大小超过了由malloc获取的内存块的大小导致的覆盖,解决方案也很简单,不管是*还是其他论坛上对这个问题的解决方案通常都是在实际的大小上+1即可。
在博主正在做的项目中,原先的代码写法是通过strlen获取字符串长度,然后调用malloc来获得这个长度对应的内存块,于是就产生了上述的问题。当然这个问题目前已经解决了,只要strlen + 1 即可,但是奇怪的地方在于在项目中只有特定的字符串会引发这个问题,而并非所有通过这种方式分配的地址都会报错,这是为什么呢?
先说明场景:项目中采用UTF-8编码,并且字符串含有汉字与各类其他字符的组合。首先,utf-8属于变长编码,汉字通常占3字节,但是英文或其他的字符占一字节。因此,在博主的机器上,如下的代码会引发上述的错误:
typedef struct{
char* name;
char* address;
char* auth;
char* birthday;
}TEST;
int main()
{
TEST* ptest = (TEST*)calloc(1 , sizeof(TEST));
char* address = "一二三四五六七八九十一二三a";
char* name = "测试名";
char* auth = "一二三四五六七八九十一二三";
char* birthday = "12345678";
FILE* fp = NULL;
fp = fopen("/home/kylin/testcode/addr.log" , "w");
fprintf(fp , "sizeof TEST : %zd\nsizeof char* : %zd\nsizeof int : %zd\n" , sizeof(TEST) , sizeof(char*) , sizeof(int));
ptest->name = calloc(strlen(name) , sizeof(char));
ptest->address = calloc(strlen(address) , sizeof(char));
ptest->auth = calloc(strlen(auth) , sizeof(char));
ptest->birthday = calloc(strlen(birthday) , sizeof(char));
strcpy(ptest->name , name);
strcpy(ptest->address , address);
strcpy(ptest->auth , auth);
strcpy(ptest->birthday , birthday);
fprintf(fp , "&ptest : %10p\n&Name : %10p\n&Addr : %10p\n&Auth : %10p\n&Birth : %10p\n" , ptest , ptest->name , ptest->address , ptest->auth , ptest->birthday);
fprintf(fp , "NameLen : %zd\nAddrLen : %zd\nAuthLen : %zd\nBirthLen : %zd\n" , strlen(ptest->name) , strlen(ptest->address) , strlen(ptest->auth) , strlen(ptest->birthday));
fprintf(fp , "Name : %s\nAddr : %s\nAuth : %s\nBirth : %s\n" , ptest->name , ptest->address , ptest->auth , ptest->birthday);
fclose(fp);
free(ptest->name);
free(ptest->address);
free(ptest->auth);
free(ptest);
return 0;
}
编译运行后,在free时会报出错误:
*** Error in `./main': free(): invalid next size (fast): 0x0000000001ac52a0 ***
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(+0x777e5)[0x7f9183da77e5]
/lib/x86_64-linux-gnu/libc.so.6(+0x7fe0a)[0x7f9183dafe0a]
/lib/x86_64-linux-gnu/libc.so.6(cfree+0x4c)[0x7f9183db398c]
./main[0x400df5]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7f9183d50830]
./main[0x4006f9]
该错误仅仅在执行free(ptest->auth)时出现,那么为什么在之前的free不会产生呢?通过多次测试发现,在address的最后的a是引发这个错误的原因,如果删掉这个a,或者多加一个a都不会出错。在测试过程中我将内存分布情况打印出来:
无错误的内存分布图:
错误的内存分布图:
由于malloc分配的实际内存大小总是大于我们申请的内存大小,其中有多出来的8个字节用于存放struct mem_control_block,这个结构体通常位于我们申请获得的首地址-8的位置上,用于存放对于该内存块的管理信息。我们注意到,在出错的内存分布中,ptest->auth地址块中,mem_control_block字段中的size(地址为0x220f2c8)值变成了0。那么这个0就应当是address中的结束符了。而其他的内存块中,mem_control_block.size的大小也并非实际我们申请的大小。这是为什么呢?
实际上这是因为堆中的内存块总是成块分配的,并不是申请多少字节,就拿出多少个字节的内存来提供使用。堆中内存块的大小通常与内存对齐有关,在我的机器上,通过测试发现内存块的分配总是按照16字节的大小来进行分配的。比如下面的测试代码:
#define MLEN 25
int main()
{
char* ptr = NULL;
unsigned long lastaddr = 0;
int i = 0;
while (i < 10)
{
ptr = malloc(sizeof(char) * MLEN);
printf("%d : %10p , Get Size : %zd\n" , i ++ , ptr , (int)ptr - lastaddr);
lastaddr = (int)ptr;
}
return 0;
}
当MLEN值为25的时候,输出如下:
当MLEN的值调整到24的时候,输出如下:
之所以使用24和25,是因为内存块分配的实际大小是要包括mem_control_block结构体的,将这个结构体的大小(8字节)算进去后,两次申请的大小是32与33,但是实际分配后的大小却是32与48字节。因此可以发现,当:申请大小+sizeof(struct mem_control_block) % 16 == 0的时候,那么刚好可以完成一次满额的分配,但是当其!=0的时候,就会多分配一个内存块,而这个内存块的大小在我的机器上就是16字节。
现在回到最开始的问题,为什么ptest->address会造成越界而其他变量的不会造成越界?原因就在于使用strlen(address)的时候不会将\0计算进长度里面,同时很巧的是strlen(address) + sizeof(mem_control_block) = 48,刚好满足了3块内存块的大小,因此整个字符串刚好填充满所有获得的内存块。在这个情况下,使用strcpy拷贝字符串,在结尾会多出一个未计算长度的结束符,这个结束符将会覆盖掉下一个指针(即ptest->auth)所指向的内存块的mem_control_block中.size的值。这种情况下就在引发了在free(ptest->auth)时由于.size大小的不对而产生的free(): invalid next size (fast)错误。
因此,对于这种内存错误,在strlen的基础上+1就是最高效有用的解决方案了。
下一篇: 新手必看:PS图片处理小技巧介绍(二)