【踩雷】指针惹的货

1.再战野指针

另外一个项目组的产品临时插入了一个需求:优化路况图层的绘制顺序。极不情愿情况下完成了编码,编码时尽可能讲code范围集中,效果实现后产品十分满意。我自己review过好几次(由于底层代码供多个平台使用,所以svn之前一般都不同时间段review几次),同步到了我们的产品以及另外一个兄弟产品主干上。

大家每天开发一直在使用,就连测试也没发现有问题,就在我们APP要上线的前两周,兄弟产品线的一个同事GG反馈,ios上开启了guard malloc机制,打开路况操作一段时间后总崩,而且是必现。直接发了邮件,而且还急匆匆地跟我要一起review code。他第一反应就是既然必现为毛我们这边开发和测试没发现呢,而且我们看了几次代码还是不觉得有问题啊,代码如下,(哎。。。):

Color filteredColors[3] = { GetRenderColor(0), GetRenderColor(1), GetRenderColor(2) };
for (int pass = 0; pass < sizeof(filteredColors) / sizeof(filteredColors[0]); pass++)
{         
     for (int i=0; i<get_vec_size(vecLines); i++)
     {
          TrafficRoad *trafficRoad = get_vec_item(vecLines, i);
         
          if (trafficRoad->color_fill != filteredColors[pass])
          {
               continue;
          }

          // 不需要再次进行坐标转化
          set_pen_color(renderConfig->pGraphicsContext, trafficRoad->color_fill, trafficRoad->lineWidth);
          draw_poly_line(renderConfig->pGraphicsContext, trafficRoad->points, trafficRoad->pointCount);
          free(trafficRoad);
     }
}

code片段介绍:vecLines是路线数组,每个路线有一颜色,filteredColors里面是所有路线可能的颜色值:通过pass循环实现按filter数组顺序分三次绘制路线。而且当时为了尽可能减小代码改动范围,将路线对象的释放一并加到了两重for循环中。于是乎:这两重for循环运行机制变得十分复杂,反而极大地增加了指针问题引入的几率

本意是分三次遍历vecLines数组,每次绘制指定颜色的路线,绘制完成然后释放这条路线,但是最终结果是:引入了悬空指针(野指针),而且正常测试根本不会发现,除非借助于专门的工具测。

PC上实验

野指针版本在PC上run的时候,debug、release版本正常情况都不会出问题,会不会出crash就看电脑自身的状态了。

如果使用gflags呢?

C:\Program Files\Debugging Tools for Windows (x86)>gflags /p /enable E:\glTest.exe /full /aligned
path: SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options
    gltest.exe: page heap enabled
Gflags是随着微软Debugging tools for windows一起发布的工具。使用Gflags就能让系统对heap的分配,访问做一些检查,尽早的发现问题。

enable以后程序野指针一定会导致crash,如下图:

【踩雷】指针惹的货

禁掉gflags:

C:\Program Files\Debugging Tools for Windows (x86)>gflags /p /disable E:\glTest.exe /full /aligned
path: SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options
    gltest.exe: page heap disabled

FIX这个BUG以后后背顿时发凉,幸亏被兄弟产品线发现,否则随版本发布出去后果很严重。反思问题根因:编码阶段过于追求完美,将路线释放也糅合在了绘制的for循环里面去,导致整个两重for循环运行逻辑十分不直观,与KISS法则相悖。。。

2. 依然内存泄露

年前大版本中的XX路网功能是我独立开发,供android、ios移植。整个功能的重点是文件级、内存级的缓存,缓存淘汰、管理涉及相对频繁的内存问题,编码的时候对这块思路十分清楚对这块小心又小心,整个模块提到主干时还是惯例review了几次,由于这个模块比较大而且很重要,还组织了一次code review,给两个大牛讲了各个子模块。提交主线半个多月过去了,android、ios平台都不同程度地做过压测,并没有明显的crash行为,本以为万事大吉了。。。

结果上线之前的内存泄露测试,ios的高工测出了明显的内存泄露,当时有点汗颜啊。。。一起review code发现并不在最核心的缓存部分,而在数据解析模块:处理压缩的数据,接收压缩buffer没有释放。代码如下:

