跨模組記憶體管理的陷阱

2021-04-16 08:00:30 字數 2616 閱讀 6208

許式偉2023年6月21日

由於編譯器、編譯模式的不同,不同模組的記憶體結構與管理程式往往並不相同。因此,如果我們在乙個模組申請記憶體,而在另乙個模組中釋放,這是乙個不安全的做法。因為模組在釋放記憶體的時候,並不會預料到需要釋放的記憶體並非是自己管理的。

直觀的說,假設我們有兩個模組:module1,module2。它們有函式module1.alloc,module1.free;module2.alloc, module2.free。雖然同為alloc和free,但是你不能假設module1.alloc/free和module2.alloc/free是同乙份**。因此,module1.alloc申請的資源,只有由module1.free去釋放才是確保安全的。

展開來講,我們通常有以下跨模組呼叫約定: 

如果有此需求,嘗試用以下幾種解決方案:

考慮提供乙個函式,讓外部模組獲得所需的記憶體大小,讓外部模組申請記憶體並傳入。這是乙個比較典型的解決方案,windows的api均採用此解決方案。

考慮使用乙個介面包裝此記憶體的訪問,讓外部模組獲得介面指標,並以此訪問記憶體。記憶體是通過介面的release()函式釋放的。這樣就保證了記憶體釋放的正確性。

考慮使用cotaskmemalloc/cotaskmemfree申請、釋放記憶體。因為記憶體的申請、釋放由系統完成,故可以保證其一致性。

作為條目3. 的特殊情形,如果返回的是字串,可考慮用bstr。此時資源管理由系統呼叫sysallocstring/sysfreestring 實現。

仍然在內部申請記憶體並返回出去,同時將該記憶體的釋放函式也作為引出函式引出去。外部模組使用完該記憶體後,用我們引出的釋放函式釋放它。這是可行的方案,雖然比較少見。你可以認為其實cotaskmemalloc/cotaskmemfree、sysallocstring/sysfreestring也是基於這條規則提供的,只不過它沒有特定目的而已。

有時候出於某種考慮(例如檢測記憶體資源洩漏),我們可能提供乙個自己實現的win32 api版本來取代windows的系統呼叫。

我們知道,如果你使用cotaskmemalloc/cotaskmemfree、sysallocstring/sysfreestring來申請、釋放記憶體,那麼哪怕存在記憶體洩漏,我們在《最快速度找到記憶體洩漏》中介紹的方法並不能檢測出來。

除了使用一些系統資源洩漏的檢測工具(其實它們的方法和我們這裡介紹的肯定也類似)外,一種方法,就是提供這些api的替換版本。這些替換版本中,我們提供了洩漏檢測的能力。

我們這裡並不準備詳細討論這個技巧。但是請注意,這裡存在的潛在危險是,有可能出現這樣的情形:設想我們的某dll使用了替換版本的sysallocstring,其中申請了乙個bstr返回給另一dll,而該dll並不使用替換版本的sysfreestring,而是呼叫系統的sysfreestring釋放這個bstr。這裡存在的問題是顯然的,因為系統並沒有分配過這樣乙個bstr。

這是因為:

同乙個類,相同的宣告,在不同的編譯器、甚至不同的編譯模式下,會有不同的記憶體布局。也就是說,看似是同乙個類,但是其實在不同的模組中,理解上根本不同。例如,你用vc++寫乙個dll,該dll返回乙個std::string,而dll的客戶程式是c++ builder寫的。你能夠保證c++ builder的std::string與vc++的記憶體布局一致嗎?

類存在成員函式(特別是構造、析構),這些成員函式對我們來說是個黑箱操作。對他們的呼叫同樣容易產生這樣的情況,就是在我們的模組中申請了記憶體,而在外部模組中(由析構函式)釋放。仍然以std::string為例。我們往往為了方便返回乙個字串,而將函式宣告為:

⑴ std::string get***();

或者:⑵ get***(std::string& str);

這種方式在同一模組中是可行的,而且是相對比較高效的方式。但是如果用於跨模組的字串傳遞,則存在風險(並不一定會出問題,關於什麼時候不出問題,我們下一回討論)。 

遇到這種要用類的情形。請嘗試採用以下方案:

考慮採用純結構體

考慮使用乙個介面包裝該類,將該類實現為com元件。

如果返回的是字串,考慮用bstr。 

所謂「純結構體」,是指該結構體:

沒有任何虛擬的成分。如虛函式、虛擬繼承等。

它的所有成員變數,均為簡單資料型別(c標準資料型別,不包括指標),或者是另乙個「純結構體」。

如果成員變數是乙個指標,那麼要麼作為輸入引數,指向的內容是乙個純結構體或c標準字串;要麼作為輸出引數,指向的內容是一系統分配的資源。

總的說來一句話,就是「純結構體」成員的型別要求,完全等同模組的引出函式引數型別的要求。

純結構體在介面定義中比較廣泛,往往用於取代在介面使用類的需求。對於我們規範中的「不允許使用類」,有乙個誤區是,使用了乙個struct關鍵字定義的,本質上還是類的東西。例如:

struct

astruct;

這個struct有構造、析構(儘管沒有顯式寫出,編譯器幫你生成的),析構中有記憶體釋放操作,是乙個標標準準的「類」。

另外,結構體需要顯式指定位元組對齊方式。例如:

#pragma

pack(push, 1)

struct

***x

;#pragam pack(pop)

對「記憶體管理」相關的技術感興趣?這裡可以看到我的所有關於記憶體管理的文章。

redis的記憶體陷阱

redis持久化時記憶體兩倍的問題 redis有rdb和aof兩種持久化方式,rdb容易丟資料,aof由於儲存的歷史,會使得檔案非常非常大,就得啟用rewrite的功能。所以都會有fork出乙個子程序,有子程序將資料寫入磁碟。之前有人說子程序會完全copy父程序的記憶體,所以必須讓redis留出一半...

strcat的記憶體越界陷阱

一下 段裡,char szplanchange 500 未初始化,會導致在strncat進行字串連線的時候因為沒有在字元陣列範圍內找到字串結束符 0 而出現記憶體操作越界的問題。乙個實際的結果是導致delete pstarttime 的時候失敗,為區域性指標變數pstarttime 的位址已經被st...

linux記憶體管理模組引數的個人理解

linux記憶體管理模組引數的個人理解 先上乙個引數圖 total 總共有多少記憶體 used 用了多少記憶體 這裡的記憶體不僅包括真正在用的,還包括給應用程式預留的,這部分空間有一部分其實等於沒用,因為它是預留的,別的程式記憶體不夠它照樣得把這部分分給人家t.t,但很新手一看used就瞬間緊張了。...