コンテンツにスキップ

コールスタック

出典: フリー百科事典『ウィキペディア(Wikipedia)』
スタックフレームから転送)

コールスタックまたは呼び出しスタック: call stack)とは、プログラムで実行中のサブルーチン(関数)に関する情報を格納するスタックである。実行中のサブルーチンとは、呼び出されたもののまだ処理が完了していないサブルーチンのことを指す。実行スタック (execution stack)、制御スタック (control stack)、関数スタック (function stack) などとも呼ばれる。なお文脈によっては、短縮して単にスタックともいう。

概要

[編集]

コールスタックを使う目的はいくつかあるが、主たる目的はサブルーチンの処理を完了して制御を戻す(呼び出し側に戻る)ときに、どこに戻ればよいかを記憶しておくことである。

コールスタックはスタックとして構成されているので、呼び出し側はリターンアドレスをスタックに push し、呼び出されたサブルーチンが完了したときにリターンアドレスをコールスタックから pop する(そしてそのアドレスに制御を戻す)。呼び出されたサブルーチンがさらに別のサブルーチンを呼び出す場合も、リターンアドレスをコールスタックに push し、プログラムに書かれている通りに情報をスタックに積んだり下ろしたりする。あるサブルーチンに関する情報をコールスタックに載せることをワインド (巻き上げ, winding)、逆にそれを削除することをアンワインド (巻き戻し, unwinding) と呼ぶ。また、サブルーチンの呼び出しごとにコールスタックに格納するひとまとまりの情報の集合をスタックフレーム (stack frame) または単にフレームと呼ぶ[1]

なお、コールスタックに割り当てられている領域を使い切ると「スタックオーバーフロー」と呼ばれる実行時エラーが発生する。スタックオーバーフローが発生したときの動作はプログラミング言語や実行環境によって異なるが、通例プログラムの異常終了といった未定義動作を引き起こし、回復不可能であることが多い。

1つの実行中のプログラム(より正確に言えばスレッド)には、1つのコールスタックが対応して存在する。シグナル処理や協調的マルチタスク処理で追加のスタックを使う場合もあるが、通常使用中のコールスタックは常に1つなので、これを単に「(そのタスクの)スタック」と呼ぶことがある。

低水準言語アセンブリ言語)の多くでは、プログラマが明示的にスタックを操作する必要がある。一方、高水準言語からはコールスタックは透過的である。つまりコールスタックの存在を意識することなく、呼び出し階層構造によって実現される上位概念としてのプログラムロジックにのみ集中できるということである。コールスタックの詳細はプログラマからは見えず、引数あるいはローカル変数といった形でスタックから切り出された部分領域だけにアクセス可能で、スタックを構成しているメモリ全体にアクセスすることはできない。識別子を使ったサブルーチン(関数)の呼び出しは言語処理系によって対応するアドレスへのジャンプ命令に解決され、またスタックへのリターンアドレスの格納やリターンアドレスへの復帰といった下位レベルの前処理・後処理も隠蔽される。x86のcallとretのように、アセンブラレベルでもそのような下位レベルのスタック操作を隠蔽する命令が用意されているアーキテクチャもある。

プログラミング言語におけるスタックの詳細は、コンパイラオペレーティングシステム (OS)、命令セットなどに依存する。x64のように、特定の条件を満たす関数引数に関しては、スタックを使わずレジスタを使って渡すアーキテクチャもある[2]

いずれにせよ、言語環境を問わず、ソフトウェアを正常動作させるにはコールスタックを正しく保つことは重要である。コールスタックの容量は、デスクトップOS環境であっても既定で(スレッドごとに)数MiB程度しかなく[3][4]、組み込み環境ではさらに制限が厳しい。高水準言語では普段コールスタックの存在を意識しないで済むがゆえに、ヒープではなくスタック上に巨大な配列を確保して容量を使い切ってしまい、スタックオーバーフローを発生させてしまうといった初歩的な間違いを犯すこともある。

具体例

[編集]

コールスタックが関係する具体例として、次の擬似コードを挙げる。

subroutine DrawSquare(Point p1, Point p2, Point p3, Point p4)
{
    ... 略 ...
    
    DrawLine(p1, p2);
    DrawLine(p2, p3);
    DrawLine(p3, p4);
    DrawLine(p4, p1);
    
    ... 略 ...
}

