記憶體洩露(memory leak)

C語言動態記憶體配置

  • C 程式執行時所有的資料變數置於三種區域:

    • 資料區 (Data segment)︰ 全域變數, static 變數,常數。
    • 堆疊區 (Stack)︰ 區域變數 (Auto variable), 函式參數,暫時變數。
    • Heap 區︰ 動態配置的記憶體。
  • memory leak指內部記憶體泄漏指由於疏忽或錯誤造成程式未能釋放已經不再使用的內部記憶體的情況。

  • 內部記憶體泄漏並非指內部記憶體在物理上的消失,而是應用程式分配某段內部記憶體後,由於設計錯誤,導致在釋放該段內部記憶體之前就失去了對該段內部記憶體的控制,從而造成了內部記憶體的浪費。

* malloc()、free()、calloc() 與 realloc() 到目前為止,都是事先宣告好所要使用的變數,當程式開始執行時,這些變數就會自動被配置記憶體空間。

+ malloc函數動態申請的記憶體空間是在heap裡(而一般區域變數存於stack裡),並且該段記憶體不會被初始化,因此必須使用memset直接給定初始值
+ calloc = malloc + memset
    - calloc(numElements ,sizeOfElement);
    - malloc(numElements *sizeOfElement) ;
+ 如果由malloc()函數分配的內存空間原來沒有被使用過,則其中的每一位可能都是0;反之,如果這部分內存空間曾經被分配、釋放和重新分配,則其中可能遺留各種各样的數據。也就是說,使用malloc()函數的程序開始時(內存空間還沒有被重新分配)能正常運行,但經過一段時間後(內存空間已被重新分配)可能會出現問題。
+ malloc記憶體分配成功則是返回void* ,需要通過強制類型轉換將void*指標轉換成我們需要的類型。
+ malloc分配記憶體失敗時返回NULL。
  • 一維陣列配置記憶體:
int size = 0;
scanf("%d", &size);

int *arr = (int*)malloc(size * sizeof(int));
if (NULL == arr){
    exit(-1);
}
...
if (NULL != arr){
    free(arr);
    arr = NULL;
}
// 如果已正確free(arr)且arr=NULL時,free(NULL)為合法的操作
free(arr);
  • 二維陣列連續配置記憶體:
    int m=0, n=0;
    scanf("%d*%d", &m, &n);
    int *arr = (int*)malloc(m * n * sizeof(int));
    if (NULL == arr){
      exit(-1);
    }
    ...
    ...
    if (NULL != arr){
      free(arr);
      arr = NULL;
    }
    

* 如果要改變先前配置的記憶體大小,則可以使用 realloc(),您必須先使用 malloc()、calloc() 或 realloc() 配置記憶體,而後使用所得到的位址來使用 realloc() 重新配置記憶體大小.

+ arr1 與 arr2 的位址相同並不保證,realloc() 會需要複製資料來改變記憶體的大小,若原位址有足夠的空間,則使用原位址調整記憶體的大小,若空間不足,則重新尋找足夠的空間來進行配置,在這個情況下,realloc() 前舊位址的空間會被釋放掉.
+ 因此,必須使用 realloc() 傳回的新位址,而不該使用舊位址,若 realloc() 失敗,則傳回空指標(null)。最好是進行檢查
int size = 0;
scanf("%d", &size);
int *arr1 = (int*)malloc(size * sizeof(int));
if (NULL == arr){
    exit(-1);
}
...
...
// 重新配置兩倍的記憶體
int *arr2 = (int*)realloc(arr1, sizeof(int) * size * 2);
if(!arr2) {
    arr1 = arr2;
}
if (NULL != arr1){
    free(arr1);
    arr1 = NULL;
}
  • 在 C/C++ 語言中,如果有人用 malloc()/new() 等函數分配了記憶體,卻忘了用 free()/delete() 等函數進行釋放,那就會產生記憶體漏洞。
  • 要解決這個問題,必須遵循幾個原則:
    • 第一個是程式紀律的問題,例如一個很好的習慣是,採用物件導向的寫法,然後在物件的建構函數中分配記憶體,並在解構函數中,釋放該物件所分配的所有記憶體。
    • 第二個原則是程式測試的問題,您可以使用記憶體檢查函數,進行記憶體漏洞檢查,像是 Linux 當中就有 mtrace 或是valgrind.

