web-dev-qa-db-ja.com

セグメンテーション障害の一般的な理由の決定的なリスト

注:多くのセグメンテーション違反の質問があり、ほぼ同じ答えがあります。そのため、 ndefined reference の場合と同様に、それらを標準的な質問にまとめようとしています。

セグメンテーション障害とは をカバーする質問がありますが、whatをカバーしますが、多くの理由はリストしません。一番上の答えは「多くの理由があります」と言っており、1つだけをリストし、他の答えのほとんどは理由をリストしていません。

全体として、このトピックに関するよく組織化されたコミュニティwikiが必要だと思います。 。答えの免責事項に記載されているように、目的はデバッグを支援することです。

セグメンテーションフォールトが何であるかは知っていますが、コードがよく見えるものを知らずにコードを見つけるのは難しい場合があります。完全にリストするには多すぎるのは間違いありませんが、CおよびC++のセグメンテーションフォールトの最も一般的な原因は何ですか?

54
CodeMouse92

警告!

以下は、potentialセグメンテーション違反の理由です。すべての理由をリストすることは事実上不可能です。このリストの目的は、既存のセグメンテーション違反の診断を支援することです。

セグメンテーションエラーと未定義の動作の関係cannotセグメンテーション違反を引き起こす可能性のある以下の状況はすべて、技術的には未定義の動作です。 これは、セグメンテーション違反だけでなく、anythingを実行できることを意味します-誰かがUSENETでかつて言ったように、「 悪魔があなたの鼻から飛び出します。 "。未定義の動作が発生するたびにセグメンテーション違反が発生することを期待しないでください。 Cおよび/またはC++にどの未定義の動作が存在するかを学習し、それらを含むコードを記述しないでください!

未定義の動作に関する詳細:


セグフォールとは何ですか?

要するに、コードがへのアクセス許可を持っていないメモリにアクセスしようとすると、セグメンテーション違反が発生します。すべてのプログラムには、動作するメモリ(RAM)が割り当てられており、セキュリティ上の理由から、そのチャンク内のメモリにのみアクセスできます。

セグメンテーション障害isについてのより詳細な技術的説明については、 セグメンテーション障害とは何ですか? を参照してください。

セグメンテーション障害エラーの最も一般的な理由は次のとおりです。繰り返しますが、これらは既存のセグメンテーション違反の診断に使用する必要があります。それらを避ける方法を学ぶには、あなたの言語のundefined behaviorsを学んでください。

このリストは、独自のデバッグ作業に代わるものではありません。 (答えの下部にあるそのセクションを参照してください。)これらはあなたが探すことができるものですが、デバッグツールは問題に焦点を合わせる唯一の信頼できる方法です。


NULLまたは初期化されていないポインターへのアクセス

ポインターがNULL(ptr=0)または完全に初期化されていない(まだ何も設定されていない)場合、そのポインターを使用してアクセスまたは変更を試みると、未定義の動作が発生します。

int* ptr = 0;
*ptr += 5;

失敗した割り当て(mallocまたはnewなど)はnullポインターを返すため、操作する前に常にポインターがNULLでないことを確認する必要があります。

また、初期化されていないポインター(および一般的な変数)のreading値(参照解除なし)も未定義の動作であることに注意してください。

Cのprintステートメントでこのようなポインターを文字列として解釈しようとする場合など、未定義のポインターのこのアクセスは非常に微妙な場合があります。

char* ptr;
sprintf(id, "%s", ptr);

こちらもご覧ください:


ダングリングポインターへのアクセス

mallocまたはnewを使用してメモリを割り当て、その後freeまたはdeleteを介してそのメモリをポインタで使用すると、そのポインタはダングリングポインタと見なされます。それを間接参照すること(および単にreadingその値-NULLなどの新しい値を割り当てなかった場合)は未定義の動作であり、セグメンテーションフォールトを引き起こす可能性があります。

Something* ptr = new Something(123, 456);
delete ptr;
std::cout << ptr->foo << std::endl;

こちらもご覧ください:


スタックオーバーフロー

[いいえ、あなたが今いるサイトではなく、namedでした。]単純化しすぎて、「スタック」は注文用紙を突き刺すようなものですいくつかのダイナー。この問題は、いわば、そのスパイクにあまりにも多くの注文を出したときに発生する可能性があります。コンピューターでは、動的に割り当てられていない変数、およびCPUによってまだ処理されていないコマンドはすべてスタックに置かれます。

この原因の1つは、関数が停止することなく自分自身を呼び出す場合など、深い再帰または無限再帰です。そのスタックがオーバーフローしたため、注文用紙は「落ち」始め、それらのために意図されていない他のスペースを占有し始めます。したがって、セグメンテーション違反が発生する可能性があります。別の原因は、非常に大きな配列を初期化しようとした可能性があります。これは単一の順序ですが、それ自体ですでに十分に大きいものです。

int stupidFunction(int n)
{
   return stupidFunction(n);
}

スタックオーバーフローの別の原因は、一度に多すぎる(動的に割り当てられない)変数を持つことです。

int stupidArray[600851475143];

野生でのスタックオーバーフローの1つのケースは、関数の無限再帰を防ぐことを目的とした条件文のreturnステートメントの単純な省略から生じました。その話の教訓、は常にエラーチェックが機能することを保証します!

こちらもご覧ください:


ワイルドポインター

