GAIO CLUB

2023年07月20日

【第10回】スタックフレーム

静的解析/コンパイラ技術
前回は、静的変数や関数をどのようにメモリ上に配置し、初期化するのかの話をしました。
今回は引数やオート変数がどこに配置されるのかという話です。

スタックエリア

引数やオート変数は、スタックフレームと呼ぶエリアに配置します。スタックフレームはRAMに作られます。スタックフレームには、引数とオート変数だけでなく、関数が動くために必要な情報が保存されています。

通常、StartUpで指定されたアドレスから0番地方向に向かって使用します。スタックフレームの量は関数呼出しで増え、リターンで減ります。今現在どこまでスタックを使ったかを管理するポインタをスタックポインタと言います。スタックポインタはCPUが持つレジスタです。レジスタはメモリとは別の記憶域で、高速にアクセスができ、主に機械語の演算命令で使用されます。レジスタはCPU毎に大きさ(ビット長)や数が決まっています。スタックポインタは通称SPと呼びますが、SPという名称のレジスタがある場合と無い場合があります。SPレジスタが無くても、他のレジスタがSPの代わりをしています。

SPの指すスタックフレームが増減してプログラムが動き、SPの移動する範囲をスタックエリアと呼びます。

以下に、ARMのレジスタとスタックエリアを示します。レジスタは他にもありますが、汎用レジスタという頻繁に使用するものだけ載せています。

スタックフレームの構造

同じ関数を書いても、スタックフレームの構造とサイズはコンパイラにより差があります。

一般的にスタックフレームに置かれるデータは以下のもので、ひとつの関数が使用するスタックフレームサイズは、関数の内容とコンパイラによって異なります。

・引数
・オート変数
・リターンアドレス
・リターン値設定アドレス
・レジスタ退避域
・一時変数

各情報のサイズは、Cソースプログラムとコンパイラのマニュアルから想定できるものと、できないものがあります。

解りやすいのは引数とオート変数ですが、スタック上に確保する変数にはアラインメント調整のために空き領域が取られることや、コンパイラの最適化により変数が無くなることもあり、ソースプログラムの記述だけでは正確に判断できません。

リターンアドレスは関数が呼び出した親関数へ戻るためのアドレスです。この情報を辿ることで、関数のネスト状況を知ることもできます。

通常、リターン値はレジスタを使って返しますが、サイズの大きな構造体はレジスタに入らず、別の方法で返すための戻り値アドレスをスタックに保存することがあります。

レジスタ退避域は、親関数の使っているレジスタを壊した時に元に戻すため、保存しておくエリアです。
一時変数とはコンパイラが勝手に作る変数で、たとえば以下の式の括弧式の値を一時的に保存するために作ることがあります。このような式の途中の値はレジスタに保存することもありますが、レジスタに保存できない時はスタックを使用します。

a = (b + c) * (d – e) / (f & g);

スタック使用量の増減

以下に、親子関係の3つの関数と、コメントに書いた動作ポイントでのスタックフレームの増減の様子を示します。
コンパイラ作業域の部分にはレジスタ退避域や一時変数が含まれます。引数やオート変数はレジスタを使用することもありますが、アドレスを求めた時はスタックに保存します(レジスタのアドレスは求められません)。

コンパイラによってスタックフレームの構造は違うのですが、関数呼び出しによってスタック使用量が拡大してゆく様子が分かって頂けると思います。図のように関数を呼び出すと新たなスタックフレームが作られ、関数からのリターンでスタックフレームは消えます。

もし再帰呼出しを使うと、同じ関数のスタックフレームが積みあがってゆくことになります。その際、同じ関数がネスト状態で動くわけですが、オート変数と引数のエリアは異なるので、親関数のオート変数が、子関数に壊されることはありません。しかし、静的変数は同じエリアをアクセスするので親関数に影響を与えます。

子関数のリターン後、別の関数を呼び出した時には、先に動いた子関数のスタックフレームと同じ場所を使用するので、前回動いた関数のオート変数や引数などの値が残っています。このため、コーディングガイドラインには、オート変数は使用する前に必ず初期化せよと書いてあります。

セキュリティ対策等のため、リターン前にオート変数をクリアしたいことがあるかもしれませんが、コンパイラが無駄な処理と判断してクリア処理を削除する可能性もありますから、コンパイラに最適化させない工夫が必要です。

最後に…

Cコンパイラ説明の最初の回に、Cコンパイラを作れないマイコンもあるというお話をしました。

たとえば、関数呼び出しのネストに3回までの制限があるようなマイコン用のCコンパイラは作れません。3回までという仕様制限付きで作ることは可能かもしれませんが、おそらくC言語プログラマは許さないと思います。

また、関数呼び出し命令機械語の特殊な仕様が原因で制限付きになっているため、今回お話ししたスタックフレームが作れない問題もあります。

筆者紹介

浅野 昌尚(あさの まさなお)

ガイオ・テクノロジー株式会社

開発2部 QTXグループ

1980年代から30年以上にわたり汎用構造のCコンパイラ開発に従事し、その間に8ビットマイコンからRISC・VLIW・画像処理プロセッサまで、さまざまなCPU向けのクロスCコンパイラを開発。

人気のコラム

最新のコラム