PolyThinker の回答を拡張して、具体的な例を示します。
int foo(int a, int b) {
if (a && b)
return foo(a - 1, b - 1);
return a + b;
}
i686-pc-linux-gnu-gcc-4.3.2 -Os -fno-optimize-sibling-calls
出力:
00000000 <フー>:
0: 55 プッシュ %ebp
1: 89 e5 mov %esp,%ebp
3: 8b 55 08 mov 0x8(%ebp),%edx
6: 8b 45 0c mov 0xc(%ebp),%eax
9: 85 d2 テスト %edx,%edx
b: 74 16 je 23 <foo+0x23>
d: 85 c0 テスト %eax,%eax
f: 74 12 je 23 <foo+0x23>
11: 51 プッシュ %ecx
12: 12 月 48 日 %eax
13: 51 プッシュ %ecx
14: 50 プッシュ %eax
15: 8d 42 ffリー -0x1(%edx),%eax
18: 50 プッシュ %eax
19: e8 fc ff ff ff コール 1a <foo+0x1a>
1e: 83 c4 10 追加 $0x10,%esp
21: eb 02 jmp 25 <foo+0x25>
23:01 d0 %edx、%eax を追加
25: c9 立ち去る
26: c3 ret
i686-pc-linux-gnu-gcc-4.3.2 -Os
出力:
00000000 <フー>:
0: 55 プッシュ %ebp
1: 89 e5 mov %esp,%ebp
3: 8b 55 08 mov 0x8(%ebp),%edx
6: 8b 45 0c mov 0xc(%ebp),%eax
9: 85 d2 テスト %edx,%edx
b: 74 08 je 15 <foo+0x15>
d: 85 c0 テスト %eax,%eax
f: 74 04 je 15 <foo+0x15>
11: 12 月 48 日 %eax
12: 4a dec %edx
13: eb f4 jmp 9 <foo+0x9>
15: 5d ポップ %ebp
16:01 d0 %edx,%eax を追加
18: c3 ret
最初のケースで<foo+0x11>-<foo+0x1d>
は、関数呼び出しの引数をプッシュし、2 番目のケースでは、プリアンブルの後のどこかで<foo+0x11>-<foo+0x14>
変数と s を同じ関数に変更します。jmp
それがあなたが探したいものです。
プログラムでこれを行うことはできないと思います。可能性のあるバリエーションが多すぎます。関数の「肉」は、開始点に近いか離れている可能性があり、jmp
それを見ずにループまたは条件付きと区別することはできません。の代わりに条件付きジャンプである可能性がありますjmp
。 gcc
場合によっては をそのままにしておくかもしれませんが、call
他の場合には兄弟呼び出しの最適化を適用します。
参考までに、gcc の「兄弟呼び出し」は、末尾再帰呼び出しよりも少し一般的です。事実上、同じスタック フレームを再利用しても問題ない関数呼び出しは、潜在的に兄弟呼び出しです。
[編集]
自己再帰を探すだけでcall
誤解を招く例として、
int bar(int n) {
if (n == 0)
return bar(bar(1));
if (n % 2)
return n;
return bar(n / 2);
}
GCC は、兄弟呼び出しの最適化を 3 つのbar
呼び出しのうち 2 つに適用します。call <bar+..>
生成されたアセンブリにa があっても、その単一の最適化されていない呼び出しが単一のレベルより先に進むことはないため、私はこれを末尾呼び出し最適化と呼んでいます。