16

無限再帰を伴う C プログラムを考えると、次のようになります。

int main() {

    main();

    return 0;
}

これにより、スタック オーバーフローが発生するのはなぜですか。これにより、次のスレッドの C++ で未定義の動作が発生することはわかっていますこの無限再帰は UB ですか? (そしてサイド ノードとしてmain()、C++ で呼び出すことはできません)。ただし、valgrind は、これがスタック オーバーフローにつながることを教えてくれます。

Stack overflow in thread 1: can't grow stack to 0x7fe801ff8

そして最後に、セグメンテーション エラーによりプログラムが終了します。

==2907== Process terminating with default action of signal 11 (SIGSEGV)
==2907==  Access not within mapped region at address 0x7FE801FF0

これも C で未定義の動作ですか、それとも実際にスタック オーバーフローにつながるはずですか? なぜスタック オーバーフローが発生するのでしょうか?

編集

1 C では無限再帰が許可されていることを知りたいですか?
2 これによりスタック オーバーフローが発生しますか? (十分に回答済み)

4

15 に答える 15

6

再帰は、次の反復に移動する前にローカル状態を暗黙的に保持する反復の一種です。これは、通常の関数が次々に呼び出していることを考えれば簡単に理解できます。

void iteration_2 (int x) {
    /* ... */
}

void iteration_1 (int x) {
    if (x > 0) return;
    iteration_2(x + 1);
}

void iteration_0 (int x) {
    if (x > 0) return;
    iteration_1(x + 1);
}

それぞれiteration_#()は基本的に互いに同一ですが、それぞれに独自のxがあり、それぞれがどの関数がそれを呼び出したかを記憶しているため、呼び出した関数が終了したときに呼び出し元に適切に戻ることができます。この概念は、プログラムが再帰バージョンに変換されても変わりません。

void iteration (int x) {
    if (x > 0) return;
    iteration(x + 1);
}

停止条件 (関数からのifチェック) を削除すると、反復は無限になります。return再帰から戻ることはありません。そのため、連続する関数呼び出しごとに記憶される情報 (ローカルxと呼び出し元のアドレス) は、OS がその情報を格納するためのメモリを使い果たすまで蓄積され続けます。

「スタック」をオーバーフローしない無限再帰関数を実装することが可能です。十分な最適化レベルでは、多くのコンパイラーが最適化を適用して、末尾再帰呼び出しのために何かを記憶するために必要なメモリーを削除できます。たとえば、次のプログラムを考えてみましょう。

int iteration () {
    return iteration();
}

でコンパイルするとgcc -O0、次のようになります。

iteration:
.LFB2:
        pushq   %rbp
.LCFI0:
        movq    %rsp, %rbp
.LCFI1:
        movl    $0, %eax
        call    iteration
        leave
        ret

ただし、 でコンパイルするとgcc -O2、再帰呼び出しが削除されます。

iteration:
.LFB2:
        .p2align 4,,7
.L3:
        jmp     .L3

この無限再帰の結果は単純な無限ループであり、「スタック」のオーバーランはありません。したがって、無限ループが許可されているため、無限再帰が許可されます。

ただし、再帰呼び出しは関数が最後に行うことではないため、プログラムは末尾呼び出しの最適化の候補ではありません。関数にはreturn、再帰呼び出しに続くステートメントがまだあります。再帰呼び出しが戻った後に実行する必要があるコードがまだあるため、オプティマイザーは再帰呼び出しのオーバーヘッドを取り除くことができません。その後のコードが実行できるように、呼び出しが正常に戻ることを許可する必要があります。そのため、プログラムは、呼び出しコードの戻りアドレスを格納するというペナルティを常に支払うことになります。

標準では、特定の用語で「無限再帰」について言及していません。ご質問に関連すると思われるものをまとめました。

  • 関数の再帰呼び出しが許可されている (C.11 §6.5.2.2 ¶11)

再帰的な関数呼び出しは、他の関数のチェーンを介して直接的および間接的に許可されます。

  • ステートメントへの再帰的なエントリは、ローカル変数の新しいインスタンスを作成します (C.11 §6.2.4 ¶5,6,7)

識別子がリンケージおよびストレージ クラス指定子 static なしで宣言されているオブジェクトは、一部の複合リテラルと同様に、自動ストレージ期間を持ちます。...

可変長配列型を持たないこのようなオブジェクトの場合、その有効期間は、関連付けられているブロックへのエントリから、そのブロックの実行が何らかの方法で終了するまで延長されます。(囲まれたブロックに入るか、関数を呼び出すと、現在のブロックの実行が中断されますが、終了しません。) ブロックに再帰的に入ると、オブジェクトの新しいインスタンスが毎回作成されます。...

可変長配列型を持つオブジェクトの場合、その有効期間は、オブジェクトの宣言から、プログラムの実行が宣言のスコープを離れるまで延長されます。スコープが再帰的に入力される場合、オブジェクトの新しいインスタンスが毎回作成されます。

標準では、多くの場所でメモリ割り当ての失敗について述べていますが、自動ストレージ期間を持つオブジェクトのコンテキストでは決してありません。標準で明示的に定義されていないものはすべて未定義であるため、自動保存期間を持つオブジェクトの割り当てに失敗したプログラムの動作は未定義です。これは、非常に長い関数呼び出しチェーンまたは再帰呼び出しが多すぎるプログラムの間でも同様に適用されます。

于 2013-08-17T03:35:27.733 に答える
3

質問への対処1:

Cで無限再帰が許可されていることを知りたいですか?

