概述:C語言的優(yōu)勢是可以直接訪問內(nèi)存地址,也就是指針操作,但其缺陷也是因為直接內(nèi)存訪問。如何通過防御性編程提前發(fā)現(xiàn)問題,盡可能減少內(nèi)存異常產(chǎn)生的后果,就是本文的重點。
1、內(nèi)存劃分
一般內(nèi)存區(qū)域劃分五段:
棧區(qū)(stack)有時也稱為堆棧,重點在棧字,存放函數(shù)內(nèi)部臨時變量
堆區(qū)(heap)也就是動態(tài)申請(malloc)、釋放(free)的內(nèi)存區(qū)域
數(shù)據(jù)區(qū)(data)初始化的全局變量和靜態(tài)變量, 占用可執(zhí)行文件空間;rodata 固定不變const修飾的全局變量,不占內(nèi)存空間
bss區(qū)未初始化的全局變量、靜態(tài)變量(static關(guān)鍵字描述的),初始化為全0的全局變量,不占用可執(zhí)行文件大小
代碼區(qū)(text)程序二進制文件
最終下載的可執(zhí)行文件包括代碼(text)和數(shù)據(jù)(data)。內(nèi)存的分配一般如下圖:
其中堆和棧的地址分配方向相反,棧比較特殊,下面以??臻g異常使用為例:
?
#include?int?main(void) { ????int?a=100; ????int?b[3]={0}; ????int?c=200; ????printf("ori>?a[%p]=%d,c[%p]=%d ",&a,a,&c,c); ????printf("???>?b[%p] ",&b); ????b[0]=0; ????b[1]=1; ????b[2]=2; ????b[3]=3;//error?->a ????printf("new>?a[%p]=%d,c[%p]=%d ",&a,a,&c,c); ????return?0; }
?
運行結(jié)果:
?
ori>?a[0028FEBC]=100,c[0028FEAC]=200 ???>?b[0028FEB0] new>?a[0028FEBC]=3,c[0028FEAC]=200
?
結(jié)合打印的變量地址,棧空間分配如下圖,因為數(shù)組b的操作越界,導致了變量a的值被覆蓋。
針對個人情況,一般情況下內(nèi)存溢出都是使用數(shù)組越界,所以在異常值后或者前查看有沒數(shù)組(全局變量可以查map文件),檢查數(shù)組的操作是否正確。
除了堆區(qū),其他幾個區(qū)都是有編譯器和系統(tǒng)運行時自動處理的,而堆區(qū)由開發(fā)者來操作的。這既是便利,也是隱患,一旦操作失誤就是內(nèi)存泄漏或溢出。
2、動態(tài)內(nèi)存管理
在硬件資源固定的情況下,棧和堆的空間此消彼長,合理的定義堆的空間,為不同任務分配合適的棧空間也是至關(guān)重要的。以FreeRTOS內(nèi)核代碼為例,《FreeRTOS及其應用》分別解讀其5種動態(tài)內(nèi)存,也就是堆的分配方式,其他系統(tǒng)的原理差不多。
FreeRTOS 內(nèi)核提供了 5 種內(nèi)存管理算法,源文件在SourceportableMemMang 下,使用時選擇其中一個。
heap_1.c內(nèi)存管理方案簡單,它只能申請內(nèi)存而不能進行內(nèi)存釋放。
一些低端嵌入式系統(tǒng)并不會經(jīng)常動態(tài)申請與釋放內(nèi)存,在系統(tǒng)啟動后申請,一直使用下去,永不釋放,適合這種方式,也可近似理解為多個全局小數(shù)組合并的使用。
heap_2.c?方案支持申請和釋放,但是它不能把相鄰的兩個小的內(nèi)存塊合成一個大的內(nèi)存塊, 隨著不斷的申請釋放,空閑空間會分割為很多小片段,如下圖
持續(xù)申請、釋放一定次數(shù),就會出現(xiàn)剩余空間的和較大,但卻申請不到內(nèi)存的情況,如上圖剩余空間是900,但無法申請600,因為沒有連續(xù)的600空間。如果每次申請內(nèi)存大小都是固定的,就不存在內(nèi)存碎片問題,但實際不會這樣,因此不推薦。
heap_3.c?方案只是封裝了標準 C 庫中的 malloc()和 free()函數(shù),由編譯器提供,需要通過編譯器或者啟動文件設置堆空間,封裝是為了保證線程安全。
heap_4.c?方案是在heap_2.c 基礎上,對內(nèi)存碎片進行了改進。
如圖E到F,用戶釋放后,把相鄰的空閑的內(nèi)存塊合并成一個更大的塊,這樣可以減少內(nèi)存碎片。
heap_5.c?方案在實現(xiàn)動態(tài)內(nèi)存分配時與 heap4.c 方案一樣,采用最佳匹配算法和合并算法,并且允許內(nèi)存堆跨越多個非連續(xù)的內(nèi)存區(qū),也就是允許在不連續(xù)的內(nèi)存堆中實現(xiàn)內(nèi)存分配,比如做圖形顯示,可能芯片內(nèi)部的 RAM 不足,額外擴展SDRAM,這種內(nèi)存管理方案則比較合適。
一般選用heap_4.c。
3、動態(tài)內(nèi)存防御性編程
內(nèi)存只申請不釋放,運行一段時間會因為內(nèi)存不足而無法運行,即內(nèi)存泄露;或者操作的內(nèi)存區(qū)域超出了申請的空間,訪問越界即內(nèi)存溢出,導致各種隨機異常。對于內(nèi)存操作的不穩(wěn)定因素,如何進行防御性編程,可以在調(diào)試階段發(fā)現(xiàn)問題?
簡單的說就是內(nèi)存分配的時候,記錄申請內(nèi)存的函數(shù)名(或者擴展加上申請時間),申請內(nèi)存大小的基礎上額外增加空間,在其首尾加入特殊的標志位,釋放該內(nèi)存前對標志位進行校驗;如果校驗不通過,則將申請該內(nèi)存的函數(shù)名打印出來,表示出現(xiàn)了內(nèi)存溢出。也支持隨時打印當前動態(tài)內(nèi)存的使用情況,查看某些函數(shù)申請的內(nèi)存釋放一直未被釋放,人工判斷是否內(nèi)存泄露。
下面是完整源碼:
?
//pal_memory.h #ifndef?_PAL_MEMORY_H #define?_PAL_MEMORY_H //配置是否開啟內(nèi)存記錄功能 #define?__MEMORY_DEBUG__ typedef?unsigned?char???uint8_t; typedef?unsigned?int????uint32_t; extern?void?*chengj_pal_memory_malloc(uint32_t?size,?const?char?*func); extern?void?chengj_pal_memory_free(void?**pv); extern?void?chengj_pal_memory_record_print(void); #define?chengj_malloc(size)?????chengj_pal_memory_malloc(size,?__FUNCTION__) #define?chengj_free(pv)?????????chengj_pal_memory_free(&pv) #endif??/*?_PAL_MEMORY_H?*/
?
具體實現(xiàn):
?
/********************************************************************** ?*? ?*?Copyright(c)??embedded-systems rights reserved ?*? ?*?Description: ?*????????memory management? ?* ?*??????[微信公眾號:?嵌入式系統(tǒng)] ?*? ?*********************************************************************/ #include?#include? #include?"pal_memory.h" //適配平臺內(nèi)存管理接口 #define?PAL_MALLOC??malloc #define?PAL_FREE????free #if?defined?(__MEMORY_DEBUG__) #define?MEMORY_RECORD_COUNT_MAX 100 //len[4]+head[4]+...[data]...+tail[2] #define?MEMORY_EXTRA_SIZE 10 //magic #define?MEMORY_DATA_MAGIC_HEAD 0x43 #define?MEMORY_DATA_MAGIC_TAIL 0x4A typedef?struct { ????const?char?*func_name; ????void?*pointer; ????//可擴展保存?時間戳?等信息 }?memory_record_struct; //記錄申請內(nèi)存的函數(shù) static?memory_record_struct chengj_memory_record[MEMORY_RECORD_COUNT_MAX]?=?{0}; #endif?/*?__MEMORY_DEBUG__?*/ /* ?*輸出未被釋放的申請函數(shù)名和指針地址 ?*/ void?chengj_pal_memory_record_print(void) { #if?defined?(__MEMORY_DEBUG__) ????uint32_t?i?=?0; ????for(;?i?>?24)?&?0xFF; ????pdata[1]?=?(size?>>?16)?&?0xFF; ????pdata[2]?=?(size?>>?8)?&?0xFF; ????pdata[3]?=?size?&?0xFF; ????pdata[4]?=?MEMORY_DATA_MAGIC_HEAD; ????pdata[5]?=?MEMORY_DATA_MAGIC_HEAD; ????pdata[6]?=?MEMORY_DATA_MAGIC_HEAD; ????pdata[7]?=?MEMORY_DATA_MAGIC_HEAD; ????pdata[size?-?2]?=?MEMORY_DATA_MAGIC_TAIL; ????pdata[size?-?1]?=?MEMORY_DATA_MAGIC_TAIL; ????for(;?i? ?
可以測試下效果:
?
#include?"pal_memory.h" //微信公眾號:?嵌入式系統(tǒng) //申請10字節(jié)但使用20字節(jié) void?test(void) { ????uint8_t?*p; ????uint8_t?i; ????p=chengj_malloc(10); ????for(i=0;i<20;i++) ????{ ????????p[i]=i; ????} ????chengj_free(p); } int?main(int?argc,?char?*argv[]) { ????printf("embedded-system? "); ????test(); ????return?0; }?
運行結(jié)果:
?
embedded-system memory error?0x04?test()?
表示test函數(shù)內(nèi)申請的一段內(nèi)存使用時溢出,尾部標記數(shù)據(jù)被覆蓋。
也可以在memory_record_struct增加時間戳成員,記錄內(nèi)存申請時間,再擴展void chengj_pal_memory_record_print(void) 打印內(nèi)存使用情況,查看長時間申請未釋放的內(nèi)存使用情況。
4、小結(jié)
內(nèi)存記錄調(diào)試方法,浪費了一定量的內(nèi)存空間,而且不能排除問題,只是提早監(jiān)測到異常,但對軟件穩(wěn)定性仍有較大意義,可以快速解決內(nèi)存問題。建議只在debug版本啟用,正式發(fā)布的release版本關(guān)閉記錄功能。
審核編輯:湯梓紅
評論
查看更多