上の疑似高水準言語のコードでは、サブルーチンDrawSquare内の4ヶ所から、直線を描画するサブルーチンDrawLineを呼び出すとしたとき、DrawLineは4ヶ所のうちのどこに戻ればよいかを知る必要がある。一般にDrawSquareのコード内でDrawLineを呼び出しているそれぞれの箇所で、呼び出し処理の次の命令のアドレス(これをリターンアドレスと呼ぶ)をコールスタックに格納することでこれを実現する。

コールスタックの機能

[編集]

前述のように、コールスタックの第一の用途は以下の通りである。

リターンアドレスの格納
サブルーチンが呼び出されたとき、戻るべき命令のアドレスをどこかに記憶しておく必要がある。スタックを使ったリターンアドレスの格納は他の方法にはない利点がある。第一に、各タスクは対応するスタックを持っているので、サブルーチンは再入可能(リエントラント)、つまり複数のタスクが同時に同じサブルーチンを実行することが可能となる。第二に、再帰呼び出しが可能となるという利点がある。関数自身は再帰的に呼び出されたとしても、リターンアドレスは呼び出される度に記憶しておかなければならない。スタックを使うとこの機能が自動的にサポートされる。

言語、オペレーティングシステム、ハードウェア環境に依存するが、コールスタックはそれ以外の機能も持つことがある。そのような機能として以下のものがある。

局所データ格納域
多くのサブルーチンは局所変数自動変数英語版)の値を格納するメモリ領域を必要とする。局所変数とは実行中のサブルーチンでのみ使われる変数で、そのサブルーチンの処理が終われば値を必要としない。このためにスタックのトップを動かして空き領域を作り、局所変数に利用することができる。これは動的メモリ確保に比べると非常に高速に行える。サブルーチンが呼び出される度にスタック上の局所データの領域が確保される点に注意されたい。
引数受け渡し
サブルーチンには引数を必要とするものがある。引数は呼び出し側のコードが提供し、その引数をコールスタック上に置くことは珍しくない。一般に引数の個数が少なければ、プロセッサのレジスタが引数の受け渡しに使われる。しかし、引数の個数が利用可能なレジスタ数より多ければ、何らかのメモリ領域を使わざるを得ない。コールスタックはそのような値の受け渡しには最適で、サブルーチンの呼び出しの度に固有の引数が渡されるのに対して、コールスタックも呼び出しの度に固有の領域を与えられる。
評価スタック
論理演算や数値演算のオペランドは多くの場合レジスタに置かれて処理される。しかし、式が複雑になるとレジスタだけでは収まりきらなくなり、何らかのメモリ領域が必要となる。そのようなオペランドのためのスタック(逆ポーランド記法の電卓に似ている)は評価スタック (evaluation stack) と呼ばれ、コールスタックを利用して実装することがある。
現在のインスタンスへのポインタ
C++のようなオブジェクト指向言語では、メソッド呼び出しの際にthisポインタを引数と共にコールスタックに格納する。thisポインタは呼び出されるメソッドに対応するオブジェクトインスタンスを指している。thisポインタはオブジェクト指向言語のコンテキストの基本要素であり、現在のオブジェクトの持つプライベートデータへのアクセスを提供する。thisポインタはオブジェクト指向プログラミングとコールスタックを結びつけるものである。
ルーチンの入れ子における静的スコープサポート
PascalAdaといったプログラミング言語はサブルーチンの入れ子が可能であり、内側のルーチンが外側のルーチンのコンテキスト(外側のルーチンの引数や局所変数)にアクセスできるようになっている。この静的な入れ子はいくつも繰り返すことができ、関数の中に別の関数を定義し、その中でさらに別の関数を定義し……といったことが可能である。このため実装に当たっては呼び出された関数が静的な入れ子を遡って外側のフレームにアクセスできる手段を提供する必要がある。一般に外側のフレームへのポインタとしてこの参照を実装し、これを「ダウンスタック・リンク」または「スタティック・リンク」と呼んで、直前の呼び出し側ルーチンとのリンク(ダイナミック・リンク)と区別する(呼び出し側は定義上の外側のルーチンとは限らない)。例えば、内側のルーチンは自分自身を再帰呼び出しできるようになっている言語が多く、同じルーチンのスタックフレームがコールスタック上にいくつも重なることがあり、それらが全て同じ外側のルーチンのコンテキストへのスタティック・リンクを持つことになる。スタティック・リンクの代わりに、外側のスタックフレームへの参照を集めてポインタの配列とする方式もある。この配列を display と呼び、インデックスを指定することで必要なフレームを得ることができる。バロース B5000 はハードウェアでこれをサポートしており、32レベルの静的入れ子を使用可能だった。
他のリターンステータス
リターンアドレスだけでなく、環境によってはサブルーチンから復帰する際に戻さなければならないハードウェアやソフトウェアのステータスがあるかもしれない。例えば、特権レベル、例外処理情報、演算モードなどである。必要に応じてこれらもリターンアドレスのようにコールスタックに格納される。