記憶體洩漏

  • 記憶體洩漏主要有以下幾種情況
    1. 記憶體分配未成功,卻使用了它。
    2. 記憶體分配雖然成功,但是尚未初始化就引用它。
    3. 記憶體分配成功並且已經初始化,但操作越過了記憶體的邊界。(overflow)
    4. 忘記了釋放記憶體,造成記憶體洩露。
    5. 釋放了記憶體卻繼續使用它。

在 stack 設置過大的變數會導致堆疊溢位(stack overflow)

  • 由於編譯器會自行決定 stack 的上限,某些預設是數 KB 或數十KB,當變數所需的空間過大時,很容易造成 stack overflow,程式亦隨之當掉(segmentation fault)。
//  錯誤例子: 在stack宣告過大陣列
int array[10000000];

// 正確例子
int *array = (int*) malloc( 10000000*sizeof(int) );
  • 建議將使用空間較大的變數用malloc/new配置在 heap 上,由於此時 stack上只需配置一個 int* 的空間指到在heap的該變數,可避免 stack overflow。
  • 使用 heap 時,雖然整個 process 可用的空間是有限的,但採用動態抓取的方式,new 無法配置時會丟出 std::bad_alloc 例外,malloc 無法配置時會回傳 null,不會影響到正常使用下的程式功能
    • 使用 heap 時,整個 process 可用的空間一樣是有限的,若是需要頻繁地malloc / free 或 new / delete 較大的空間,需注意避免造成記憶體破碎(memory fragmentation)。
    • 由於Linux使用overcommit機制管理記憶體,malloc即使在記憶體不足時仍然會回傳非NULL的address,同樣情形在Windows/Mac OS則會回傳NULL。
    • 因此linux下不會請求多少記憶體,就為你分配多少記憶體,而是按需(實際使用情況)分配,malloc的時候,僅僅是做個標記,有一定的overhead。僅當overhead用光所有的用戶空間之後,malloc才會返回NULL。
    • overcommit機制

malloc in function

// 解法1
int* createNewArray(int size) {
    return (int*) malloc( size * sizeof(int) );
}
int main() {
    int* ptr= NULL;
    // 必須有一個pointer接住傳回來的動態配置記憶體
    ptr = createNewArray(10);
}

// 解法2
void createNewArray(int** local, int size) {
    *local = (int*) malloc( size * sizeof(int) );
}

int main() {
   int *ptr = NULL;
   createNewArray(&ptr, 10);
}

free after crash

void function(int arg) {
  char *foo;
  foo = (char *)malloc(sizeof(char) * 100);
  // 若在func1發生錯誤exit()後,foo所配置的記憶體會被OS回收
  int i = func1(arg, &foo);

  char *bar;
  bar = (char *)malloc(sizeof(char) * 100);
  // 同上, 若在func2發生錯誤exit()後,foo與bar所配置的記憶體會被OS回收
  int j = func2(&bar);

  free(foo);
  free(bar);
}
``

## 使用valgrind偵測

