無限再帰によるスタック オーバーフローを回避したい場合は、残念ながら、新しいアクティベーション レコードが常にスタックにプッシュされないように、スタックを変更するためにアセンブリを掘り下げる必要があります。オーバーフローを引き起こします。関数の最後で再帰呼び出しを行うため、これは、再帰が一般的な他の言語 (つまり、Lisp、Scheme、Haskell など) では、トレイル コールの最適化として呼び出されます。基本的に末尾呼び出しをループに変換することで、スタック オーバーフローを防ぎます。Cでは次のようになります(注:x86でgccでインラインアセンブリを使用しており、引数をint
からに変更しましたdouble
組み立てを簡単にするため。また、関数名の名前マングリングを避けるために、C++ から C に変更しました。最後に、各ステートメントの末尾にある "\n\t" は実際のアセンブリ コマンドではありませんが、gcc でのインライン アセンブリに必要です):
#include <stdio.h>
void rek(int x)
{
printf("Value for x: %d\n", x);
//we now duplicate the equvalent of `rek(x+1);` with tail-call optimization
__asm("movl 8(%ebp), %eax\n\t" //get the value of x off the stack
"incl %eax\n\t" //add 1 to the value of x
"movl 4(%ebp), %ecx\n\t" //save the return address on the stack
"movl (%ebp), %edx\n\t" //save the caller's activation record base pointer
"addl $12, %ebp\n\t" //erase the activation record
"movl %ebp, %esp\n\t" //reset the stack pointer
"pushl %eax\n\t" //push the new value of x on the stack for function call
"pushl %ecx\n\t" //push the return value back to the caller (i.e., main()) on the stack
"movl %edx, %ebp\n\t" //restore the old value of the caller's stack base pointer
"jmp rek\n\t"); //jump to the start of rek()
}
int main()
{
rek(1);
printf("Finished call\n"); //<== we never get here
return 0;
}
Ubuntu 10.04 で gcc 4.4.3 を使用してコンパイルすると、これはスタック オーバーフローのない無限ループでほとんど「永久に」実行されましたが、末尾呼び出しの最適化がないと、セグメンテーション フォールトですぐにクラッシュしました。このセクションのコメントから、__asm
スタック アクティベーション レコード スペースがどのように「リサイクル」されているかがわかります。これにより、新しい呼び出しごとにスタックのスペースが使い果たされなくなります。これには、古いアクティベーション レコード (前の呼び出し元のアクティベーション レコードのベース ポインターと戻りアドレス) にキー値を保存し、それらを復元することが含まれますが、関数の次の再帰呼び出しのために引数が変更されます。
繰り返しますが、他の言語、主に関数型言語は、言語の基本機能として末尾呼び出しの最適化を実行します。したがって、Scheme/Lisp/etc の末尾呼び出し再帰関数。このタイプのスタック操作は、既存の関数の最後のステートメントとして新しい関数呼び出しが行われたときに内部で行われるため、スタックがオーバーフローすることはありません。