【踩雷】指針惹的貨

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://stackoverflow.com/questions/413477/is-there-a-good-valgrind-substitute-for-windows


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章