典型的なコールスタックはリターンアドレス、局所データ、引数を格納する(これを「コールフレーム」と呼ぶ)。環境によってはコールスタックの機能に差異がある。例えばFORTH言語では、コールスタックにはリターンアドレスと局所変数のみが格納され(これをリターンスタックと呼ぶ)、引数は別のデータスタックに格納される。多くのFORTHの実装では浮動小数点数の引数を格納するための第三のスタックが存在する。

構造

[編集]

コールスタックはスタックフレームから構成される(アクティベーションレコードとも呼ばれる)。スタックフレームはマシン依存のデータ構造であり、サブルーチンの状態情報が格納される。各スタックフレームは完了していないサブルーチン呼び出しに対応する。例えば、DrawSquareから呼び出されたDrawLineを現在実行中としたとき、コールスタックのトップ部分は下図のようになる。

スタックトップのスタックフレームは現在実行中のルーチンのためのものである。最も典型的な手法では、スタックフレームには次の情報が格納されている。

  • そのルーチンの局所変数領域
  • 呼び出し側に戻るためのリターンアドレス
  • そのルーチンに渡された引数

フレーム内のメモリ領域はスタックポインタと呼ばれるレジスタを使ってアクセスされることが多い。スタックポインタはスタックのトップを指している。別の方法として、スタックポインタとは別のレジスタ(フレームポインタと呼ばれることが多い)を使うこともある。フレームポインタはフレームの中の決まった場所を指していて、例えばリターンアドレスが格納されている位置を指している。

スタックフレームは必ずしも同じサイズではない。サブルーチン毎に引数の個数も違うので、スタックフレームのサイズも異なる。ただし、同じサブルーチンを呼び出したときのスタックフレームは一般に同じサイズとなる。同様に局所変数領域もサブルーチンが違うとサイズが変わってくる。実際、言語によってはスタック上に動的にメモリを確保する機能を持っているので、同じサブルーチンを呼び出してもフレームサイズは変わってくるし、そのサイズはコンパイル時にはわからない。そのような場合、スタックポインタではなくフレームポインタでアクセスする必要が生じる。というのは、スタックポインタからリターンアドレス格納位置までのオフセットがコンパイル時に判明しないからである。

多くのシステムでは、スタックフレーム内に前のフレームポインタレジスタの値を格納する場所がある。つまり呼び出し側ルーチンを実行していたときに使っていたフレームポインタの値である。例えば、上図のDrawLineのスタックフレーム内にDrawSquareが使っているフレームポインタの値を格納する場所があるということになる。その値はサブルーチン呼び出し時に格納され、復帰時に戻される。そのようなフィールドがスタックフレーム内の所定の位置にあると、コールスタックに積まれているスタックフレーム群を(さかのぼって)辿っていくことが可能となる。

場合によっては、スタックフレームはオーバーラップしていると見なすこともできる。オーバーラップしているのは、呼び出し側から呼び出されたルーチンに渡される引数の部分である。環境によっては呼び出し側はスタックに引数をpushして自身のスタックフレームを拡張し、その後に呼び出しを行う。また別の環境では、各サブルーチンは自身が呼び出すかもしれない別のサブルーチンへの引数の領域を予めスタックフレーム内に確保していることがある。この領域は outgoing arguments area あるいは callout area と呼ばれる。この手法ではコンパイラが呼び出す可能性のあるサブルーチンのうち最大の引数領域を必要とするものを予め求めて領域サイズを決定する。

使用法

[編集]

呼び出し側処理

[編集]

通常、サブルーチンを呼び出す側でのコールスタック処理は最小限になっている。呼び出すコードがあちこちに存在することを考慮すれば、こうすることでコードの増大を抑えることができる。実際の引数の値は呼び出し毎に固有なので呼び出し側で評価され、呼出規約に従ってスタックにpushされるかレジスタに置かれる。「Branch and Link」のような実際の呼び出し命令が制御をターゲットのサブルーチンに転送するために実行される。

呼ばれた側の処理

[編集]

