摘? 要:為了獲得最佳性能,C/C++語(yǔ)言把操縱內(nèi)存的權(quán)限以指針的方式暴露給開(kāi)發(fā)人員。但是C/C++語(yǔ)言編譯器GCC和Clang都不提供內(nèi)存安全檢測(cè),導(dǎo)致開(kāi)發(fā)人員使用C/C++語(yǔ)言編寫(xiě)的項(xiàng)目可能存在內(nèi)存泄漏的風(fēng)險(xiǎn)。文章介紹了改進(jìn)指針?biāo)惴ê蛃hared_ptr源碼,分析了它們檢測(cè)內(nèi)存泄漏的方式,最后指出該類算法的缺陷,提出改進(jìn)思路,并建議用戶避免寫(xiě)出該類算法無(wú)法檢測(cè)的代碼結(jié)構(gòu)。
關(guān)鍵詞:C/C++程序;內(nèi)存泄漏;改進(jìn)指針?biāo)惴?shared_ptr
中圖分類號(hào):TP311 ? ? 文獻(xiàn)標(biāo)識(shí)碼:A文章編號(hào):2096-4706(2021)22-0098-03
Abstract: In order to obtain the best performance, C/C++ language exposes the permission to manipulate memory to developers in the form of pointers. However, the C/C++ language compiler GCC and Clang do not provide memory security detection, which leads to the risk of memory leakage for projects written by developers in C/C++ language. This paper introduces the improved pointer algorithm and shared_ptr source code, analyzes their ways to detect memory leakage, finally points out the defects of this kind of algorithm and puts forward improvement ideas, and advises users to avoid writing code structures that cannot be detected by such algorithms.
Keywords: C/C++ program; memory leak; improved pointer algorithm; shared_ ptr
0? 引? 言
隨著使用C/C++語(yǔ)言所構(gòu)建項(xiàng)目規(guī)模的不斷增大,內(nèi)存安全也越來(lái)越受重視。內(nèi)存泄漏往往在程序運(yùn)行中就發(fā)生,不易被發(fā)現(xiàn)和修改。相較于其他運(yùn)行于虛擬機(jī)上的語(yǔ)言(如Python和Java),C/C++為了實(shí)現(xiàn)對(duì)內(nèi)存的細(xì)粒度的操作,沒(méi)有設(shè)計(jì)垃圾收集器。因此,使用C/C++編寫(xiě)項(xiàng)目時(shí),開(kāi)發(fā)人員需要格外注意內(nèi)存的申請(qǐng)和釋放。本文介紹了改進(jìn)指針?biāo)惴╗1-3]和shared_ptr[4,5]源碼,分析了它們對(duì)內(nèi)存泄漏的檢測(cè)方式,并指出它們所存在的不足。希望讀者可以通過(guò)改進(jìn)指針?biāo)惴ɑ騭hare_ptr來(lái)規(guī)避內(nèi)存泄漏,盡量不要寫(xiě)出連檢測(cè)算法也無(wú)法處理的代碼結(jié)構(gòu)。
1? 內(nèi)存泄漏檢測(cè)原理分析
1.1? 改進(jìn)指針?biāo)惴?/p>
改進(jìn)指針?biāo)惴ㄊ且环N新的內(nèi)存安全性動(dòng)態(tài)分析方法,采用源碼插樁技術(shù)實(shí)現(xiàn),支持復(fù)雜的動(dòng)態(tài)內(nèi)存檢查。改進(jìn)指針?lè)椒ǖ淖畲髣?chuàng)新點(diǎn)是分別為每一個(gè)內(nèi)存對(duì)象和指針創(chuàng)建和維護(hù)一個(gè)狀態(tài)節(jié)點(diǎn)數(shù)據(jù)(status node data, snd)和指針元數(shù)據(jù)(pointer matedata, pmd)。該方法不僅可以在程序運(yùn)行時(shí)記錄每個(gè)指針指向內(nèi)存對(duì)象的邊界信息,還可以記錄內(nèi)存對(duì)象所對(duì)應(yīng)snd的狀態(tài)和計(jì)數(shù)信息。snd的狀態(tài)(stat)是指對(duì)應(yīng)內(nèi)存對(duì)象的內(nèi)存類型,如無(wú)效(invalid)、棧(heap)、全局(global)、靜態(tài)(static)和函數(shù)(function)等,snd的定義為
1: typedef enum{
2:? ?invalid, heap,global,...
3: } status;
4: typedef struct{
5:? ?status stat;size_t count; 7: } SND;
由于多個(gè)指針引用同一塊內(nèi)存,這些指針?biāo)鶎?duì)應(yīng)的指針元數(shù)據(jù)將共享同一個(gè)snd。通過(guò)snd的count變量判斷出該內(nèi)存對(duì)象無(wú)用后,其所對(duì)應(yīng)的狀態(tài)節(jié)點(diǎn)也會(huì)足夠“智能”地自我銷(xiāo)毀,不會(huì)常駐內(nèi)存。
指針pmd是改進(jìn)指針?biāo)惴ǖ闹匾獢?shù)據(jù)結(jié)構(gòu),用來(lái)存儲(chǔ)運(yùn)行時(shí)指針相關(guān)信息。改進(jìn)指針?biāo)惴ㄔ诔绦蜻\(yùn)行時(shí)為每個(gè)指針變量維護(hù)一個(gè)pmd。pmd結(jié)構(gòu)存儲(chǔ)對(duì)應(yīng)指針?biāo)脙?nèi)存對(duì)象的基地址(base)、邊界地址(bound)以及內(nèi)存對(duì)象的snd地址,pmd結(jié)構(gòu)定義為:
1: typedef struct{
2:? ? ?void *base;
3:? ? ?void *bound;
4:? ? ?SND *snda;
5: }PMD;
改進(jìn)指針?biāo)惴ㄔ跈z查內(nèi)存泄漏上可以做到非常細(xì)粒度,這是其他工具所不具備的優(yōu)勢(shì)。第2行執(zhí)行賦值語(yǔ)句后,指針p2和p1同時(shí)指向同一塊8個(gè)字節(jié)的堆內(nèi)存,它們的pmd共享同一個(gè)snd:
1:p1 = (int*)malloc(8);2:p2 = p1;3:int i; p1 = &i;4:p2 = &i; /*mem leak*/
第3行賦值語(yǔ)句執(zhí)行后,狀態(tài)如圖1(a)所示,p1指針指向變量i的首地址,pmd引用了變量i的snd,因此指向堆內(nèi)存的指針減1,相應(yīng)snd的count也減1。第4行賦值語(yǔ)句執(zhí)行后,狀態(tài)如圖1(b)所示。p2指針不再指向堆內(nèi)存,p2指針pmd引用變量i的snd,此時(shí)8個(gè)字節(jié)的堆內(nèi)存沒(méi)有指針指向它,因此其snd的count值為0。在檢查到堆內(nèi)存的count值為0后,內(nèi)存泄漏的錯(cuò)誤將被報(bào)出,因?yàn)闆](méi)有指針指向這塊內(nèi)存。
1.2? shared-_ptr原理分析
閱讀C++STL源碼可知,shared_ptr的_M_use_count變量值為0是判斷內(nèi)存泄漏的必要條件,shared_ptr部分重要源碼:
1:? __shared_count<_Lp>? _M_refcount;? ? ?// Reference counter
2: _Sp_counted_base<_Lp>*? _M_pi; //__shared_count類私有變量
3: typedef int _Atomic_word; ? ? ?//C++標(biāo)準(zhǔn)庫(kù)的GNU擴(kuò)展文件atomic_word.h
4:? _Atomic_word? _M_use_count;? ? ? ? // _Sp_counted_base類私有變量
5: inline void _Sp_counted_base<_S_single>::_M_add_ref_copy()
6: { ++_M_use_count; } ? ? ?//_Sp_counted_base類的內(nèi)聯(lián)函數(shù)
7: _Sp_counted_base<_S_single>::_M_release() noexcept
8: if (--_M_use_count == 0){
9: _M_dispose();? ? ? //當(dāng)_M_use_count自減到0,釋放資源? ? ?...}
代碼均來(lái)自C++11標(biāo)準(zhǔn)模板庫(kù)源文件。
_M_refcount是shared_ptr模板類的成員變量,它是用于處理引用計(jì)數(shù)最核心的變量。_M_refcount的類型__shared_count也是一個(gè)模板類,這個(gè)類有一個(gè)私有指針變量_M_pi,所有指向同一動(dòng)態(tài)對(duì)象的shared_ptr都共享同一個(gè)_M_pi變量,如第2行所示。_M_pi變量指向的_Sp_counted_base類型有一個(gè)int類型的_M_use_count變量,如第3、第4行所示。_M_use_count變量表示引用數(shù),每當(dāng)有新的shared_ptr通過(guò)函數(shù)調(diào)用或拷貝等操作指向同一個(gè)動(dòng)態(tài)對(duì)象時(shí),_M_pi變量都會(huì)調(diào)用如第5、第6行所示的內(nèi)聯(lián)函數(shù)_M_add_ref_copy,將_M_use_count值加1。當(dāng)指向某動(dòng)態(tài)對(duì)象的shared_ptr不再指向該動(dòng)態(tài)對(duì)象時(shí),其析構(gòu)函數(shù)會(huì)使_M_pi變量調(diào)用_M_release(),將_M_use_count值減1,_M_release()也會(huì)在此時(shí)判斷無(wú)用動(dòng)態(tài)對(duì)象。如第7~9行所示,_M_release()函數(shù)調(diào)用將M_use_count值減1后示,若_M_use_count值為0,則表示最后一個(gè)指向該動(dòng)態(tài)對(duì)象的shared_ptr被銷(xiāo)毀或最后一個(gè)指向該動(dòng)態(tài)對(duì)象的shared_ptr通過(guò)操作符“=”或reset函數(shù)調(diào)用被賦值為其他值,檢測(cè)產(chǎn)生內(nèi)存泄漏,因此調(diào)用_M_dispose()來(lái)釋放無(wú)用動(dòng)態(tài)對(duì)象。
2? 無(wú)法檢測(cè)的內(nèi)存泄漏介紹
通過(guò)改進(jìn)指針?biāo)惴ê蛃hared_ptr判斷的堆內(nèi)存對(duì)象泄漏都是依據(jù)引用計(jì)數(shù)值“PREFIXcount”為0來(lái)判斷的。然而,我們?cè)谡{(diào)試專業(yè)測(cè)試集時(shí)發(fā)現(xiàn)當(dāng)“PREFIXcount”值大于0時(shí),也存在內(nèi)存泄漏和無(wú)用動(dòng)態(tài)對(duì)象。在程序中的表現(xiàn)為:存在數(shù)量大于等于1的指針指向該內(nèi)存對(duì)象,但是這些指針無(wú)法獲取,從而導(dǎo)致內(nèi)存泄漏?!爸羔樧灾浮本褪瞧渲幸环N:
/*point-self*/
1:#include <malloc.h>
2:int main()
3:{
4: int **m, i = 5;
5: m = malloc(sizeof(int*)*6);
6: m[i] = (int*)m;
7: m = 0;? /* mem leak*/
8: return 0;}
第5行賦值語(yǔ)句執(zhí)行后,指針m指向一塊容納6個(gè)“int*”類型變量的內(nèi)存對(duì)象首地址。第6行賦值語(yǔ)句執(zhí)行后,m[i]相當(dāng)于*(m+5)向該內(nèi)存尾部空間寫(xiě)進(jìn)了m變量的值(即該內(nèi)存首地址)。當(dāng)?shù)?行中的m變量通過(guò)賦值語(yǔ)句指向它時(shí),會(huì)發(fā)生內(nèi)存泄漏,狀態(tài)如圖2所示。雖然仍然有指針指向該堆內(nèi)存對(duì)象,但是指向它的指針來(lái)自于自身空間的內(nèi)部指針,由于任何方式都無(wú)法獲取內(nèi)部指針,因此導(dǎo)致內(nèi)存泄漏。
另一種“循環(huán)引用”也會(huì)導(dǎo)致這種內(nèi)存泄漏:
/*Memory leaks on memory circles.*/
1:#include <malloc.h>
2:typedef struct st {
3:? ? int i;
4:? ?struct st *next;
5:} st;
6:int main(){
7: st *m, *n;
8: m = malloc(sizeof(st));
9: n = malloc(sizeof(st));
10: n->next = m;
11: m->next = n; //構(gòu)成循環(huán)狀態(tài)
12: m=0;
13: n=0;? ? ? ? /*mem leak*/ return 0;}
循環(huán)引用代碼
第8、第9行賦值語(yǔ)句執(zhí)行后,指針m和指針n分別指向一個(gè)容納st類型變量的內(nèi)存對(duì)象首地址。第10、第11行使這兩個(gè)內(nèi)存對(duì)象的內(nèi)部指針“struct st *next”形成互指彼此內(nèi)存對(duì)象的狀態(tài),因此在第12、第13行執(zhí)行后,引用這兩個(gè)內(nèi)存對(duì)象的指針只來(lái)自彼此內(nèi)部,形成了一個(gè)沒(méi)有起點(diǎn)的環(huán),此時(shí)的狀態(tài)如圖3所示。因此在程序運(yùn)行到圖2和圖3這兩種狀態(tài)時(shí),改進(jìn)指針?biāo)惴ê蛃hared_ptr的“計(jì)數(shù)值”都大于0,根據(jù)它們“計(jì)數(shù)值”大于0的描述,這個(gè)內(nèi)存對(duì)象是有用的,但是事實(shí)上已經(jīng)存在內(nèi)存泄漏。
3? 改進(jìn)方案
雖然C++1x提供了一個(gè)解決方案,但是該方案過(guò)于依賴用戶,需要開(kāi)發(fā)人員在使程序構(gòu)成循環(huán)時(shí),利用weak_ptr的弱引用替換部分shared_ptr的強(qiáng)引用,程序編寫(xiě)者通過(guò)手動(dòng)破壞循環(huán)結(jié)構(gòu)來(lái)解決shared_ptr的設(shè)計(jì)缺陷。我們需要實(shí)現(xiàn)自動(dòng)化和智能化的工具,目的是解決人為原因帶來(lái)的易錯(cuò)性、低效性和不可靠性問(wèn)題,靠開(kāi)發(fā)人員自己發(fā)現(xiàn)程序特定缺陷并做出相應(yīng)修改是非常低效的。
shared_ptr的設(shè)計(jì)丟失了動(dòng)態(tài)對(duì)象信息,所有指向同一個(gè)動(dòng)態(tài)對(duì)象的指針都共享同一個(gè)_Sp_counted_base類型_M_pi,但是_Sp_counted_base類私有變量只有“計(jì)數(shù)值”和一系列用來(lái)更新“計(jì)數(shù)值”的接口。判斷程序是否存在“指針自指”,需要獲取內(nèi)存對(duì)象的信息,由于shared_ptr的功能單一和對(duì)高性能的追求,并沒(méi)有記錄動(dòng)態(tài)對(duì)象的具體信息。相較于shared_ptr的設(shè)計(jì),改進(jìn)指針?biāo)惴ㄓ涗浀膬?nèi)存對(duì)象信息更加具體和完善。用pmd和snd的聯(lián)接,模擬了指針指向內(nèi)存對(duì)象的狀態(tài),指針pmd包含了所指內(nèi)存對(duì)象的基地址和邊界。因此改進(jìn)指針?biāo)惴ㄓ涗浀臓顟B(tài)信息更用于實(shí)現(xiàn)對(duì)該類型錯(cuò)誤的檢測(cè)。
根據(jù)改進(jìn)指針?biāo)惴ǖ脑创a,我們提出完善改進(jìn)指針?biāo)惴ǖ臋z測(cè)接口,在指針pmd與snd將要解綁時(shí),執(zhí)行“指針自指”算法:掃描存儲(chǔ)pmd的hash表,查找當(dāng)前“解綁”pmd記錄的邊界內(nèi)自指指針數(shù)和對(duì)應(yīng)snd的count是否相同,若相同snd則可判斷出發(fā)生“指針自指”,導(dǎo)致內(nèi)存泄漏。然后采用深度遍歷的方式,逐個(gè)解綁該內(nèi)存泄漏范圍內(nèi)的指針pmd。
4? 結(jié)? 論
利用C/C++語(yǔ)言編寫(xiě)項(xiàng)目后,可以使用改進(jìn)指針?biāo)惴ê蛃hared_ptr來(lái)檢測(cè)普通的內(nèi)存泄漏。完善后的改進(jìn)指針?biāo)惴ㄒ部梢詸z測(cè)“指針自指”發(fā)生的內(nèi)存泄漏,但是盡量避免編寫(xiě)“循環(huán)引用”代碼結(jié)構(gòu),因?yàn)楸疚恼撟C了該結(jié)構(gòu)導(dǎo)致的內(nèi)存泄漏,目前C/C++尚未找到一個(gè)較為完備的檢測(cè)算法來(lái)對(duì)內(nèi)存泄漏進(jìn)行檢測(cè)和預(yù)防。甚至是Google公司開(kāi)發(fā)的內(nèi)存安全性動(dòng)態(tài)分析工具AddressSanitizer和國(guó)內(nèi)基于改進(jìn)指針?biāo)惴▽?shí)現(xiàn)的Movec都無(wú)法完全檢測(cè)出C/C++程序中“循環(huán)引用”導(dǎo)致的內(nèi)存泄漏。
參考文獻(xiàn):
[1] 朱云龍.C程序運(yùn)行時(shí)監(jiān)控和驗(yàn)證的插樁方法研究與應(yīng)用 [D].南京:南京航空航天大學(xué),2016.
[2] CHEN Z,TAO C Q,ZHANG Z Y,et al. Poster: Beyond Spatial and Temporal Memory Safety [C]//2018 IEEE/ACM 40th International Conference on Software Engineering: Companion (ICSE-Companion). Gothenburg:IEEE,2018:189-190.
[3] 嚴(yán)峻琦.C程序內(nèi)存安全錯(cuò)誤的運(yùn)行時(shí)檢測(cè)技術(shù)研究與實(shí)現(xiàn) [D].南京:南京航空航天大學(xué),2017.
[4] 葉蓉,陳榕.運(yùn)用CAR智能指針實(shí)現(xiàn)Callback機(jī)制 [J].計(jì)算機(jī)技術(shù)與發(fā)展,2008(2):9-12+16.
[5] 張彤,何源.一種自適應(yīng)的引用計(jì)數(shù)智能指針的實(shí)現(xiàn) [J].成都大學(xué)學(xué)報(bào)(自然科學(xué)版),2007(1):55-57.
作者簡(jiǎn)介:仵?。?997—),男,漢族,江蘇南京人,碩士研究生在讀,研究方向:軟件驗(yàn)證。