■この記事の概要
Segmentation fault(コアダンプ)はC言語プログラミングにおける代表的なエラーで、メモリの不正なアクセスにより発生します。
本記事ではSegmentation faultが起こる典型的な原因(未初期化ポインタ、NULLポインタの参照、配列境界外アクセスなど)について解説し、gccやclangを使った静的・動的解析によるデバッグ手法を紹介します。
エラーの発見と対策に役立つ詳細な実例も掲載しています。
■静的解析でSegmentation faultを見つける
●gcc -Wall -Wextra -Oでコンパイルして警告を直す
clang -Wall -Wextra -Oでもコンパイルして
警告を直しましょう。
-Oオプションで最適化有りと無しで警告が出たり、出なかったりします。
ざっくり最適化有りのほうが有用警告が沢山出るようです。
例えば未初期化変数の警告は
最適化無しだとあまり出ませんが、
最適化ありだと警告してくれます。
でも、未使用static関数が最適化で消えてしまうと、、、、、
●gcc -fanalyzer -O2でコンパイルして警告を直す
clang –analyze -O2でもコンパイルして
警告を直しましょう。
●clang-tidyやcppcheckも使ってみる
使い方はこちら。
https://bugs-c.info/uncategorized/3248/
ここまでやって問題解決できない場合
静的解析はあきらめましょう。
■動的解析でSegmentation faultを見つける
●gccの場合
gcc -fsanitize=address -g でコンパイルして実行してください。
●clangの場合
clang -fsanitize=address -g でコンパイルして実行してください。
以下のようにエラー個所(ファイル名行番号)を教えてくれます。
-gオプションを付けないと行番号までは教えてくれません。
-Oオプション(最適化)の有無でエラーが出たり出なかったりする時があります。
前略
SUMMARY: AddressSanitizer: heap-buffer-overflow /mnt/中略/x.c:10 in main
後略
■segmentation faultはこんな時出ます
出ない時もあります。
再現性が無いので苦労します。
➡未初期化ポインタを間接参照した時
#include <stdio.h>
#include <stdlib.h>
int main(void){
int x ;
int *p ; //未初期化ポインタ
x = 1234 ;
*p = 5678 ;
char *str; //未初期化ポインタ
printf("数字を入れてね");
scanf("%s",str);
printf("%d\n",atoi(str));
}
ポインタのpとstrは初期化されていないのでメモリのどこを指しているか分かりません。メモリの何処かを破壊してしまいます。
➡NULLポインタを間接参照した時
#include <stdio.h>
int main(void){
char x = '\0';
char *p = NULL;
x = 'A' ;
*p = 'B' ;//NULLポインタ間接参照
printf("%c:%c\n",x,*p);
}
NULLポインタの指す先を参照すると未定義動作となります。
➡配列の添字が大きすぎる時
#include <stdio.h>
int main(void){
int buf[10] = {1,2,3,4,5,6,7,8,9,10};
//buf[10]で宣言したらbuf[9]までしか使えない
printf("%d\n",buf[10]);
}
buf[10]で宣言したらbuf[9]までしか使えません。
➡for文で回り過ぎた時
#include <stdio.h>
int main(void){
int buf[10];
// int i = 0; i < 10;i++){ が正解
for(int i = 1; i <= 10;i++){
buf[i] = i;
}
printf("%d\n",buf[0]);
}
これはOff-by-oneエラーと言うバグです。
<=と<を間違えて配列の境界外を参照してしまうバグです。
➡自動変数配列のサイズが大きすぎる時
#include <stdio.h>
int main(void){
//自動変数配列のサイズが多き過ぎる
char buf[0x7FFFffff] = {0};
printf("%d\n",buf[0]);
}
配列が大きすぎます。
自動変数ではなくて外部変数にするとうまくいく場合もあります。
➡fclose(NULL)を実行した時
#include <stdio.h>
int main(void){
FILE *fp = fopen("存在しなファイル","re");
if(fp == NULL){
perror(NULL);
fclose(fp);
}
}
➡strcpy()のバッファサイズをケチった時
#include <stdio.h>
#include <string.h>
int main(void){
char filename[6];
strcpy(filename,"jugemu");//終端文字の考慮が無い
puts(filename);
fflush(NULL);
strcpy(filename,"/etc/jugemu/jugemu/Goko/no/Surikire");//第二引数長すぎ
puts(filename);
fflush(NULL);
}
バッファサイズに余裕を持たせましょう。
➡fgets()のバッファサイズをケチった時
#include <stdio.h>
#include <stdlib.h>
int main(void){
//終端文字の考慮がない。
//ピッタリサイズでなくて余裕を持たせる。
char buf[4];
printf("4桁の数字を入れて下さい");
fgets(buf,BUFSIZ,stdin);
printf("%d\n",atoi(buf));
}
ピッタリサイズでなく余裕を持たせましょう。
➡malloc()で確保したサイズが小さい時
#include <stdio.h>
#include <stdlib.h>
int main(void){
//sizeof(int)はsizeof(double)の間違い
double *p = malloc(sizeof(int));
if(p == NULL){
perror(NULL);
return 1;
}
*p = 3.14 ;
printf("%lf\n",*p);
free(p);
}
intとdoubleでは大きさが違うのでmallocで確保した領域外を破壊します。
➡文字列リテラルに書き込みした時
#include <stdio.h>
int main(void){
char *p = "string" ;
*p = 'X' ;
}