int BlockProcessor::UnCompress(UINT8* buf, UINT32 length, UINT8 iszip, Point ltCorner, int SCALE, int nBytes, BlockTypePtr& result)
{
	uLong  ulUncomprLen = 5*length;
	if(iszip == 1)
	{
		UINT8 *m_pUncompr = (UINT8*)SysMalloc(ulUncomprLen * sizeof(UINT8));
		int err = uncompress(m_pUncompr, &ulUncomprLen, (const Bytef*)buf, (uLong)length);
		buf = m_pUncompr;

		if(err != 0)
		{
			SysFree(buf);
			return _FAIL;
		}	
		length = ulUncomprLen;
	}
	char* bufPtr = (char*)buf;
	result = BlockProcessor::DeltaUnCompressBlock(bufPtr, length, ltCorner, SCALE, nBytes);
	return _OK;
}

code代码段介绍 :函数参数buf传入原始缓冲区内容,如果压缩则解压,然后解析按字节解析二进制buffer内容,上面code存在的问题是对于解压成功后,函数return之前并没有释放申请的堆内存。函数编码之初一个原则是,buf指向的原始缓冲区在函数外面进行释放,函数内部只负责释放它申请的堆内存。正常的编码逻辑如下:

if (iszip==1)
{
	UINT* m_pUncompr = malloc;
	
	int err = uncompress();
	if (err != 0)
	{
		free(m_pUncompr); 
		return _FAIL;  
	}
	
	BlockProcessor::DeltaUnCompressBlock(m_pUncompr);
}
else 
{
	BlockProcessor::DeltaUnCompressBlock(buf);
}
return _OK;

上面代码简单,直接但是有点冗余,DeltaUnCompress函数被写了两遍。为了显示水平,编码时我楞是把两种情况糅合在了一起,以至于把自己搞晕,解压失败都记得释放内存,而解压成功则忽略了。。反思:其实if else区分两种情况处理,是最直观的,case多时switch case更加直观

PC上实验

内存泄露并不会crash,所以更加难以发现。VLD全程Visual Leak Detector,其官网https://vld.codeplex.com/,源自codeproject上的一个开源项目,支持vs2008,vs2010及更高版本。

下载安装vld.exe,然后将vld以第三方库加入项目工程中,引入头文件#include <vld.h>以后,程序退出时如果有内存泄露,vld会实时将相关调用堆栈和内存信息dump到控制台窗口,以及vs的调试输出窗口,具体格式如下:

