(許繼電氣股份有限公司,河南 許昌 461000)
C++中堆內(nèi)存的泄漏就是業(yè)界通稱的內(nèi)存泄漏。堆內(nèi)存的特點是允許編程人員自由分配內(nèi)存大小,但編譯器不會自動釋放,需要編程人員自己釋放,也就是“誰申請,誰管理”,堆內(nèi)存的這個特點方便編程人員靈活申請內(nèi)存的同時,也給內(nèi)存管理帶來了隱患。編程人員需要申請內(nèi)存時,只需要使用malloc,new等內(nèi)存申請函數(shù),如果函數(shù)執(zhí)行成功,就能順利從堆中分配到所需要的內(nèi)存。但申請的內(nèi)存在使用完后,一定要有對應(yīng)的釋放內(nèi)存操作,釋放內(nèi)存的函數(shù)主要有free、delete等。如果沒有釋放,就會發(fā)生內(nèi)存泄漏,如果多次釋放會導(dǎo)致更嚴(yán)重的問題。內(nèi)存泄漏的發(fā)生往往會影響程序性能;更有甚者影響到整個軟件運行環(huán)境,如果內(nèi)存泄漏問題嚴(yán)重到耗盡系統(tǒng)的內(nèi)存資源,整個軟件系統(tǒng)都會癱瘓,那造成的后果是無法估量的。如果內(nèi)存釋放多了,會導(dǎo)致軟件莫名奇妙的問題,因為多釋放的那塊內(nèi)存存儲的信息是隨機的。總之,內(nèi)存問題至關(guān)重要,這關(guān)系到系統(tǒng)能否安全可靠運行的問題。
Qt繼承了C++語言動態(tài)分配內(nèi)存機制,保證了開發(fā)人員能根據(jù)實際需要靈活地使用內(nèi)存,同時Qt也不可避免的要面對“內(nèi)存泄漏”這個嚴(yán)重威脅軟件安全的問題,雖然Qt采取了半自動化內(nèi)存管理機制等措施,但不能從根本上解決問題。針對內(nèi)存泄漏問題,市場上出現(xiàn)了很多針對C++內(nèi)存泄漏檢測的工具,由于Qt的特殊性,針對C++的內(nèi)存檢測工具,不適應(yīng)Qt程序的測試。所以,針對Qt的內(nèi)存泄漏檢測方法及工具問題已成為當(dāng)前測試領(lǐng)域的一個急需解決的問題。
Qt是一個跨平臺的基于 C++ 編程語言的圖形用戶界面應(yīng)用程序框架,該框架提供給應(yīng)用程序開發(fā)者建立藝術(shù)級的圖形用戶界面所需的按鈕、滾動條、菜單及其他對象等功能[1]。我們在讀Qt程序時不難發(fā)現(xiàn),Qt程序中申請內(nèi)存的地方很多,但很少出現(xiàn)delete等有程序開發(fā)人員主動釋放的函數(shù)。其實,詳細(xì)讀代碼會進(jìn)一步發(fā)現(xiàn)Qt程序有自己的特點,凡是程序開發(fā)人員沒有手動釋放的內(nèi)存,指向它的指針都是QObject類或該類的繼承類。通過深入研究Qt內(nèi)存管理發(fā)現(xiàn),Qt本身有一套半自動化管理機制。該機制有一個顯著特點是,只要程序開發(fā)人員申請的內(nèi)存交給QObject類或繼承類托管,就不需要人工釋放,QObject類中有自動釋放堆內(nèi)存的機制。但同時要求,一旦申請的內(nèi)存交給托管人,就不能再人為釋放,釋放就會錯誤。
Qt的半自動化管理機制還有另一種體現(xiàn)形式父子繼承關(guān)系,比如,一個類A的子類B申請了一塊堆內(nèi)存,如果類A釋放了內(nèi)存,子類B就不用再釋放,就是父類可以統(tǒng)一托管、釋放子類的內(nèi)存。除此之外,Qt的內(nèi)存管理還有其他幾個特點。(1)Qt針對QWidget類及其子類有一個特殊的內(nèi)存自動釋放標(biāo)志位,在申請QWidget類及子類的內(nèi)存時只要設(shè)置了Qt::WA_DeleteOnClose標(biāo)志位,就不用再手動釋放內(nèi)存了。(2)針對QAbstractAnimation類及其子類有一個特殊的內(nèi)存自動釋放標(biāo)志位,在申請QAbstractAnimation類及子類的內(nèi)存時只要設(shè)置了QAbstractAnimation::DeleteWhenStopped標(biāo)志位,就不用再手動釋放內(nèi)存了。(3)針對QRunnable類及其子類有一個特殊的內(nèi)存自動釋放標(biāo)志位,在申請QRunnable類及子類的內(nèi)存時只要設(shè)置了QRunnable::setAutoDelete()和MediaSource::setAutoDelete()標(biāo)志位,就不用再手動釋放內(nèi)存了。
文獻(xiàn)[2-4]在2017年提出的一種基于Qt的系統(tǒng)內(nèi)存泄漏檢測方法只解決了QObject的類及其繼承的類相關(guān)的內(nèi)存泄漏檢測問題,還有相當(dāng)一大部分Qt內(nèi)存泄漏問題沒有解決。文中詳細(xì)給出了Qt內(nèi)存泄漏的靜態(tài)檢測方法,并基于該方法設(shè)計開發(fā)了靜態(tài)檢測工具。
在Qt程序中,如果發(fā)現(xiàn)一個malloc,realloc,new等函數(shù),首先檢查該函數(shù)中是否包含WA_DeleteOnClose、DeleteWhenStopped、setAutoDelete()、parent等關(guān)鍵字。如果有這些關(guān)鍵字,則不需要再進(jìn)行分析下去,可以確定該處的內(nèi)存不需要人工釋放;如果有這些關(guān)鍵字,又發(fā)現(xiàn)對應(yīng)的free或delete,則認(rèn)為是重復(fù)釋放內(nèi)存問題,屬于嚴(yán)重問題。如果沒有發(fā)現(xiàn)這些關(guān)鍵字,接著檢查使用該函數(shù)申請內(nèi)存的對象是否有父控件[5-8],如果有父控件則也認(rèn)為該處申請的內(nèi)存不需要人工釋放,如果發(fā)現(xiàn)人工釋放的free或delete函數(shù),認(rèn)為是重復(fù)釋放內(nèi)存問題。如果既沒有parent、WA_DeleteOnClose、DeleteWhenStopped、setAutoDelete()等關(guān)鍵字,也沒有發(fā)現(xiàn)父控件。則按C++的內(nèi)存泄漏檢測方法檢查,申請多少內(nèi)存,用后一定要釋放多少內(nèi)存,一定要確保申請與釋放的內(nèi)存,大小相等,位置相同,否則認(rèn)為內(nèi)存處理異常。該內(nèi)存泄漏檢測方法的具體流程如圖1所示。
圖1 Qt程序內(nèi)存泄漏檢測方法實現(xiàn)圖
基于文中所提出的Qt內(nèi)存泄漏的檢測方法設(shè)計開發(fā)了測試工具。
3.1.1 靜態(tài)測試工具的功能設(shè)計
靜態(tài)測試工具的功能結(jié)構(gòu)如圖2所示,工具主要有靜態(tài)分析和結(jié)果展示兩個大模塊,其中,靜態(tài)分析模塊包含文件分析模塊、工程分析模塊兩個子模塊;結(jié)果展示模塊包含界面展示模塊、問題報告生成模塊等模塊。
圖2 靜態(tài)檢測工具功能結(jié)構(gòu)圖
該靜態(tài)檢測工具的程序分析模塊主要功能是實現(xiàn)支持單個Qt程序文件(單個.cpp或.c文件)加載和支持整個工程加載,這里主要支持QtCreator工程加載;如果是QtCreator工程加載,其分析過程相對復(fù)雜些,首先,對工程的文件類型進(jìn)行分類,找出需要分析的.cpp文件和.h文件,然后對.cpp文件進(jìn)行重點分析,通過逐行掃描.cpp文件,找出文件中的變量信息、函數(shù)信息、程序語句結(jié)構(gòu)信息和內(nèi)存信息。其中,變量信息主要是指變量的類型,變量的賦值情況等;函數(shù)信息主要是指函數(shù)的調(diào)用關(guān)系、函數(shù)的類型,參數(shù)等;程序語句結(jié)構(gòu)信息主要是指程序的數(shù)據(jù)流程等關(guān)鍵信息;內(nèi)存信息就是本文所重點關(guān)注的信息,主要是指內(nèi)存泄漏或內(nèi)存重復(fù)釋放等問題。在掃描.cpp文件的過程中,如果有復(fù)雜的結(jié)構(gòu)體、類等需要頭文件的,直接查找相應(yīng)的頭文件進(jìn)行分析獲取相關(guān)信息。如果是.cpp文件加載,其分析過程相對簡單,只需要重復(fù)工程分析中對.cpp文件的分析過程即可,這里不在贅述。需要說明的是再加載.cpp文件的時候,如果有對應(yīng)的頭文件,也一并加載上,以保證分析程序的順利進(jìn)行。
結(jié)果展示模塊的主要功能是實現(xiàn)對分析結(jié)果的界展示,從展示形式上又劃分為界面形式的展示和報告形式的展示。其中,界面形式的展示主要包括:1)被測源代碼及定位問題的展示,主要方便測試人員現(xiàn)場分析并定位問題;2)變量信息的集中展示,包括變量是需要賦值的或給其他變量的賦值的信息等,主要方便測試人員對整體被測程序變量的分析特別是整個QtCreator工程加載時,特別有用;3)函數(shù)調(diào)用關(guān)系的展示,可以方便測試人員對整個測試源代碼的整體函數(shù)關(guān)系有個了解,便于分析和理解程序;4)程序語句結(jié)構(gòu)展示,主要是程序數(shù)據(jù)流程圖的展示,方便測試人員進(jìn)一步動態(tài)測試用。5)內(nèi)存問題展示,這個是界面展示功能的核心所在,主要是展示工具靜態(tài)分析中發(fā)現(xiàn)的疑似內(nèi)存泄漏或內(nèi)存重復(fù)釋放的問題,以供測試人員分析。報告生成模塊主要是對工具發(fā)現(xiàn)的疑似問題生成一個Word文檔的報告,用于報告形式展示靜態(tài)分析的結(jié)果。
3.1.2 靜態(tài)測試工具的工作流程設(shè)計
靜態(tài)測試工具的工作流程如圖3所示,加載單個文件的測試流程與加載工程的測試流程的處理有所區(qū)別。加載對象為一個工程時,首先分析工程,把工程中的所有文件分類保存在不同的鏈表中,然后在進(jìn)一步處理鏈表中的節(jié)點信息。當(dāng)加載的是單個.cpp(或.c)文件時,需要指定.cpp文件所對應(yīng)的頭文件路徑,然后再對文件進(jìn)行靜態(tài)分,該流程的具體實現(xiàn)將在工具的實現(xiàn)中做詳細(xì)描述。
圖3 靜態(tài)檢測工具的工作流程圖
靜態(tài)檢測工具是在Linux系統(tǒng)下采用Qt/C++開發(fā)實現(xiàn)的,開發(fā)環(huán)境使用Qt Creator。主界面如圖4所示,主菜單中有文件、靜態(tài)分析測試和質(zhì)量評價模型三項。其中,文件菜單中包含設(shè)置頭文件目錄、加載C文件和加載工程文件。因為靜態(tài)分析工具加載被測對象時支持單個C文件加載,所以需要對應(yīng)的頭文件需要設(shè)置。靜態(tài)分析測試項中不僅包含內(nèi)存泄漏情況分析,還包括靜態(tài)全局分析、函數(shù)關(guān)系分析等源代碼其他以供質(zhì)量度量時需要的度量指標(biāo)信息和生成測試報告。質(zhì)量度量模型項主要包括質(zhì)量度量元信息餅圖、質(zhì)量度量標(biāo)準(zhǔn)餅圖和質(zhì)量度量因素餅圖的展示子項。
圖4 靜態(tài)測試工具主界面
總之,該靜態(tài)工具的實現(xiàn)中,不僅包含了內(nèi)存泄漏靜態(tài)分析的功能,還有還包含了質(zhì)量度量的功能。在實際應(yīng)用過程中,執(zhí)行內(nèi)存靜態(tài)分析時,后期測試和質(zhì)量度量的其他信息都一起分析統(tǒng)計了,這樣進(jìn)一步提高了整體的測試效率。
以下對靜態(tài)分析工具的核心功能給出具體的實現(xiàn)。
3.2.1 加載分析單個文件的功能實現(xiàn)
文中所設(shè)計的靜態(tài)分析工具支持對單個文件(.cpp或.c文件)的加載并分析,這里重點介紹工具該項功能的實現(xiàn)。
對加載文件進(jìn)行逐行掃描,取得文件中的new、malloc等關(guān)鍵字放到一個內(nèi)存申請鏈表中,并進(jìn)一步判斷new、malloc處理的對象是否有delete、free關(guān)鍵字的對應(yīng)處理,如果有,則放到內(nèi)存釋放鏈表中;如果沒有對應(yīng)的處理,則繼續(xù)判斷是否是屬于Qt的內(nèi)存托管,具體托管的種類,依據(jù)前面提到的Qt內(nèi)存半自動化管理分類,并把對應(yīng)的Qt內(nèi)存托管類型存儲到內(nèi)存釋放鏈表中。 文件掃描分析結(jié)束后,通過對比內(nèi)存申請鏈表與內(nèi)存釋放鏈表的信息,得出程序內(nèi)存釋放存在內(nèi)存泄漏的情況。
另外,在對文件進(jìn)行逐行分析的過程中,如果遇到數(shù)據(jù)類型為結(jié)構(gòu)體或更復(fù)雜的類時,需要進(jìn)一步分析與.cpp(或.c)文件對應(yīng)的頭文件時,分析工具可以通過用戶指定路徑的方法自動加載并分析所對應(yīng)的頭文件。
3.2.2 加載分析工程的功能實現(xiàn)
工具加載并分析工程的功能主要是掃描并分析工程里的所有.c,.cpp和.h文件。首先把所有.h文件放到一個頭文件鏈表中,把所有.c,.cpp文件放到實現(xiàn)鏈表中。然后,遍歷實現(xiàn)鏈表中的每個節(jié)點,并對每個節(jié)點中的文件進(jìn)行逐行掃描分析,分析的內(nèi)容與2.2.1中對單個文件的分析過程及內(nèi)容相同,這里不再贅述。需要說明的是這里每個節(jié)點分析出的內(nèi)存申請鏈表和內(nèi)存釋放鏈表,需要與文件鏈表中的節(jié)點關(guān)聯(lián)起來,關(guān)聯(lián)關(guān)系如圖5所示,實現(xiàn)文件鏈表中,每個節(jié)點對應(yīng)兩個內(nèi)存申請和釋放信息鏈表。通過對比內(nèi)存申請鏈表與內(nèi)存釋放鏈表的信息,得出程序內(nèi)存釋放存在內(nèi)存泄漏的情況。
圖5 實現(xiàn)文件的鏈表信息結(jié)構(gòu)圖
另外,在對每個.cpp(或.c)文件進(jìn)行逐行分析的過程中,如果遇到數(shù)據(jù)類型為結(jié)構(gòu)體或更復(fù)雜的類時,需要進(jìn)一步分析與.cpp(或.c)文件對應(yīng)的頭文件時,分析工具通過遍歷頭文件鏈表并分析所對應(yīng)的頭文件。
文中所設(shè)計開發(fā)的Qt程序內(nèi)存泄漏檢測工具在某公司重點項目的白盒測試中得到了廣泛應(yīng)用。工具支持Linux和window操作系統(tǒng),本次應(yīng)用主要運行在Linux系統(tǒng)上,靜態(tài)分析工具安裝在一臺PC機上,PC機具體軟硬件配置信息如下表1所示。
表1 PC機軟硬件配置信息
通過對幾十個模塊的多輪次的迭代測試,發(fā)現(xiàn)內(nèi)存泄漏問題近百個,并且都能準(zhǔn)確定位,經(jīng)驗證,問題誤報和漏報率都少于1%。工具發(fā)現(xiàn)的Qt內(nèi)存泄漏問題主要總結(jié)為兩類:第一類是未管理的Qt內(nèi)存發(fā)生泄漏;第二類是Qt半自動管理的內(nèi)存被再次釋放。比如申請內(nèi)存時,構(gòu)造函數(shù)不帶參數(shù),即未指定父親,會引發(fā)內(nèi)存泄漏;申請內(nèi)存時指定了父親卻被再次釋放導(dǎo)致錯誤。除此以外,還有一些特殊情況,比如指定父親為this指針、申請的內(nèi)存交給全局變量管理、用增加孩子addChild的方式交給父親管理內(nèi)存、申請的內(nèi)存永久性加入主窗口等,這些情況都相當(dāng)于間接指定了父親,也可以不用考慮內(nèi)存泄漏。工具準(zhǔn)確定位的Qt內(nèi)存泄漏問題,為研發(fā)人員的編碼工作提供了有效的參考,得到測試和研發(fā)人員的高度認(rèn)可。
在測試效率方面的效果更顯著,能幫助測試人員快速排除非內(nèi)存泄漏問題,比如很多關(guān)于Qt內(nèi)存半自動化管理范圍內(nèi)的程序,測試人員人工分析排除非內(nèi)存泄漏問題時,需要耗費大量的時間?,F(xiàn)在工具可以直接根據(jù)文中所提出的Qt內(nèi)存泄漏測試方法進(jìn)行自動排除,很大程度上提高了測試效率,如圖6所示,人工分析與工具掃描一個模塊源代碼的平均時間對比圖。
圖6 人工分析與工具掃描耗時對比圖
通過對Qt內(nèi)存半自動化管理的分析,提出了基于Qt程序內(nèi)存泄漏檢測方法,并設(shè)計開發(fā)了與該方法相對應(yīng)的內(nèi)存泄漏靜態(tài)分析測試工具,經(jīng)過集團公司某重點項目的具體應(yīng)用發(fā)現(xiàn),該工具切實有效,在提高測試效率和內(nèi)存泄漏問題代碼精準(zhǔn)定位方面效果明顯。