呼ばれたサブルーチンでは、最初にサブルーチンプロローグ英語版と呼ばれるコードを実行する。そこで実際のコードを実行する前に行わなければならない細々とした処理を行う。

プロローグでは、一般に呼び出し命令が所定のレジスタに置いたリターンアドレスをコールスタックにpushする。同様に現在のスタックポインタかフレームポインタ(あるいは両方)をpushする。命令セットアーキテクチャによってはこれらが呼び出し命令の一部として実行され、そのような環境ではプロローグですべきことは無い。

フレームポインタを使っている場合、プロローグではフレームポインタに新たな値をセットする(スタックポインタの値を活用する)。局所変数の領域は必要に応じて徐々にスタックポインタを変化させて確保していく。

FORTH言語ではコールスタック(リターンスタック)を明示的にワインドすることができる。Scheme言語では「動的ワインド (dynamic wind)」という機能があり、スタック上に特殊なフレームをワインドすることができる。

復帰処理

[編集]

サブルーチンから復帰することができる状態になると、プロローグの逆のエピローグ処理が行われる。これは一般的には保存されていたレジスタの値(フレームポインタなど)をスタックフレームからリストアし、スタックポインタの値を変更してスタックフレーム全体をpopし、最後にリターンアドレスに分岐する命令を実行する。多くの呼出規約ではエピローグ処理でpopする範囲に元々の引数も含まれる。その場合、呼び出した側に戻ったときにすべきことは何もない。呼出規約によっては、引数部分のpopを呼び出し側の責任で行うものがある。

アンワインド

[編集]

呼び出された関数から復帰するとスタックのトップにあったフレームがpopされ、戻り値が残される。

Pascalなどの言語は関数の入れ子を越えた広域のgoto文をサポートしており、呼び出し側関数に制御を移すことができる。このとき、スタックのアンワインドを行って、goto文の戻り先の関数に対応したスタックフレームまで戻す必要がある。このような制御の転送は一般にエラー処理にのみ使われる。

スタックは例外処理の際にもアンワインドされなければならない。例外をサポートするためには、スタックフレームにさらに例外ハンドラを示すエントリが必要となる。例外がスローされると、スタックはその例外を処理できる例外ハンドラが見つかるところまでアンワインドされる。Common Lispではスタックがアンワインドされたときに起きることを制御するunwind-protectという特殊な形式がある。

継続を適用する場合、スタックは一度アンワインドされ、再度ワインドされて実行を継続する。継続を実装する方法はこれだけではなく、明示的に複数のスタックを用意して継続するアプリケーションが単にそのスタックを起動して渡すべき値をワインドする。

コールスタックとソフトウェアテスト

[編集]

2006年、コールスタックを使ったこれまでとは全く異なる技法が発表された[5]。それはコールスタックを使った test suite reduction と呼ばれる技法である。大まかに言えば、実行時のコールスタックが同じになるテストケースは等価だとみなしてテスト件数を減らしつつ、テストスイート全体の問題検出能力を維持するという考え方である[6]

性能解析

[編集]

無作為にコールスタックの標本を採取することで、プログラムの性能最適化に利用することができる。コールスタック上によく現れるサブルーチンは頻繁に呼び出されるか1回の実行に時間がかかっていると想定でき、その呼び出し回数を減らしたり、1回の実行にかかる時間を短縮することで大きな効果が期待できる。詳しくは性能解析を参照。

セキュリティ

[編集]

コード(リターンアドレス)とデータ(引数、戻り値、局所変数)がコールスタックに混在していることはセキュリティ上危険である。詳しくはバッファオーバーランおよびスタックを参照されたい。

脚注・出典

[編集]
  1. ^ Interstage Application Server/Interstage Web Server チューニングガイド - 7.1.4 スタック
  2. ^ x64 calling convention | Microsoft Learn
  3. ^ /STACK (Stack allocations) | Microsoft Learn
  4. ^ Threading Programming Guide - Thread Management | Apple Developer Documentation Archive
  5. ^ “Call Stack Coverage for GUI Test-Suite Reduction” by Scott McMaster and Atif M. Memon. In Proceedings of the 17th IEEE International Symposium on Software Reliability Engineering (ISSRE 2006), Nov. 2006.
  6. ^ “Call-Stack Coverage for GUI Test-Suite Reduction” by Scott McMaster and Atif M. Memon. IEEE Trans. Softw. Eng., 2008, IEEE Press.

関連項目

[編集]

外部リンク

[編集]