この記事Compilers and Termination Revisitedは、 が無限再帰を許可John Regehrするかどうかについての回答でC standardあり、標準をくまなく調べた後、それがあいまいであるという結論であることは私にとってそれほど驚くことではありません。この記事の主な目的は、無限ループと、さまざまな言語 (Cおよびを含むC++) の標準で非終了実行がサポートされているかどうかについてです。私が知る限り、この議論は無限再帰にも同様に適用されます。もちろん、スタック オーバーフローを回避できると仮定します。

C++セクションの1.10 Multi-threaded executions and data races段落で言うのとは異なり24

The implementation may assume that any thread will eventually do one of the
following:
  — terminate,
  [...]

の無限再帰を除外しているようC++です。draft C99 standardセクションの6.5.2.2 Function calls段落で言う11

再帰的な関数呼び出しは、他の関数のチェーンを介して直接的および間接的に許可されます。

これは再帰に制限を設けず、セクション5.1.2.3 Program execution段落でこれを述べています5

The least requirements on a conforming implementation are:
— At sequence points, volatile objects are stable in the sense that previous 
  accesses are complete and subsequent accesses have not yet occurred.
— At program termination, all data written into files shall be identical to the
  result that execution of the program according to the abstract semantics would
  have  produced.
— The input and output dynamics of interactive devices shall take place as
  specified in 7.19.3. The intent of these requirements is that unbuffered or     
  line-buffered output appear as soon as possible, to ensure that prompting
  messages actually appear prior to a program waiting for input.

記事にあるように、最初の条件は簡単に満たす必要があり、記事によると 3 番目の条件は実際には終了をカバーしていません。したがって、対処する 2 番目の条件が残ります。あいまいな記事によると、記事からの引用は次のとおりです。

抽象マシンで実行されているプログラムの終了について話している場合、プログラムが終了しないため、空虚に満たされます。コンパイラによって生成された実際のプログラムの終了について話している場合、ファイルに書き込まれたデータ (stdout はファイル) が抽象マシンによって書き込まれたデータと異なるため、C の実装にはバグがあります。(この読み方はハンス・ベームによるものです。私はこの微妙な点を標準から引き出すことに失敗しました。)

つまり、コンパイラ ベンダーは標準を一方的に読み、他の人 (私のようなもの) は逆に読みます。標準に欠陥があることは明らかです。C++ や Java のように、この動作が許可されているかどうかを明確にする必要があります。

2 番目の条件には 2 つの合理的ではあるが相反する解釈があるように思われるため、標準には欠陥があり、この動作が許可されるかどうかを明示的に定義する必要があります。

于 2013-08-17T18:37:58.500 に答える
3

関数呼び出し (main() を含む) を行うたびに、関数呼び出し "情報" (引数など) がスタックの一番上にプッシュされます。この情報は、関数が戻るときにスタックからポップされます。しかし、コードでわかるように、戻るに main を再帰的に呼び出すため、スタックは限界に達するまで増加し続け、それによってセグメンテーション エラーが発生します。

多くの場合、スタックのサイズは制限され、実行前に決定されます (たとえば、オペレーティング システムによって)。

これは、スタック オーバーフローが main() に限定されないことを意味しますが、ツリーを終了する適切な方法がない他の再帰関数 (つまり、基本ケース) に限定されます。

于 2013-08-14T09:08:54.280 に答える
2

関数がローカル変数や引数の受け渡しにスタック スペースを使用しない場合でも、戻りアドレスと (場合によっては) フレームのベース ポインターを格納する必要があります (gcc では、これは で無効にできます-fomit-frame-pointer)。

十分に高い最適化レベルでは、末尾呼び出しの最適化が適用可能な場合、コンパイラは再帰をループに書き直すことができる可能性があります。これにより、スタック オーバーフローが回避されます。

于 2013-08-14T08:59:27.777 に答える
1

C では無限再帰が許可されています。コンパイル時に、コンパイラはこれを許可しますが、そうすると実行時エラーが発生する場合があります。

于 2013-08-23T03:21:40.923 に答える
1

メイン メモリのスタック セクションは無限ではないため、関数を再帰的に無期限に呼び出すと、スタックは各関数呼び出しに関する情報でいっぱいになります。これにより、他の関数呼び出しに使用するスペースがなくなると、スタック オーバーフローが発生します。

于 2013-08-14T08:57:08.120 に答える
1

Cで無限再帰は許可されていますか? 簡単な答えはイエスです。コンパイラは、スタック スペースがなくなるまで関数を無限に呼び出すことができます。これを行うことを妨げるものではありません。

無限再帰は可能ですか? いいえ。既に指摘したように、関数を呼び出すたびに、関数が動作するために必要なパラメーターとともに、リターン アドレスをプログラム スタックにプッシュする必要があります。プログラムのスタック サイズは限られているため、スタックを使い果たすと、アプリケーションは失敗します。

偽の無限再帰は可能ですか? はい。自分自身を 1000 回呼び出し、その後 1000 回の関数呼び出しを終了できるようにする関数を設計して、スタックが元の関数呼び出しのみをスタックに持つようにすることは可能です...そして、プロセス全体をもう一度繰り返します無限ループ。ただし、この本当の無限再帰は考慮していません。

于 2013-08-22T22:00:08.857 に答える
0

原因 スタックは制限されており、関数を呼び出すたびに呼び出し先が保存されます (ベース ポインターをスタックにプッシュし、現在のスタック ポインターを新しいベース ポインター値としてコピーすることにより)。そのため、スタックを消費すると無限の呼び出しでオーバーフローします。ここで呼び出し規約とスタックの反応を参照してください ( http://www.csee.umbc.edu/~chang/cs313.s02/stack.shtml )

于 2013-08-14T09:06:42.067 に答える