warning: leak of ‘xp’ [CWE-401]
警告:mallocで獲得したメモリ領域が解放されていない
[-Wanalyzer-malloc-leak]
■この記事の概要
この記事では、C言語のメモリリーク検出と推奨されるエラー処理方法を解説しています。静的解析ツール(-fanalyzer
)や動的解析ツール(-fsanitize=leak
)の活用法を紹介し、リソースリークを防ぐためのgoto
を使ったエラー処理が推奨されています。一方で、do-while(0)
を用いる非推奨の方法についても議論されています。具体例を通じて、安全で効率的なメモリ管理の重要性を強調しています。
■メモリリークの例
#include <stdio.h>
#include <stdlib.h>
void leak(size_t x,size_t y,size_t z){
//メモリ獲得処理
if(x == 0)
return ;
size_t *xp = malloc(x);
if(xp == NULL){
perror(NULL);
return ;
}
*xp = x;
if(y == 0)
return ;//メモリリーク
size_t *yp = malloc(x*y);
if(yp == NULL){
perror(NULL);
return ;//メモリリーク
}
*yp = y;
if(z == 0)
return ;//メモリリーク
size_t *zp = malloc(x*y*z);
if(zp == NULL){
perror(NULL);
return ;//メモリリーク
}
*zp = z;
//本処理
printf("%zx:%zx:%zx\n",*xp,*yp,*zp);
//解放処理
free(xp);
free(yp);
free(zp);
}
int main(void){
for(size_t x = 0;x<0x10;x++){
for(size_t y = 0;y<0x10;y++){
for(size_t z = 0;z<0x10;z++){
leak(x,y,z);
}
}
}
}
ハイライトの行でfreeしないでreturn するので
メモリリークが発生します。
リークするメモリの量が少ないと
3年掛かってシステムダウンする事も
あるとかないとか。
■gcc -fanalyzer -gでメモリリークを検出する(静的解析)
gcc -fanalyzer -g bug.c
In function ‘leak’:
bug.c:15:9: warning: leak of ‘xp’ [CWE-401] [-Wanalyzer-malloc-leak]
15 | return ;//メモリリーク
| ^~~~~~
‘leak’: events 1-7
|
| 5 | if(x == 0)
| | ^
| | |
| | (1) following ‘false’ branch (when ‘x != 0’)...
| 6 | return ;
| 7 | size_t *xp = malloc(x);
| | ~~~~~~~~~
| | |
| | (2) ...to here
| | (3) allocated here
| 8 | if(xp == NULL){
| | ~
| | |
| | (4) assuming ‘xp’ is non-NULL
| | (5) following ‘false’ branch (when ‘xp’ is non-NULL)...
|......
| 12 | *xp = x;
| | ~~~~~~~
| | |
| | (6) ...to here
|......
| 15 | return ;//メモリリーク
| | ~~~~~~
| | |
| | (7) ‘xp’ leaks here; was allocated at (3)
|
bug.c:24:9: warning: leak of ‘xp’ [CWE-401] [-Wanalyzer-malloc-leak]
gccでコンパイル時に-fanalyzerオプションを付けると
メモリリークが発生するまでの詳細なトレース情報を表示してくれます。
■gcc -fsanitize=leak -gでメモリリークを検出する(動的解析)
$gcc -fsanitize=leak bug.c -g
$./a.out
=================================================================
==117020==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 14400 byte(s) in 225 object(s) allocated from:
#0 0x7f897864d302 in __interceptor_malloc ../../../../src/libsanitizer/lsan/lsan_interceptors.cpp:75
#1 0x55df0d8a9218 in leak /mnt/f/メモリリーク/bug.c:16
#2 0x55df0d8a931c in main /mnt/f/メモリリーク/bug.c:44
#3 0x7f8978439d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
Direct leak of 3720 byte(s) in 465 object(s) allocated from:
#0 0x7f897864d302 in __interceptor_malloc ../../../../src/libsanitizer/lsan/lsan_interceptors.cpp:75
#1 0x55df0d8a91d7 in leak /mnt/f/メモリリーク/bug.c:7
#2 0x55df0d8a931c in main /mnt/f/メモリリーク/bug.c:44
#3 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オプションを付けて、
プログラムを走らせると
メモリリークが発生する箇所の
ソースコードの行数を表示してくれます。
参考:
■リソースリークの例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void f(void){
FILE *fp1 = NULL;
FILE *fp2 = NULL;
FILE *fp3 = NULL;
fp1 = fopen("/usr/include/stdio.h","r") ;
if(fp1 == NULL) {
perror("stdio.h");
return ;
}
fp2 = fopen("/usr/include/unistd.h","r") ;
if(fp2 == NULL) {
perror("unistd.h");
return ;//fp1解放漏れ
}
fp3 = fopen("/usr/include/windows.h","r") ;
if(fp3 == NULL) {
perror("windows.h");
return ;//fp1,fp2解放漏れ
}
puts("本体処理~色々");
// 後始末
fclose(fp3);
fclose(fp2);
fclose(fp1);
}
int main(void){
for(;;){
f();
usleep(10000);
}
}
fopen()で確保した領域を解放し忘れると
解放漏れ(メモリリーク)が発生します。
この例では fp1とfp2の解放漏れが発生します。
このように動的資源を複数回獲得した場合
解放漏れが発生しやすいです。
■gotoを使った推奨エラー処理
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void f(void){
FILE *fp1 = NULL;
FILE *fp2 = NULL;
FILE *fp3 = NULL;
fp1 = fopen("/usr/include/stdio.h","r") ;
if(fp1 == NULL) {
perror("stdio.h");
goto end ;
}
fp2 = fopen("/usr/include/unistd.h","r") ;
if(fp2 == NULL) {
perror("unistd.h");
goto end ;
}
fp3 = fopen("/usr/include/windows.h","r") ;
if(fp3 == NULL) {
perror("windows.h");
goto end ;
}
puts("本体処理~色々");
end:// 後始末
if(fp3) fclose(fp3);
if(fp2) fclose(fp2);
if(fp1) fclose(fp1);
}
int main(void){
for(;;){
f();
usleep(10000);
}
}
関数末尾に解放処理を設け
そこにエラー発生時
goto で飛ぶ方法を推奨します。
■do-while(0)を使った非推奨エラー処理
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void f(void){
FILE *fp1 = NULL;
FILE *fp2 = NULL;
FILE *fp3 = NULL;
do {
fp1 = fopen("/usr/include/stdio.h","r") ;
if(fp1 == NULL) {
perror("stdio.h");
break ;
}
fp2 = fopen("/usr/include/unistd.h","r") ;
if(fp2 == NULL) {
perror("unistd.h");
break ;
}
fp3 = fopen("/usr/include/windows.h","r") ;
if(fp3 == NULL) {
perror("windows.h");
break ;
}
puts("本体処理~色々");//字下げが深くなる
} while(0);
// 後始末
if(fp3) fclose(fp3);
if(fp2) fclose(fp2);
if(fp1) fclose(fp1);
}
int main(void){
for(;;){
f();
usleep(10000);
}
}
このコードはエラーが発生したら、
breakして、
末尾のエラー処理までジャンプする
テクニックです。
可読性のためと称してgoto文を禁止し
do-while(0)を使って
字下げを深くするのは
本末転倒では?と考えるのは筆者だけ?