分類: 程式設計

C 語言中關於記憶體釋放的議題

這裡討論一些在 C 語言中關於記憶體釋放的議題,包含記憶體的重複釋放、敏感性資料的處理等。

在 C 語言中在使用完動態配置的記憶體時,我們通常只會記得將自己配置的記憶體釋放,避免記憶體洩漏(memory leak),然後就結束了,但是對於一些敏感性的資料而言,其實這樣還不夠。

這裡我們會討論一些 C 語言中關於動態記憶體的相關議題,像是處理敏感性資料的方法,如何讓資料不會外洩,確保資料的安全等。

Heap 與系統記憶體

程式中的 heap 通常會使用系統所提供的函數來管理它的記憶體,而 heap 記憶體的大小可以在程式一開始執行時就固定住,或者在程式執行時再調整大小。

當 heap 在釋放之前配置的記憶體時,並不一定會把這些釋放的記憶體直接還給系統,它通常會把這些釋放的記憶體先保留下來,等到程式接下來又需要配置動態記憶體時,再拿出來繼續使用。

所以若我們使用系統中的監控指令(如 top),從系統的角度來看程式執行時所使用的記憶體,通常跟該程式實際上所使用到的記憶體會有一些差異。

記憶體重複釋放(Double Free)

C 語言中一般都是使用 free() 函數來釋放記憶體,而在釋放記憶體時常會碰到的問題就是不小心重復釋放同一塊記憶體,這通常是因為程式設計者的疏忽所造成的,像下面這樣就是個典型的例子:

char *name = (char*) malloc(...);
// ...
free(name);     // First free
// ...
free(name);     // Double free

上面這種情況是很明顯的錯誤,想要避免掉並不是很困難,但是在使用指標的別名(alias)的時候,就會比較難檢查:

char *name = (char*) malloc(...);
char *tmp = name;
// ...
free(name);     // First free
// ...
free(tmp);     // Double free

在早期的 zlib 函式庫有一些 bugs,可能會讓記憶體重復釋放造成阻斷服務攻擊或是外部的程式介入,只是它發生的機會很小,而且在新的版本中已經修正這個問題了,若你對這個問題有興趣,可以到 cert.org 的網站上查詢。

一個很簡單避免記憶體重復釋放的方式就是直接將已經釋放的記憶體指標指定為 NULL,而之後如果對這個 NULL 指標再次釋放記憶體時,大部分的 heap 管理程式都會忽略這個動作:

char *name = (char*) malloc(...);
// ...
free(name);
name = NULL;

清除敏感性資料(Clearing Sensitive Data)

如果你在記憶體中存放一些敏感性資料(像帳號、密碼或信用卡卡號等),那麼你最好在使用完這些資料之後,自己把這些資料從記憶體中抹除,以確保資料不會外洩。

當程式執行完畢之後,那些被程式所使用的記憶體會被系統收回,然後繼續配置給其他的程式來使用,這可能是大家都知道事情,但是這其中有些問題。

大部份的系統並不會幫你把使用過的記憶體清乾淨,也就是說當你把一些資料儲存在記憶體中,在程式結束之後,記憶體雖然被系統收回,但是那些記憶體中的資料卻還是存在那裡(系統只是把記憶體的使用權收回而已,記憶體中的資料卻還在)。然後接著如果有另外一個程式需要記憶體時,系統又剛好把這些剛收回來的記憶體配置給這個新的程式來使用,那麼這個時候這個新的程式就可以讀取原本存放在這些記憶體中的資料了。

下面示範如何自己把記憶體中的資料清乾淨,避免資料外洩問題:

char name[32];
int userID;
char *securityQuestion;

// assign values
// ...

// Delete sensitive information
memset(name, 0, sizeof(name));
userID = 0;
memset(securityQuestion, 0, strlen(securityQuestion));

如果是使用動態配置的記憶體,那資料就要在釋放記憶體之前清除:

char *name = (char*)malloc(...);
// ...
memset(name,0,sizeof(name));
free(name);

在某些支援機密資料處理的系統中,當程式執行結束後,系統會自動對那些收回且不再被該程式使用的記憶體做一次清除的動作,雖然這樣比較安全,但是這也會對系統的效能造成一定的負擔。

在程式執行結束前釋放記憶體

作業系統會依照程式的需求來調配各種資源的調配(包含記憶體),而當程式執行完畢之後,系統會把所有的記憶體都一起收回,而在這個時候,不管程式的記憶體有沒有使用 free() 函數正常釋放,其實都沒有什麼差別,因為等程式一結束,系統就會像資源回收車一樣一次全部收走,再重新配置給其他程式使用。

以這個觀點來看,其實程式在結束之前沒有必要釋放記憶體,而因為某些因素,你可能需要正常釋放記憶體:

  • 一些比較盡責的程式設計者,為了程式的品質,會很注意所有記憶體的管理。在記憶體不使用時就把它釋放掉,也是一種很好的習慣。
  • 如果使用一些記憶體泄漏的偵測工具測試沒有正常釋放記憶體的程式時,就會出現一些錯誤報告,將記憶體都正常釋放可避免這些問題。
  • 在一些比較陽春的系統中,系統不會自動收回記憶體,要靠程式在結束之前自行處理。
  • 如果程式後來持續修改,而原本程式的結尾沒有正常釋放記憶體,再加上新的程式碼時,就會產生問題。

但是在另一方面,確認程式結束前正常釋放記憶體也會造成:

  • 沒多大效益,又造成一些額外的困擾。
  • 如果有很複雜的資料結構,可能會花上很多時間改程式。
  • 增加整體程式的大小。
  • 讓程式的執行時間拉長。
  • 造成更多程式出錯的機會。

基本上,到底要不要在程式結束前釋放記憶體,每個問題可能都會不同,要看實際的情況而定。

總結

這裡你可以發現記憶體的釋放不是單純只有呼叫一個 free() 而已,光是這樣還不夠。這邊介紹的許多技巧也可以應用於其他類似的函數上(例如 realloc() 函數),深入了解系統如何管理記憶體可以幫助程式設計者在記憶體的使用上更得心應手。

參考資料:O’Reilly Programming

G. T. Wang

個人使用 Linux 經驗長達十餘年,樂於分享各種自由軟體技術與實作文章。

Share
Published by
G. T. Wang
標籤: C/C++

Recent Posts

光陽 KYMCO GP 125 機車接電發動、更換電瓶記錄

本篇記錄我的光陽 KYMCO ...

2 年 ago

[開箱] YubiKey 5C NFC 實體金鑰

本篇是 YubiKey 5C ...

2 年 ago

[DIY] 自製竹火把

本篇記錄我拿竹子加上過期的苦茶...

3 年 ago