スタックオーバーフローとは、プログラムがシステムによって割り当てられたスタックメモリを超えてメモリを消費する状況を指します。歴史的に、スタックはローカル変数、リターンアドレス、および関数呼び出し時の一時データの格納に使用されてきました。初期のC言語の実装では、スタックは非常に小さく、境界を超えることに対して保護されていませんでした。
問題は、関数または関数の呼び出しチェーンが過剰なローカル変数を使用するか、終了条件なしで再帰的な関数を呼び出すときに発生します。これにより、プログラムはスタックメモリの割り当てを超えてデータを書き込むことになり、エラー、クラッシュ、および脆弱性を引き起こします。
解決策は、メモリ使用量の少ない関数を設計し、深いまたは無限の再帰を避け、大きなオブジェクトをスタックに配置しないことです。オペレーティングシステムは、メモリセグメントの保護(ガードページ)を通じてスタックのオーバーフローを防止できますが、開発者はオーバーフローを引き起こさないコードを書く責任があります。
無限再帰を引き起こすスタックオーバーフローのコード例:
void foo() { int arr[1000]; // 大きなローカル配列は問題を悪化させる foo(); // 終了条件のない再帰呼び出し } int main() { foo(); return 0; }
主な特徴:
関数内で非常に大きなローカル配列(例: int arr[1000000])を宣言した場合、どうなるか?
答え: 大きなローカル配列は直ちにスタック全体を使用する可能性があります。OSやコンパイラによっては、これは関数の実行時にクラッシュを引き起こすか、プログラム全体がクラッシュすることになります。
コード例:
void func() { int arr[1000000]; // 非常に多くのメモリを使用 arr[0] = 1; }
再帰は常にスタックオーバーフローを引き起こすのか?
答え: いいえ、再帰は深さに制限がある場合に有用です。オーバーフローは再帰の深さが大きいか、制限がない場合にのみ発生します。
メモリ削減のために関数内に大きな静的配列を配置してもよいか?
答え: いいえ、関数内の大きなstatic配列はメモリを占有しますが、スタックではなく静的データセグメントに占有されます。これは、一時的なローカルメモリが必要な場合には必ずしも効率的ではありません。
コード例:
void func() { static int arr[1000000]; // スタックではないが静的領域が永久に占有される }
プログラマーは、大きな配列のソートのためのクイックソートを再帰的に実装しましたが、呼び出しの深さを制限せず、終了条件を使用しませんでした。コードは実データの処理時にスタックオーバーフローを引き起こしました。
利点:
欠点:
別のプログラマーは、ヒープに小さなスタックを持つ反復実装を使用し、再帰の深さを制御し、大きな一時配列をmallocを通じて割り当てました。
利点:
欠点: