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