```c
// leak.c
#include <stdlib.h>

void func (void){
    char *buff = (char*)malloc(10);
}

int main (void){
    // 在function中配置了記憶體,但在程式結束時沒有釋放
    func();
    return 0;
}
  • 編譯檔案,必須加上-g除錯訊息 gcc -g -o leak leak.c
  • valgrind --leak-check=full ./leak ,會出現以下訊息
    • 可看出有記憶體沒有釋放 1 allocs, 0 frees, 發生在程式的第4行。
==2680== Memcheck, a memory error detector
==2680== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==2680== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==2680== Command: ./leak
==2680==
==2680==
==2680== HEAP SUMMARY:
==2680==     in use at exit: 10 bytes in 1 blocks
==2680==   total heap usage: 1 allocs, 0 frees, 10 bytes allocated
==2680==
==2680== 10 bytes in 1 blocks are definitely lost in loss record 1 of 1
==2680==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==2680==    by 0x400537: func (leak.c:4)
==2680==    by 0x400547: main (leak.c:9)
==2680==
==2680== LEAK SUMMARY:
==2680==    definitely lost: 10 bytes in 1 blocks
==2680==    indirectly lost: 0 bytes in 0 blocks
==2680==      possibly lost: 0 bytes in 0 blocks
==2680==    still reachable: 0 bytes in 0 blocks
==2680==         suppressed: 0 bytes in 0 blocks
==2680==
==2680== For counts of detected and suppressed errors, rerun with: -v
==2680== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
  • 從報告中可以看到 memory lost 分成幾種類型:

    • definitely lost: 真的 memory leak 了
    • indirectly lost: 間接的 memory leak,structure 本身發生 memory leak,而內部的 member 如果是 allocate 的出來的,一樣會 memory leak,但是只要修好前面的問題,後面的問題也會跟著修復。
    • possibly lost: allocate 一塊記憶體,並且放到指標 ptr,但事後又改變 ptr 指到這會計一體的中間 (這一點我目前也不是很清楚,建議看原文說明)
    • still reachable: 程式結束時有未釋放的記憶體,不過卻還有指標指著,通常會發生在 global 變數
  • 就算是 library 所 malloc 的記憶體,如 evbuffer_new(),也都可以偵測的到。

Invalid Memory Access

* Invalid memory access 有時候並不會立即造成 segmentation fault,所以不會有 core dump可以查詢,需要借助像 valgrind 這類的工具來偵測。 + 一般情況可能是用了 allocate 的 memory 之外的地方,或是用了已經 free 的 memory.

// invalid.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main (void) {
    // 1. Invalid write
    char *str = malloc(4);
    strcpy(str, "Brian");
    free(str);

    // 2. Invalid read
    int *arr = malloc(3);
    printf("%d", arr[4]);
    free(arr);

    // 3. Invalid read
    printf("%d", arr[0]);

    // 4. Invalid free
    free(arr);

    return 0;
}
  • 編譯並執行 gcc -g -o invalid invalid.c
    • 錯誤一:Invalid write of size 2,試圖寫入一個非法的區域,valgrind 還好心告訴你這個地方是在 mem_test.c:6 allocate 出來的 memory 之後的 3 byte,通常遇到這種情況都是忘記檢查 buffer 的 size 就去用。
    • 錯誤二:Invalid read of size 4,試圖讀取一個非法的區域。
    • 錯誤三:Invalid read of size 4,讀取的區域已經被 free 了,free 的位置 valgrind 也幫你指出來 mem_test.c:12。
    • 錯誤四:Invalid free,也就是 free 一個不存在的地方,或是 double free
==3538== Memcheck, a memory error detector
==3538== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==3538== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==3538== Command: ./invalid
==3538==
==3538== Invalid write of size 2
==3538==    at 0x4005D6: main (invalid.c:8)
==3538==  Address 0x5203044 is 0 bytes after a block of size 4 alloc'd
==3538==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==3538==    by 0x4005C7: main (invalid.c:7)
==3538==
==3538== Invalid read of size 4
==3538==    at 0x4005FE: main (invalid.c:12)
==3538==  Address 0x52030a0 is 13 bytes after a block of size 3 alloc'd
==3538==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==3538==    by 0x4005F1: main (invalid.c:11)
==3538==
==3538== Invalid read of size 4
==3538==    at 0x400621: main (invalid.c:15)
==3538==  Address 0x5203090 is 0 bytes inside a block of size 3 free'd
==3538==    at 0x4C2EDEB: free (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==3538==    by 0x40061C: main (invalid.c:13)
==3538==  Block was alloc'd at
==3538==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==3538==    by 0x4005F1: main (invalid.c:11)
==3538==
==3538== Invalid free() / delete / delete[] / realloc()
==3538==    at 0x4C2EDEB: free (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==3538==    by 0x40063F: main (invalid.c:17)
==3538==  Address 0x5203090 is 0 bytes inside a block of size 3 free'd
==3538==    at 0x4C2EDEB: free (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==3538==    by 0x40061C: main (invalid.c:13)
==3538==  Block was alloc'd at
==3538==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==3538==    by 0x4005F1: main (invalid.c:11)
==3538==
00==3538==
==3538== HEAP SUMMARY:
==3538==     in use at exit: 0 bytes in 0 blocks
==3538==   total heap usage: 3 allocs, 4 frees, 1,031 bytes allocated
==3538==
==3538== All heap blocks were freed -- no leaks are possible
==3538==
==3538== For counts of detected and suppressed errors, rerun with: -v
==3538== ERROR SUMMARY: 5 errors from 4 contexts (suppressed: 0 from 0)

results matching ""

    No results matching ""