メモリ内のランダムな場所へのポインターを作成することは、コードでロシアンルーレットをプレイするようなものです。アクセス権のない場所へのポインターを簡単に見逃して作成してしまう可能性があります。

int n = 123;
int* ptr = (&n + 0xDEADBEEF); //This is just stupid, people.

一般的な規則として、リテラルのメモリ位置へのポインタを作成しないでください。一度働いても、次回は働かないかもしれません。特定の実行時にプログラムのメモリがどこにあるかを予測することはできません。

こちらもご覧ください:


配列の終わりを超えて読み込もうとしています

配列はメモリの連続した領域であり、連続する各要素はメモリ内の次のアドレスに配置されます。ただし、ほとんどの配列には、その大きさや最後の要素が何であるかという生来の感覚がありません。したがって、特にポインター演算を使用している場合は、配列の終わりを過ぎて吹き飛ばして簡単に知ることはできません。

配列の最後を過ぎて読むと、初期化されていないか、他の何かに属しているメモリに入ってしまう可能性があります。これは技術的には未定義の動作です。セグメンテーション違反は、多くの潜在的な未定義の動作の1つにすぎません。 [率直に言って、ここでセグメンテーション違反が発生した場合、幸運です。他の人は診断が難しい。]

// like most UB, this code is a total crapshoot.
int arr[3] {5, 151, 478};
int i = 0;
while(arr[i] != 16)
{
   std::cout << arr[i] << std::endl;
   i++;
}

または、<=の代わりに<forを使用して頻繁に表示されるもの(1バイトを読みすぎます):

char arr[10];
for (int i = 0; i<=10; i++)
{
   std::cout << arr[i] << std::endl;
}

または、うまくコンパイル( here を参照)し、dim要素ではなくdimで初期化された1つの要素のみを割り当てる不運なタイプミスです。

int* my_array = new int(dim);

さらに、配列の外側を指すポインターを作成することもできません(逆参照は言うまでもありません)(そのようなポインターは、配列内の要素または末尾の1つを指す場合にのみ作成できます)。それ以外の場合は、未定義の動作をトリガーしています。

こちらもご覧ください:


C文字列のNULターミネータを忘れる。

C文字列自体は、いくつかの追加の動作を伴う配列です。文字列として確実に使用するには、ヌルで終了する必要があります。つまり、末尾に\0が必要です。これは、場合によっては自動的に行われ、他の場合では行われません。

これを忘れると、Cの文字列を処理する一部の関数はいつ停止するのか分からなくなり、配列の終わりを超えて読み取る場合と同じ問題が発生する可能性があります。

char str[3] = {'f', 'o', 'o'};
int i = 0;
while(str[i] != '\0')
{
   std::cout << str[i] << std::endl;
   i++;
}

C文字列では、\0が違いを生むかどうかは本当に当たり前です。未定義の振る舞いを避けることができると仮定する必要があります:char str[4] = {'f', 'o', 'o', '\0'};


文字列リテラルを変更しようとしています

文字列リテラルをchar *に割り当てると、変更できません。例えば...

char* foo = "Hello, world!"
foo[7] = 'W';

...トリガー未定義の動作、およびセグメンテーション違反は、1つの可能な結果です。

こちらもご覧ください:


不一致の割り当て方法と割り当て解除方法

mallocfreeを一緒に、newdeleteを一緒に、new[]delete[]を一緒に使用する必要があります。混同すると、セグメンテーション違反やその他の奇妙な動作が発生する可能性があります。

こちらもご覧ください:


ツールチェーンのエラー。

コンパイラのマシンコードバックエンドのバグは、有効なコードをセグメンテーション違反の実行可能ファイルに変換する可能性が非常に高いです。リンカのバグも間違いなくこれを行うことができます。

これは、ユーザーのコードによって呼び出されるUBではないという点で特に怖いです。

そうは言ったが、問題が他の方法で証明されるまで、あなたは常にあなたであると仮定すべきです。


その他の原因

セグメンテーション違反の考えられる原因は、未定義の動作の数とほぼ同じくらい多く、標準のドキュメントでさえリストするには多すぎます。

確認する一般的ではないいくつかの原因:


デバッグ

デバッグツールは、セグメンテーション違反の原因の診断に役立ちます。デバッグフラグ(-g)を使用してプログラムをコンパイルし、デバッガで実行して、セグメンテーション違反が発生しそうな場所を見つけます。

最近のコンパイラは、-fsanitize=addressを使用したビルドをサポートしています。これにより、通常、プログラムの実行速度は約2倍遅くなりますが、アドレスエラーをより正確に検出できます。ただし、他のエラー(初期化されていないメモリからの読み取りやファイル記述子などの非メモリリソースのリークなど)はこのメソッドではサポートされておらず、多くのデバッグツールと ASan を使用することはできません同じ時間。

メモリデバッガー

  • GDB | Mac、Linux
  • valgrind(memcheck)| Linux
  • メモリー博士|ウィンドウズ

さらに、静的分析ツールを使用して未定義の動作を検出することをお勧めしますが、これも単に未定義の動作を見つけるためのツールであり、すべての未定義の動作を検出することを保証するものではありません。

ただし、本当に運が悪い場合は、デバッガー(または、まれにデバッグ情報を使用して再コンパイルする)を使用すると、プログラムのコードとメモリに十分な影響を与え、セグメンテーション違反が発生しなくなる可能性があります( heisenbug

67
CodeMouse92