レッスン
理論 — コールスタック:スタックフレーム、戻りアドレス、オーバーフロー
コールスタックは、関数を呼び出すたびにフレームを確保するメモリ領域です。この例では、フレームに引数、戻りアドレス(呼び出し元で処理を再開する位置)、呼び出し元の保存したフレームポインタ、局所変数を置きます。スタックポインタは、使用中の領域の最も低いアドレスを指します。呼び出すとフレームの大きさだけ下がり、戻ると上がります。再帰の深さがd、各フレームがfバイトなら、必要なスタックはd·fバイトです。
各記号の意味
d- 深さ:同時にスタックにあるフレーム数
f- 各フレームのバイト数:ここでは12に、局所変数ごとに4を加える
- 前提
- 古典的なCの呼び出し規約を使う32ビットのマシンを想定しています。格納場所は各4バイトで、スタックは低いアドレスへ伸び、引数はスタックで渡されます。各フレームにはフレームポインタを保存します。実際の64ビット環境の呼び出し規約では、最初のいくつかの引数をレジスタで渡します。コンパイラがフレームポインタを省くこともあります。ARMでは、ほかの関数を呼び出さない関数なら、戻りアドレスをレジスタに保持できます。そのため、実際のフレームの大きさや配置は、この例の三つの格納場所とは異なります。
全プロセスの詳細解説
-
再帰はどこまで深くできる? 4 ステップ
fact(11)でオーバーフローを押してみましょう。このページのスタック容量は128バイトです。Microsoftのリンカーがプログラムに割り当てる既定のスタック容量は1 MB。これを1 MiB、つまり1,048,576バイトとして、このページと同じフレームで考えます。factはどこまで深く呼び出せるでしょうか。各呼び出しに4バイトの局所変数を一つ加えたらどうでしょう。また、fib(30)には何バイトのスタックが必要で、呼び出しは何回あるでしょうか。
-
この例のフレームには、引数、戻りアドレス、保存したフレームポインタが入ります。大きさは3 × 4 = 12バイト。128 / 12 = 10.67なので、収まるのは10フレームです。fact(11)には11 × 12 = 132バイト必要で、結果の表にもオーバーフローと表示されます。
-
1,048,576 / 12 = 87,381.33なので、87,381フレーム収まります。factは、スタックを使い切るまで深さ87,381の再帰が可能です。ただし、答えはそれよりはるかに早く32ビットのint型の範囲を超えます。13! = 6,227,020,800となる時点です。
-
4バイトの局所変数を一つ加えると、各フレームは16バイトになります。1,048,576 / 16 = 65,536で、ちょうど割り切れます。
-
fib(30)の最も長い呼び出しの連なりは30フレームなので、30 × 12 = 360バイトです。呼び出し回数は2·F(31) − 1。第31フィボナッチ数がF(31) = 1,346,269なので、2,692,537回になります。
解答
深さ87,381まで、局所変数を一つ加えると65,536までです。 fib(30)は2,692,537回呼び出されますが、必要なスタックは360バイトです。スタック使用量を決めるのは深さで、fibの実行時間を決めるのは呼び出し回数です。
-
学習の道すじ
コンピュータはどうやってプログラムを動かすのか
参考文献 (2)
- The worked problem's stack size: the linker's default reservation: Microsoft Learn, "/STACK (Stack allocations)," C/C++ build reference, updated 23 February 2022.
- The first insight's 1988 example: fingerd's unbounded read into a 512-byte buffer on the stack: Bob Page, "A Report on the Internet Worm." University of Lowell, 7 November 1988 (copy hosted at Toronto Metropolitan University).