近段时间在调试一个程序的时候遇到了malloc()函数调用失败的问题,该程序的内存消耗不大,系统内存也比较充足,在调试后终于知道原来是内存溢出破坏了内存分配表。相关代码抽象如下:
typedef struct _COMPONENT_
{
char name[100 + 1];
char value[100 + 1];
}component_t;
int some_function(const char *name, const char *value)
{
component_t* c = NULL;
char *desc = NULL;
c = (component_t *)malloc(sizeof(component_t));
if(!c)
{
/* print error message! */
}
strncpy(c.name, name, strlen(name));
strncpy(c.value, value, strlen(value));
...
desc = (char *)malloc(sizeof(char) * 16); /* malloc failed here.*/
if(!desc)
{
/* print error message! */
}
...
}
刚开始执行程序是没有问题的,换了一些参数后就弹出一个崩溃的对话框。根据以往的经验,我认为是一个与内存相关的问题。于是我使用出现问题的参数进行调试,不久之后找到了出错的位置,原来程序的崩溃是上述模型中的第二个malloc()函数调用失败导致的。考虑到该程序对内存占用较小,当时系统的内存也还有很多空余,于是我知道了不是因为内存不够而导致分配内存失败,而应该是某处的操作破坏了系统内存分配表造成malloc()函数不能正常工作。接下来我使用打桩的方法来查看从何处开始就会造成malloc()调用失败。打桩的代码如下:
/* For debugging. 2012-2-6 */
{
char *pTemp = NULL;
pTemp = (char *)malloc(sizeof(char) * 16);
if(!pTemp)
{
/* print error message! */
}
}
其中第一句注释是统一加在调试代码之前的,可以在完成调试后通过搜索找出所有添加的调试代码,一并删除。代码开始和结束的大括号是为了将这一段代码隔离,避免影响周围其他代码。将这段代码插入到程序中的几个关键位置后再调试,终于发现在some_function()的第二个strncpy()调用后,malloc()函数调用就会失败。再次调试,停在第二个调用strncpy()的地方,发现value的长度已经超过了100,而c.value只能容纳100个字符和一个结束符。value中多余的数据就会被写到c.value之后的内存中,Windows系统的内存分配大概要在已分配内存之后记录相关信息,这段信息被覆盖后当然不能分配内存成功。将component_t的定义中value的范围增大后,问题就暂时解决了。
其实这在C语言中是一个相当常见的bug,但是在测试时又很难发现。为了避免这样的问题,可以根据情况采用下面的方法:
(1). 如果对字段的长度有限定,就在赋值前检查赋值的长度。;例如,在上面的例子中先检查传入的value的长度,检查合法后再赋值给c.value.(2). 如果没有限定字段的长度,则需要使用能够动态增长的字符串。在接下来的博客里,我会实现一个能够动态增长的字符串,其原理就是在已有的空间不够存储字符串时,另外分配一块更大的内存,将以前的内容复制到新的内存里,再将要加入的字符串连接进去,最后释放之前的内存。
以前在Open VMS系统里调试一个程序时也遇到过这样的问题,并且同样的代码在Windows里运行良好。当时我花了很久的时间去查找这个问题,最后也是利用上面这种打桩的方法解决的,这也是为什么在遇到上面的问题的时,我首先就想到了这种方法。由此可以看出,不同操作系统的内存管理方法不同,即使在一个系统里没有发现问题,在另一个系统里也可能出现问题。这就需要我们在设计和编码的时候多加注意,添加必要的检查语句,防止内存溢出。