Call Stack:
    e:\dev_code\XX\src\streetviewroad\map_road_block_processor.cpp (21): glTest.exe!svr::BlockProcessor::UnCompress + 0xC bytes
    e:\dev_code\XX\src\streetviewroad\map_road_overlay_streetview.cpp (500): glTest.exe!svr::MapRoadStreetviewOverlay::LoadBlock + 0x3B bytes
    e:\dev_code\XX\src\streetviewroad\map_road_overlay_streetview.cpp (145): glTest.exe!svr::MapRoadStreetviewOverlay::GetRenderBlocks + 0x2E bytes
    e:\dev_code\XX\src\streetviewroad\map_road_overlay_render.cpp (96): glTest.exe!CMapRoadOverlayRender::Render + 0x37 bytes
    e:\dev_code\XX\src\streetviewroad\map_road_activity.cpp (56): glTest.exe!MapRoadActivity::RenderStreetviewRoad
    e:\dev_code\XX\src\streetviewroad\qstreetview_road_api.cpp (49): glTest.exe!QRenderStreetviewRoad
    e:\dev_code\XX\test\gltest\gl_map.cpp (403): glTest.exe!renderMapTile + 0x10 bytes
    e:\dev_code\XX\test\gltest\gl_map_pc.cpp (561): glTest.exe!renderMap + 0x20 bytes
    e:\dev_code\XX\test\gltest\gl_map_pc.cpp (801): glTest.exe!render + 0x32 bytes
    e:\dev_code\XX\test\gltest\gltest.cpp (832): glTest.exe!display + 0x48 bytes
    0x10004564 (File and line number not available): glut32.dll!glutMainLoop + 0x70F bytes
    0x10003E9B (File and line number not available): glut32.dll!glutMainLoop + 0x46 bytes
    e:\dev_code\map2.0\handmap\test\gltest\gltest.cpp (1285): glTest.exe!main
    f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c (582): glTest.exe!__tmainCRTStartup + 0x19 bytes
    f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c (399): glTest.exe!mainCRTStartup
    0x7669ED5C (File and line number not available): kernel32.dll!BaseThreadInitThunk + 0x12 bytes
    0x7750377B (File and line number not available): ntdll.dll!RtlInitializeExceptionChain + 0xEF bytes
    0x7750374E (File and line number not available): ntdll.dll!RtlInitializeExceptionChain + 0xC2 bytes
  Data:
    B5 04 00 00    12 01 00 00    00 CC 04 05    00 29 03 00     ........ .....)..
    29 0D 00 2B    10 00 2B 0C    00 2B 09 00    2B 0D 00 2B     )..+..+. .+..+..+
    0C 00 2B 05    00 2B 06 00    2B 03 00 2B    03 00 2B 02     ..+..+.. +..+..+.
    00 2B 15 00    2B 14 00 2B    09 00 2B 0A    00 2B 1C 00     .+..+..+ ..+..+..
    2C 29 00 2C    07 00 2C 06    00 2C 1E 00    2C 06 00 2C     ,).,..,. .,..,..,
    07 00 2C 08    00 2C 09 00    2C 08 00 2C    06 00 2C 09     ..,..,.. ,..,..,.
    00 2C 0B 00    2C 0A 00 2C    08 00 2C 04    00 2C 0B 00     .,..,.., ..,..,..
    2C 03 00 2C    06 00 2C 0F    00 2C 0E 00    2C 06 00 2C     ,..,..,. .,..,..,
    05 00 2C 03    00 2C 05 00    2C 05 00 2C    06 00 2C 06     ..,..,.. ,..,..,.
    00 2C 06 00    2C 06 00 2C    08 00 2C 0C    00 2C 06 00     .,..,.., ..,..,..
    2C 0A 00 2C    03 00 2C 0C    00 2C 0A 00    2C 08 00 2C     ,..,..,. .,..,..,
    05 00 2C 03    00 2C 04 00    2C 04 00 2C    05 00 2C 06     ..,..,.. ,..,..,.
    00 2C 04 00    2C 03 00 2C    05 00 2C 04    00 2C 05 00     .,..,.., ..,..,..
    2C 0D 00 2C    02 00 2C 02    00 2C 05 00    2C 04 00 2C     ,..,..,. .,..,..,
    04 00 2C 04    00 2C 04 00    2C 03 00 2C    04 00 2C 03     ..,..,.. ,..,..,.
    00 2C 05 00    2C 03 00 2C    05 00 2C 02    00 2C 05 00     .,..,.., ..,..,..
双击vs调试窗口的函数堆栈,可以直接调至对应的源码行,十分方便定位问题。

FIX了这个bug,内心也觉得深度ashamed,当初code review,总监还特意问我有没有自己做专业测试,我心里还很不服气,我coding时特别注意肯定不会。。。以后对于内存问题不能盲目自信,一定要用专业工具测试,及时在pc端发现问题。

3.尾声

引用《程序员修炼之道》中的一段话:

你有没有看过老式的黑白战争片?一个疲惫的士兵警觉地从灌木丛中钻出来,前面有一片空旷地,那里有地雷吗?还是可以安全通过?没有任何迹象表明那是一片雷区,没有标记,没有带刺的铁丝网,也没有弹坑,士兵用他的刺刀戳了戳前方的地面,又赶紧缩回来,以为会发生爆炸。没有发生爆炸,于是他紧张地向前走了一会儿,刺刺这里,戳戳那里,最后,他确信这地方是安全的,于是直起身来,骄傲地正步向前走去,结果被炸成了碎片。

士兵地起初探测没有发现地雷,但这不过是侥幸,于是他得出了错误地结论——结果是灾难性的。同样地道理,作为开发者,对于指针、内存相关问题,不能盲目自信,切记靠巧合编程,很多隐晦的问题指望自身或牛人的code review也很难发现,要有一套成熟的测试方案,使用专业的分析工具,否则就会像上文中的士兵,“死的“很惨。

指针分析工具下载地址:http://download.csdn.net/detail/dizuo/6860907


refer:http://*.com/questions/413477/is-there-a-good-valgrind-substitute-for-windows


【踩雷】指针惹的货

上一篇:[Raspberry Pi] [Model 4B] [BCM2711] SPI官方英文文档整理


下一篇:异或找唯一整数的程序是错的!