VC调用giflib(4):内存泄漏与功能缺失

作者:马健
邮箱:stronghorse_mj@hotmail.com
主页:http://www.comicer.com/stronghorse
发布:2020.03.14

一、EGifSpew的内存泄漏与功能缺失

在上一篇笔记里说过,我因为要计算全局调色板和局部调色板,所以只能用EGifSpew对GIF进行编码,但giflib中对这个函数的实现存在严重的内存泄漏问题。

1)EGifPutScreenDesc函数调用造成的内存泄漏

在EGifSpew函数开头部分是这样调用EGifPutScreenDesc函数的:

int
EGifSpew(GifFileType *GifFileOut) 
{
    int i, j; 

    if (EGifPutScreenDesc(GifFileOut,
                          GifFileOut->SWidth,
                          GifFileOut->SHeight,
                          GifFileOut->SColorResolution,
                          GifFileOut->SBackGroundColor,
                          GifFileOut->SColorMap) == GIF_ERROR) {
        return (GIF_ERROR);
    }
	……

而在EGifPutScreenDesc函数中,是这样操作的:

int
EGifPutScreenDesc(GifFileType *GifFile,
                  const int Width,
                  const int Height,
                  const int ColorRes,
                  const int BackGround,
                  const ColorMapObject *ColorMap)
{
    GifByteType Buf[3];
    GifFilePrivateType *Private = (GifFilePrivateType *) GifFile->Private;
    const char *write_version;
    GifFile->SColorMap = NULL;
    
    ……

    if (ColorMap) {
        GifFile->SColorMap = GifMakeMapObject(ColorMap->ColorCount,
                                           ColorMap->Colors);
        ……
    } else
        GifFile->SColorMap = NULL;	

即一进EGifPutScreenDesc函数,三不管就把GifFile->SColorMap赋空了,后面再重新给它分配内存。而在我的源代码中,是先给GifFile->SColorMap分配了全局调色板的,结果进去后就成了野指针。这个操作够不够骚?说实话我刚看到的时候真的是被雷到了,不由感慨我还是太年轻,这世上还有无数的奇葩我还没见识过。

解决办法就是在调用EGifPutScreenDesc函数前先对GifFile->SColorMap指针进行备份,调用后释放掉即可。

2)EGifCloseFile函数调用造成的内存泄漏

一般使用EGifSpew函数生成动画GIF的过程,都是先把各帧图像存储到GifFileType结构体的SavedImages数组中,然后再调用EGifSpew把数组中的各帧顺序编码、存盘。但是EGifSpew的问题就在于各帧编码完成后,并没有释放SavedImages数组中的各帧内存,直接就调用EGifCloseFile函数释放掉GifFileType结构体,结果SavedImages数组中的各帧就成了没爹没娘的野指针。

解决的办法就是在调用EGifCloseFile之前,先调用GifFreeSavedImages释放掉SavedImages数组中分配的内存。

除了上述内存泄漏问题外,EGifSpew函数中还存在一个功能缺失:在EGifPutScreenDesc后,没有写入动画GIF的循环次数,导致生成的GIF文件只能动一次。正常情况下循环次数是写在帧数据前面的,所以应该在EGifPutScreenDesc之后,在for循环写各帧数据之前。

最后我实在是没办法,只能把原版EGifSpew函数复制一份出来修改成MyEGifSpew,完整代码如下:

// MyEGifSpew要用到此函数,但在egif_lib.c中此函数是static局部函数,所以照抄一遍
static int
EGifWriteExtensions(GifFileType *GifFileOut, 
					ExtensionBlock *ExtensionBlocks, 
					int ExtensionBlockCount) 
{
	if (ExtensionBlocks) {
		int j;

		for (j = 0; j < ExtensionBlockCount; j++) {
			ExtensionBlock *ep = &ExtensionBlocks[j];
			if (ep->Function != CONTINUE_EXT_FUNC_CODE)
				if (EGifPutExtensionLeader(GifFileOut, ep->Function) == GIF_ERROR)
					return (GIF_ERROR);
			if (EGifPutExtensionBlock(GifFileOut, ep->ByteCount, ep->Bytes) == GIF_ERROR)
				return (GIF_ERROR);
			if (j == ExtensionBlockCount - 1 || (ep+1)->Function != CONTINUE_EXT_FUNC_CODE)
				if (EGifPutExtensionTrailer(GifFileOut) == GIF_ERROR)
					return (GIF_ERROR);
		}
	}

	return (GIF_OK);
}

// 原版的EGifSpew存在内存漏洞,而且没有设置循环播放次数,导致生成的GIF只能播放一次
// nLoopTimes:循环播放次数,0表示无限循环播放
static int MyEGifSpew(GifFileType *GifFileOut, int nLoopTimes) 
{
	int i, j; 

	// EGifPutScreenDesc会破坏GifFileOut->SColorMap,所以调用前必须先备份,否则出现野指针
	ColorMapObject* SColorMap = GifFileOut->SColorMap;
	if (EGifPutScreenDesc(GifFileOut,
		GifFileOut->SWidth,
		GifFileOut->SHeight,
		GifFileOut->SColorResolution,
		GifFileOut->SBackGroundColor,
		GifFileOut->SColorMap) == GIF_ERROR) {
			return (GIF_ERROR);
	}
	// 在EGifPutScreenDesc中重新分配了GifFileOut->SColorMap,所以手工释放前面保存的
	GifFreeMapObject(SColorMap);

	// 在共享调色板之后,写入循环次数。原版EGifSpew少了这一项,所以生成的GIF只能动一次
	{
		/* Create a Netscape 2.0 loop block */
		unsigned char params[3] = {1, 0, 0};	// 后两个字节是循环次数,0表示无限循环
		params[1] = (nLoopTimes & 0xff);
		params[2] = (nLoopTimes >> 8) & 0xff;
		if (EGifPutExtensionLeader(GifFileOut, APPLICATION_EXT_FUNC_CODE) != GIF_OK ||
			EGifPutExtensionBlock(GifFileOut, 11, "NETSCAPE2.0") != GIF_OK ||
			EGifPutExtensionBlock(GifFileOut, 3, params) != GIF_OK ||
			EGifPutExtensionTrailer(GifFileOut) != GIF_OK
			)
			return (GIF_ERROR);
	}

	for (i = 0; i < GifFileOut->ImageCount; i++) {
		SavedImage *sp = &GifFileOut->SavedImages[i];
		int SavedHeight = sp->ImageDesc.Height;
		int SavedWidth = sp->ImageDesc.Width;

		/* this allows us to delete images by nuking their rasters */
		if (sp->RasterBits == NULL)
			continue;

		if (EGifWriteExtensions(GifFileOut, 
			sp->ExtensionBlocks,
			sp->ExtensionBlockCount) == GIF_ERROR)
			return (GIF_ERROR);

		if (EGifPutImageDesc(GifFileOut,
			sp->ImageDesc.Left,
			sp->ImageDesc.Top,
			SavedWidth,
			SavedHeight,
			sp->ImageDesc.Interlace,
			sp->ImageDesc.ColorMap) == GIF_ERROR)
			return (GIF_ERROR);

		if (sp->ImageDesc.Interlace) {
			/* 
			* The way an interlaced image should be written - 
			* offsets and jumps...
			*/
			int InterlacedOffset[] = { 0, 4, 2, 1 };
			int InterlacedJumps[] = { 8, 8, 4, 2 };
			int k;
			/* Need to perform 4 passes on the images: */
			for (k = 0; k < 4; k++)
				for (j = InterlacedOffset[k]; 
					j < SavedHeight;
					j += InterlacedJumps[k]) {
						if (EGifPutLine(GifFileOut, 
							sp->RasterBits + j * SavedWidth, 
							SavedWidth)	== GIF_ERROR)
							return (GIF_ERROR);
				}
		} else {
			for (j = 0; j < SavedHeight; j++) {
				if (EGifPutLine(GifFileOut,
					sp->RasterBits + j * SavedWidth,
					SavedWidth) == GIF_ERROR)
					return (GIF_ERROR);
			}
		}
	}

	if (EGifWriteExtensions(GifFileOut,
		GifFileOut->ExtensionBlocks,
		GifFileOut->ExtensionBlockCount) == GIF_ERROR)
		return (GIF_ERROR);

	// 原版EGifSpew少了这一句,导致大量的内存漏洞
	GifFreeSavedImages(GifFileOut);

	if (EGifCloseFile(GifFileOut, NULL) == GIF_ERROR)
		return (GIF_ERROR);

	return (GIF_OK);
}

上面的代码修正了两处内存泄漏,并允许调用时设置循环次数。在写入循环次数的时候,使用了最流行的NETSCAPE2.0,没有用比较少见的ANIMEXTS1.0。

另外如果是对细节比较在意的程序员,在调用EGifSpew开始写GIF文件之前,还应该调用EGifGetGifVersion函数,让giflib自动设置输出的GIF文件的版本。虽然不调用也不会有啥大影响,但细节终归是细节。

二、EGifPutImageDesc函数中的内存泄露

这个内存泄漏比较隐蔽。先看该函数中的一段代码:

    if (ColorMap != GifFile->Image.ColorMap) {
		if (ColorMap) {
		    // 用ColorMap更新GifFile->Image.ColorMap
		    ……
		} else {
		    GifFile->Image.ColorMap = NULL;
		}
    }

如果满足第一层条件ColorMap != GifFile->Image.ColorMap,说明这两个指针总有一个非空,第二层判断的else块中ColorMap为空情况下,GifFile->Image.ColorMap就必然不为空,这个时候把它直接赋空,妥妥的内存泄露。

解决方法很简单,在
GifFile->Image.ColorMap = NULL;
之前加一句
GifFreeMapObject(GifFile->Image.ColorMap);
即可。

我喜欢VC的原因之一,就是VC有内存泄漏报告机制,即在debug状态下应用退出后,会逐条报告所有未被释放的内存及该条内存申请时的编号,有了编号再找导致内存泄漏的源代码就相对比较容易,下条件断点即可。

(全文完)

posted @ 2022-03-14 09:43  strnghrs  阅读(325)  评论(1编辑  收藏  举报