ショートカット
ファシリテーター × あり方
コーディングの向こう側
Hello, ANOTHER world!
オブジェクト指向のはなし
プログラミングのはなし
C言語実力診断クイズ
eSkillBooks
Hello, ANOTHER world!

スタックと関数呼び出し

2つの関数 main() と sub() を考えましょう。sub() はサブルーチンで、main() の途中で呼び出されるものとします。2つの関数は当然メモリ上にあるので、次のようなイメージですね。
この図だとメモリの中でのイメージはよくわかりますが、こればかりだと疲れてしまうので、もう少し気楽な書き方に変えて、それからサブルーチンの呼び出しの様子を矢印で書き込んでみます。
main() の途中で関数呼び出しがあるので、呼び出し(=call)命令を表す記号として CAL と書きました。まず main() の中身が先頭から CAL まで実行されます。次に、CAL のところから sub() に移動します。sub() の先頭から最後まで実行されると、最後に RTS という記号で表された命令にたどり着きます。これはサブルーチンの呼び出し元に戻る命令(ReTurn from Sub-routine の略)なので、CAL の次の命令に戻って main() の残り部分が実行されます。
このような関数呼び出しのとき、スタックとプログラムカウンタ(PC)が巧妙に使われます。
main() 内で最初に実行される命令のアドレスを a とし、CAL 命令の次にある、つまり戻って来たときに最初に実行される命令のアドレスを b としましょう。それから、sub() 内で最初に実行される命令のアドレスを c としましょう。このとき、関数呼び出しの様子は次のようになります。
1)
プログラムカウンタには a がセットされている。CPU は a に格納されている命令から順に、プログラムカウンタの値を1つずつ増やしながら実行していく。やがて CAL 命令にたどり着く。
2)
CAL 命令が読み込まれたとき、プログラムカウンタはその次の命令のアドレスである b を指している。CPU は CAL 命令のための処理として、プログラムカウンタの値をスタックに入れ、ジャンプ先のアドレス c をプログラムカウンタに入れる。そして、c に格納されている命令から順に、プログラムカウンタの値を1つずつ増やしながら実行していく。やがて RTS 命令にたどり着く。
3)
RTS 命令が読み込まれたとき、プログラムカウンタはその次の命令のアドレスを指しているが、この値は無視される。CPU は RTS 命令のための処理として、スタックの値をプログラムカウンタに入れる。その結果、プログラムカウンタには戻り先のアドレスである b が入る。CPU は b に格納されている命令から順に、プログラムカウンタの値を1つずつ増やしながら実行していく。
これが関数呼び出しです。ただし、実際にはパラメータの受け渡しにもスタックが使われるので、スタックにはパラメータと戻り先アドレスの両方が入ることになります。
ところで、もしスタック中のデータが破壊され、戻り先アドレスが変わってしまったらどうなるでしょう。そういう場合、CPU はメモリ上のとんでもない場所を戻り先のプログラムと勘違いしてしまうので、クラッシュします。ですからスタックの中身をポインタで指して処理するときは、十分注意しないといけません。また、コンピューターウィルスの中にはスタックに無理矢理別の戻り先アドレスを埋め込むことで制御を奪ってしまうものもあります(バッファオーバーフローと呼ばれる手口です)。
さて、今までさんざん「メモリの中を想像しろ」と言ってきたので、今度もそれをやりましょう。次回はスタックが本当はどんな姿をしているのかについて説明します。

("Hello, ANOTHER world!" は2001年11月から2002年11月にかけて作成されたコンテンツです。)