【C言語】
mallocとfreeの使い方
~メモリリーク検出方法~

warning: leak of ‘xp’ [CWE-401]

警告:mallocで獲得したメモリ領域が解放されていない

[-Wanalyzer-malloc-leak]

この記事の概要

この記事では、
C言語のメモリリーク検出方法を解説しています。
静的解析ツール(-fanalyzer)や
動的解析ツール(-fsanitize=leak)の
活用法を紹介し、
具体例を通じて、安全で効率的なメモリ管理の重要性を強調しています。


mallocの基本的な使い方と自動変数配列との違い

#include <stdio.h>
#include <stdlib.h>
void auto_ary(size_t n){
    int ary[n]; //自動変数の可変長配列
    for(size_t i = 0;i < n;i++){
        ary[i] = i;
        printf("%d\n",ary[i]);
    }
    //自動変数の解放は不要
}
void malloc_ary(size_t n){
    int *ary = malloc(sizeof(int) * n); //動的にメモリ確保
    for(size_t i = 0;i < n;i++){
        ary[i] = i;
        printf("%d\n",ary[i]);
    }
    free(ary); //解放が必要
}
int main(void){
    size_t n = 0;
    printf("配列の要素数を入れてね\n");
    scanf("%zd",&n);
    auto_ary(n);
    malloc_ary(n);
}

mallocは自動変数の配列に似ていますが以下の点が違います。
(1)領域はプログラマが自分で確保する(malloc)
(2)不要になった領域はプログラマが自分で解放する(free)
(3)コンパイル時に決まっていないメモリのサイズを動的に決める事ができる
※可変長配列はコンパイラによってはサポートされていません。


■借金を返さないでメモリリークする例

//gcc -fanalyzer -fsanitize=leak bug.c
#include <stdio.h>
#include <stdlib.h>
int main(void){
    for(int i = 0;;i++){
        char *cp = malloc(0x7fffffff);
        if(cp == NULL){
            perror("借金を返さない");
            break;
        }
        fprintf(stderr,"督促 %d回目\r",i);
    }
}

例えるならば
mallocはOS銀行からの借金で、
freeはOS銀行への返済です。
借りたお金を返さないとメモリリークという
恐ろしい事態になります。


■複数個所で借金してメモリリークする例

//gcc -fanalyzer -fsanitize=leak -g bug.c
#include <stdio.h>
#include <stdlib.h>
void NG_malloc_free(size_t money){
    size_t *p1 = malloc(money);//借金1回目
    if(p1 == NULL){
        perror(NULL);
        return;
    }
    p1[0] = money;
    printf("%zx\n",p1[0]);
    
    size_t *p2 = malloc(money);//借金2回目
    if(p2 == NULL){
        perror(NULL);
        return;
    }
    p2[0] = money;
    printf("%zx\n",p2[0]);

    free(p1);
    free(p2);
}
int main(void){
    size_t money = 0x7fffffff ;
    for(;;){
        NG_malloc_free(money);
    }
}

実際のプロジェクトで一番多いのがこのパターンです。
例えるならば
一回目の借金に成功して
二回目の借金に失敗した場合
一回目の借金返済を忘れるミスです。


■mallocの戻り値をキャストする必要は無い!

#include <stdio.h>
//#include <stdlib.h> ★忘れる
int main(void){
    int *x = (int *)malloc(sizeof(int));
    *x = 1;
    printf("%d\n",*x);
    free(x);
}
//gcc -fsanitize=address -fno-builtin cast-NG.c
//-fsanitize=address 動的解析
//-fno-builtin 組込みのmallocでなくて関数のmallocを使う

mallocの戻り値をキャストする人は多いですが
C言語ではキャストする必要はありません。
筆者の環境下では
このプログラムでは次の理由で異常終了します。
(1) “stdlib.h”をinclude していないのでmallocのプロトタイプ宣言がない
(2) プロトタイプ宣言が無いのでmallocの型は省略時int型32bitになる
(3) mallocの戻り値32bitを(int *)64bitでキャストしてアドレスを水増しする
(4) アドレスを水増しした領域に書き込みするので不正書き込みとなる。 


静的解析でメモリリーク検出

gcc  -fanalyzer -g bug.c
中略
bug.c:24:9: warning: leak of ‘xp’ [CWE-401] [-Wanalyzer-malloc-leak]

gccでコンパイル時に-fanalyzerオプションを付けると
メモリリークが発生するまでの詳細なトレース情報を表示してくれます。


動的解析でメモリリーク検出

 $gcc -fsanitize=leak bug.c -g
$./a.out
0x7f8978439d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
中略 
SUMMARY: LeakSanitizer: 18120 byte(s) leaked in 690 allocation(s)

gccでコンパイル時に
-fsanitize=leakオプションを付けて、
プログラムを走らせると
メモリリークが発生する箇所の
ソースコードの行数を表示してくれます。


参考:

CWE-401: 有効期間後のメモリの解放が欠落している

CWE-416: Use After Free

C-FAQ7.20: 動的に割り当てた記憶領域は解放した後には使えないね。

MEM12-C. リソースの使用および解放の最中に発生するエラーが原因で関数を終了する場合に、Goto 連鎖の使用を検討する