8.5.1 为什么要自己编码检查越界和泄漏
有了CRT库和BoundsChecker为什么还要自己编码检查泄漏呢?主要有以下几个方面的原因:
(1)CRT库只能检查当前项目代码中的内存问题。如果使用了第三方库,则对调用第三方库时出现的泄漏和越界无法进行检查。BoundsChecker或Purify虽然能检查第三方泄漏和越界,但是它只能检查系统的泄漏情况;如果用户有自定义的内存管理函数或其他资源分配函数,则BoundsChecker和Purify无法检查,必须要自己写代码进行检查。
(2)不仅在白盒测试时需要做内存测试,黑盒测试时同样需要做内存测试。在黑盒测试中,不仅内部的系统测试需要做内存测试,在做Beta测试时同样需要做内存测试。同样的程序在开发的机器上运行没有问题,到了用户那里运行时往往就会因为硬件、操作系统、网络甚至操作次序的不同而暴出一大堆问题,所以在用户Beta测试里做内存测试是很有必要的。由于不可能给每个用户都装一个BoundsChecker或Purify之类的工具,最好的情况就是自己的代码本身就有内存测试的功能,所以很有必要自己动手编写检查内存泄漏和越界的代码。
(3)商业的内存检查工具通常价格都比较昂贵,要购买这些软件对于中小型软件公司来说是一笔不小的投入。
综上所述,自己编写检测越界和泄漏的程序很有必要。接下来就来讲解如何自己编码来实现内存越界和泄漏检查。
8.5.2 自己编码检查内存越界和泄漏
1. 将malloc()调用定向到自定义的Tmalloc_dbg()函数上
我们还是先以如何检查malloc()和free()函数为例来讲解如何自己编码实现内存越界和泄漏检查。
要实现malloc()和free()函数的检查,首先得将这两个函数定向到自己编写的内存分配和释放函数上,本书假定自己编写的内存分配和释放函数名为Tmalloc_dbg()和Tfree_dbg()。那么以下的宏定义就可以实现将malloc()定向到Tmalloc_dbg()上,将free()定向到Tfree_dbg()上。
#define malloc(s) Tmalloc_dbg(s, __FILE__, __LINE__)
#define free(s) Tfree_dbg(s, __FILE__, __LINE__)
使用以上的定义后,所有的malloc()和free()函数调用在编译后被替换成了Tmalloc_dbg()和Tfree_dgb()函数调用。接下来需要实现Tmalloc_dbg()和Tfree_dbg()函数以实现越界和泄漏检查。
编译器预定义宏__FILE__和__LINE__用以标识函数是在源代码的哪个文件的哪一行被调用的,便于发现泄漏和越界后定位问题代码。
对于C++中的new和delete操作符,可以使用以下定义来进行定向:
void * operator new (size_t size, char *pszFile, int nLine);
void * operator new [] (size_t size, char *pszFile, int nLine);
void operator delete (void *p);
void operator delete[] (void *p);
#define new DBG_NEW
#define DBG_NEW new(__FILE, __LINE__)
这样如果包含上面定义的源文件中使用了new 和delete操作符的话,它们就会被定向到上文声明的几个函数上:
例如:char *p = new char [128];
那么最终将调用void * operator new [] (size_t size, char *pszFile, int nLine);这个函数。
注意,在operator new和operator delete实现的4个函数的文件中不要有以下的宏定义:
#define new DBG_NEW
#define DBG_NEW new(__FILE__, __LINE__)
2. 实现自己的Tmalloc_dbg函数来进行内存越界和泄漏检查
当malloc和free调用被定向到Tmalloc_dbg和Tfree_dbg上后,如何实现自己的Tmalloc_dbg和Tfree_dbg去进行内存越界和泄漏检查呢?
可以在Tmalloc_dbg函数中将分配的内存地址保存到一张全局表中,Tfree_dbg函数释放内存时,先在表中找到对应的地址项,从表中删除包含这个地址的节点。这样只要在程序退出时检查表中还剩多少节点就知道有多少泄漏了。
要检查越界则需要在分配内存时要比原来分配的内存尾部多分配4个字节,在尾部4个字节上填上校验字节,释放时检查尾部4个校验字节是否遭到破坏,如果遭到破坏就表明存在越界。
用以下的TCheckMemory类来管理分配的内存,负责检查内存越界和泄漏情况。
class TCheckMemory {
public:
static THashTable *m_pMemTable;
TCheckMemory(UINT uBucketCount);
Destroy();
~TCheckMemory();
void TCheckMemoryLeak();
void TCheckMemoryOverrun();
void TCheckMemAll(); //Both check memory leaks and overrun.
};
TCheckMemory类内包含了一个哈希表、一个构造函数和析构函数、一个Destroy()函数和内存越界检查及泄漏检查函数。
在构造函数中实现哈希表的创建时要注意,由于malloc被重定向,所以不能再使用malloc来分配内存。为了方便起见,我们先使用new来分配内存。
TCheckMemory::TCheckMemory(UINT uBucketCount)
{
INT nRet;
m_pMemTable = new THashTable;
nRet = m_pMemTable->Create(uBucketCount, HashMemNode,
MemNodeCompare, MemNodeDestroy);
assertTrue( nRet == CAPI_SUCCESS );
}
对于每条分配的内存,需要使用以下结构体来记录每块内存的信息,这些信息包括内存地址、内存大小、内存分配调用处的源文件名和行号等。
typedef struct MEMNODE_st{
DWORD dwAddr;
unsigned int uSize;
unsigned int uLine;
char * pszFileName;
} MEMNODE;
下面再来实现Tmalloc_dbg函数,需要注意的是Tmalloc_dbg函数中不能使用malloc分配内存,否则编译后malloc会被替换成Tmalloc_dbg,产生无限循环的递归调用。为方便起见,我们仍然使用new来分配内存。
void *Tmalloc_dbg(size_t size, char *pszFile, unsigned int uLine)
{
char *pAddr;
pAddr = (char *)new char[size+sizeof(UINT)];
assertTrue( pAddr != NULL);
*(UINT *)(pAddr+size) = MEM_CHECK_FLAG;
MEMNODE *pNode = NULL;
pNode = new MEMNODE;
pNode->dwAddr = (DWORD)pAddr;
pNode->uSize = size;
pNode->pszFileName = new char[strlen(pszFile)+1];
strcpy(pNode->pszFileName, pszFile);
pNode->uLine = uLine;
if ( TCheckMemory::m_pMemTable != NULL )
{
TCheckMemory::m_pMemTable->Insert(pNode);
}
return (void *)pAddr;
}
在Tmalloc_dbg函数中,先分配一块比原来要分配的内存大4字节的内存,再在尾部4个字节上填上校验字节,然后将分配内存的地址、大小、源文件名、行号等信息放到一个MEMNODE结构体里,最后将结构体插入到哈希表中保存起来。
Tmalloc_dbg函数实现后,就可以对分配的内存进行检查了,首先是在释放操作时要检查内存是否有越界发生,其次在程序退出时需要检查是否有越界和泄漏发生。
3. 释放函数Tfree_dbg()的实现
下面是Tfree_dbg()函数的实现:
void Tfree_dbg(void *p, char *pszFile, UINT uLine)
{
MEMNODE *pNode;
pNode = (MEMNODE *)TCheckMemory::m_pMemTable->Find( p, HashInt );
if ( pNode != NULL)
{
char *psz = (char *)pNode->dwAddr;
UINT *pTail = (UINT *)(psz + pNode->uSize);
if ( *pTail != MEM_CHECK_FLAG )
{
char msg[1024];
sprintf(msg, "Memory overrun at 0x%x/n/t malloc position: "
"File:%s, Line:%d/n /tfree position: File:%s,
Line:%d/n",
pNode->dwAddr, pNode->pszFileName, pNode->uLine, pszFile,
uLine);
printf(msg);
}
TCheckMemory::m_pMemTable->Delete((void *)psz, HashInt);
delete [] (char *)p;
}
else
{
printf("Try to free a undefined memeory not alloced by malloc()./n"
"File:%s, Line:%d./n", pszFile, uLine);
}
}
Tfree_dbg()函数中,先在哈希表中找到要释放的内存节点,找到后先检查内存尾部4个字节和校验字节是否相等,如果不相等,则表明存在泄漏,打印出泄漏发生处的源文件名和行号,然后从哈希表中将找到的节点删除。
当在哈希表中未找到对应的地址时,表明这块内存不是被Tmalloc_dbg()函数分配的,这种情况在后面讲检查第三方库时再进行讲解。
4. 在程序退出时检查内存越界和泄漏
在程序退出时检查内存泄漏很容易,只要在哈希表中进行遍历操作,发现有未被释放的节点就表明存在泄漏,并且检查是否有越界发生。程序如下:
void TCheckMemory::TCheckMemoryLeak()
{
UINT uNodeCount;
UINT uOverRunCount;
MEMNODE *pNode;
char msg[1024];
uNodeCount = TCheckMemory::m_pMemTable->GetNodeCount();
if ( uNodeCount == 0 )
{
return;
}
uOverRunCount = 0;
TCheckMemory::m_pMemTable->EnumBegin();
while( (pNode = (MEMNODE *)TCheckMemory::m_pMemTable->EnumNext())
!= NULL )
{
sprintf(msg, "Memory leak at 0x%x/n/t malloc() position: "
"File:%s, Line:%d/n",
pNode->dwAddr, pNode->pszFileName, pNode->uLine);
printf(msg);
}
sprintf(msg, "TCheckMemoryLeak(): Total memory leak count: %d/n",
uNodeCount);
printf(msg);
}
void TCheckMemory::TCheckMemoryOverrun()
{
UINT uNodeCount;
UINT uOverRunCount;
MEMNODE *pNode;
char msg[1024];
uNodeCount = TCheckMemory::m_pMemTable->GetNodeCount();
if ( uNodeCount == 0 )
{
return;
}
uOverRunCount = 0;
TCheckMemory::m_pMemTable->EnumBegin();
while( (pNode = (MEMNODE *)TCheckMemory::m_pMemTable->EnumNext())
!= NULL )
{
char *psz = (char *)pNode->dwAddr;
UINT *pTail = (UINT *)(psz + pNode->uSize);
if ( *pTail != MEM_CHECK_FLAG )
{
sprintf(msg, "Memory overrun at 0x%x/n/t malloc() position: "
"File:%s, Line:%d/n",
pNode->dwAddr, pNode->pszFileName, pNode->uLine);
printf(msg);
uOverRunCount += 1;
}
}
sprintf(msg, "TCheckMemory(): Total memory overrun count: %d/n",
uOverRunCount);
printf(msg);
}
需要注意的是,上面代码里使用了new来分配内存,在实际检查时,由于new也需要被检查,也会被定向到新的操作符上,所以如果要对new进行检查时,里面就不能使用new分配内存了。
另外,上面使用的哈希表THashTable类的代码没有给出,因为哈希表里不能使用malloc等函数分配内存,必须用另外的函数来分配内存,所以不能使用一些商业库里的哈希表,必须专门写一个哈希表。所有的关于内存越界和泄漏检查的完整代码可以参考作者主持的开源项目CAPI,可以访问网址http://gforge.osdn.net.cn/projects/capi以获取最新的代码。