メモリリークとは、消えてしまったメモリのことではありません。まだあなたのもので、まだ確保されていて、しかし返却する手立てを失ってしまったメモリのことです - そこを指す最後のポインタがなくなったからです。何もクラッシュしません。プログラムは動き続け、一回りごとに少しずつ重くなり、やがてまったく無関係などこかで何かが失敗します。
このページでは、リークがどう生じるか、その大半を防ぐ規律、そして残りを見つける 2 つのツールを扱います。
リークとはどう見えるか
反復ごとに block が新しいポインタで上書きされます。前のブロックはまだ確保されたままで、そのアドレスを持つ変数はどこにもなく、二度と解放できません。3 回の反復で 12 キロバイトを失います。リクエストごとにこれをやるサーバーは、リクエストが来る速度でそれを永久に失い続けます。
直し方は 1 行 - 本体の最後に free(block); - ですが、どこにそれが属するのかを見抜くことこそが本当の技術です。
リークはどう起こるか
1. 失われたポインタ
生きているブロックへの唯一の参照をまだ保持しているポインタへの代入は、すべてそのブロックをリークさせます。
char *name = malloc(32);
name = malloc(64); /* 最初の 32 バイトはもう到達できない */
上のループは同じバグがループをまとっただけのものです。構造体のフィールドへの再代入も同じですし、calloc と realloc にある realloc の略記も同じです:
p = realloc(p, n); /* 失敗時: p は NULL になり、古いブロックは孤児になる */
2. 早期 return
関数から出るすべての経路は、その関数がすでに取得したものを解放しなければなりません。そして忘れられるのは、いつだってエラー経路です。
正常系は正しく、エラー経路がリークする - だからこれはテストを生き延びてしまいます。失敗する分岐は開発中にはほとんど走らないからです。直し方は、各経路が飛び先とする単一のクリーンアップ区画です:
これは、経験を積んだ C プログラマが積極的に勧める唯一の goto の使い方です。うまくいくのは、すべてのポインタが NULL から始まり、free(NULL) が何もしないからです。関数がどこまで進んでいようと、単一の出口ブロックが正しく働きます。
3. あいまいな所有権
最も分かりにくいリークは、そもそもコーディングの誤りですらありません - 2 つの関数が、それが誰の仕事だったのかで食い違っているだけです。
char *build_message(void); /* これは呼び出し側が解放するのか? */
void store(char *text); /* store は所有権を受け取るのか? */
build_message が確保したメモリを返し、store がそれをコピーするなら、呼び出し側が解放しなければなりません。store がそのポインタを保持するなら、呼び出し側は解放してはいけません。コードのどこにもどちらかが書かれていないので、2 つの前提のうち一方が二重に採られ - リークか二重解放のどちらかになります。
処方箋は慣習であり、メモリを確保するすべての関数のそばにコメントで明記することです:
/* 新しく確保した文字列を返す。呼び出し側が解放すること。 */
char *build_message(void);
/* 'text' の所有権を受け取る。store_free() によって解放される。 */
void store(char *text);
規則は設計文書ではなく関数のところに書きましょう。C のメモリ管理において、最も価値の高い唯一の習慣です。
所有権の規律
4 つの規則でほぼすべてを覆えます:
- 確保したものにはちょうど 1 人の所有者がいる - それを解放する責任を持つコードはただ 1 か所です。
- 確保する関数には、解放する関数を対にする。
vec_init/vec_free、config_load/config_free。この対称性のおかげで、呼び出しの抜けが目に見えるようになります。 - 確保したのと同じ層で解放する。 ただし関数のコメントが明示的に所有権を移譲している場合を除きます。
- 解放したポインタには
NULLを入れる。 そうすれば、後でうっかり使ったときに、静かにヒープを壊す代わりにその場でクラッシュしてくれます。
リークを見つける: valgrind
Linux では、valgrind は再コンパイルを必要としません。ただしデバッグシンボルがあると報告が読みやすくなります:
gcc -g -O0 program.c -o program
valgrind --leak-check=full --show-leak-kinds=all ./program
このページ冒頭のリークするループに対しては、報告の末尾はおおよそ次のようになります:
==12345== HEAP SUMMARY:
==12345== in use at exit: 12,000 bytes in 3 blocks
==12345== total heap usage: 3 allocs, 0 frees, 12,000 bytes allocated
==12345==
==12345== 12,000 bytes in 3 blocks are definitely lost in loss record 1 of 1
==12345== at 0x4C2FB0F: malloc (vg_replace_malloc.c:299)
==12345== by 0x108671: main (program.c:6)
==12345==
==12345== LEAK SUMMARY:
==12345== definitely lost: 12,000 bytes in 3 blocks
下から読みましょう。"definitely lost"(確実に失われた)は、終了時点でそのブロックへのポインタが存在しなかったという意味 - 本物のリークです。スタックトレースが示すのは、それが失われた行ではなく、それを作った malloc の行ですが、たいていは抜けている free を見つけるにはそれで十分です。
他に 2 つの分類が現れます:
- indirectly lost(間接的に失われた) - それ自体が失われたブロックを通じてしか到達できないブロック。リークした連結リストの要素などです。"definitely lost" のほうを直せばこれらは消えます。
- still reachable(まだ到達可能) - 終了時点で確保されているが、生きたポインタがあるもの。典型的にはグローバルなキャッシュです。危険な意味でのリークではありませんが、報告を空のまま保つために解放しておく価値はあります。
valgrind は未初期化メモリの読み取りやブロック末尾を越えた書き込みも捕らえます。リークの背後にあるバグは、しばしばそうやって見つかります。
リークを見つける: AddressSanitizer
AddressSanitizer は GCC と Clang に組み込まれており、valgrind よりはるかに高速で、valgrind が動かない環境(現在の macOS を含む)でも動きます:
gcc -g -fsanitize=address -fno-omit-frame-pointer program.c -o program
./program
リークの報告は終了時に自動的に表示されます:
=================================================================
==12345==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 12000 byte(s) in 3 object(s) allocated from:
#0 0x7f... in malloc
#1 0x1086... in main program.c:6
SUMMARY: AddressSanitizer: 12000 byte(s) leaked in 3 allocation(s).
ASan はさらに、解放後の使用やヒープバッファのオーバーフローを、後になってから起こる謎めいた破壊ではなく、その場ではっきりラベル付けされた異常終了に変えてくれます。テスト実行はこれを有効にしてビルドし、リリースは無効にしてビルドしましょう。メモリと速度のコストがかかるからです。
自分のプラットフォームでリーク検出が働かない場合は、実行前に環境変数 ASAN_OPTIONS=detect_leaks=1 を設定してください。
リークのあるプログラムを一歩ずつ直す
3 つの別々のリークを持つ小さなプログラムがこちらです:
valgrind は 3 つの異なる行番号を持つ 3 件の "definitely lost" を報告します。1 つずつ直したものがこちらです:
修正 1 は所有権のコメントを実行に移したものです。shout が確保し、main が解放します。修正 2 は最初のものを解放するのではなく、二重の確保そのものを取り除いています - より単純なコードが同時に正しいコードでもあります。修正 3 は早期 return に抜けていた free を足したものです。確保がもっと多く絡むなら、先に示した単一の cleanup: ラベルのほうが、解放を繰り返すよりうまくスケールします。
リークを防ぐ習慣
mallocを書いたらすぐにfreeを書き、それから間のコードを埋めていきましょう。- 確保する関数には、必ず対になる解放する関数を用意しましょう。
- 自分が確保したポインタを返す関数、または受け取る関数には、所有権をコメントで明記しましょう。
- 複数の確保を抱える関数では、単一の
cleanup:出口ブロックを使いましょう。 - 何かおかしいときだけでなく、当たり前のこととしてテストを
-fsanitize=addressの下で実行しましょう。 - 「definitely lost: 0 bytes」をテスト成功の一部として扱いましょう。
よくある質問
C言語のメモリリークとは何ですか?
malloc で確保したのに、もう解放できなくなったメモリのことです。プログラム内のどこからもそこを指していないからです。そのブロックはプロセスが生きているあいだ確保されたままになります。クラッシュではありません - プログラムは動き続け、ただ処理のたびにメモリ使用量が増え、やがて尽きるだけです。
C言語のメモリリークはどうやって見つけますか?
valgrind の下でプログラムを実行します: valgrind --leak-check=full ./program。終了時点でまだ確保されているすべてのブロックを、それを作った malloc のスタックトレースとともに報告してくれます。macOS や valgrind が使えない環境では、-fsanitize=address を付けてコンパイルすれば、同じ報告が終了時に出ます。
C言語のメモリリークの原因は何ですか?
3 つのパターンでほぼすべてを説明できます。あるブロックへの唯一のポインタを上書きすること(失敗時の p = realloc(p, n) を含む)、すでにメモリを確保した関数から早期に return すること、そして所有権があいまいなこと - 2 つの関数が互いに相手が解放すると思い込み、結果どちらも解放しないことです。
プログラムがどのみち終了するなら、メモリリークは問題になりますか?
一度走って終了するプログラムなら OS がすべてを回収するので、実害はほぼゼロです。問題になるのは長く動き続けるもの - サーバー、ゲームループ、デーモン - で、リクエストごとのリークが際限なく積み上がります。それでも一貫して解放しましょう: リークの報告はノイズとなり、本当に問題になるものを覆い隠してしまいます。