「昨日、風邪引いて会社休んじゃったから、仕事がスタックしちゃって」というような会話を聞いたことがあるんじゃないかな。「スタック」は「積み重ねる」という意味の単語だから、わりとイメージしやすいよね。でも、組込み業界では以下のような使い方をするんだ。
「スタックがあふれて例外になっていたんですよ」「このタスクのスタックサイズはどれくらいにすればいいですか?」「スタックのトレースはとれないのか」
・・・なんだか雰囲気が違うよね。(^-^;これらはどういうことを意味しているんだろうか。
スタックのおさらい
大学などで情報工学を勉強したことのある人なら、「スタック」といえば「アルゴリズムとデータ構造」の授業を思い出すんじゃないかな?「スタック」は、入れる口と取り出す口が等しい袋のようなデータ構造なんだ。以下に、スタック操作の例を示してみよう。
[ がスタックの底を示しているんだけど、右方向に積みあがっていく様子がわかるかな?スタックに要素を追加することをPush、一番上の要素を取り出すことをPopというんだ。また、最後に入った要素が最初に取り出されるので、スタックはLIFO(Last-In, First-Out)とも呼ばれる。
ちなみに、スタックと並んで教えてもらうデータ構造に「キュー」があり、これは、入れる口と取り出す口が異なる筒みたいなものだ。こちらは最初に入った要素が最初に取り出されるので、FIFO(First-In, First-Out)ということになる。
逆ポーランド記法
スタックの例として、本などでは、逆ポーランド記法がよく取り上げられる。逆ポーランド記法とはカッコがいらない計算表記法で、例えば、
5 3 +
と書くと、5+3の意味になるんだ。
((5+3)*2-4)/3=
というような加減乗除が混ざった式は、通常は演算順序を指定するためにカッコが必要だよね。でも、逆ポーランド記法を使うと、
5 3 + 2 * 4 - 3 / =
という風にカッコを使わずに表現できるんだ。演算子を後ろに書くのがポイントで「後置記法」とも呼ばれている。この計算方法をプログラムで実現するのに「スタック」の考え方が使えるんだ。つまり、
- 数値だったらスタックにPushする
- +、- 、× 、÷ の演算子がきたら2回Popする
- 演算したら答えをスタックにPushする
- =がきたら1回Popして答えとする
という手順(こういうのをアルゴリズムという)を繰り返すと、計算結果が得られるというものなんだ。わかるかな?上記の例((5+3)*2-4)/3=の場合を、順番に見てみよう。逆ポーランド記法では、5 3 + 2 * 4 - 3 / = となる。
で、答え4が得られることになる。
では、これがプログラムの世界でどのように利用されているかを説明していこう。これもまだ一般的な話なんだけど、もうちょっと我慢してしてね。
関数とスタック
C言語のプログラムは、関数の集まりで構成されているのは知っているよね。例えば、以下のようなmain(), funcA(), funcB()で構成されるプログラムを考えてみよう。
例だから、各関数の内容自体はあまり気にしないでほしい。
実行結果は、以下のようになる。
当たり前じゃないか、と思うかもしれないけど、このプログラムを実行するのにスタックが重要な役割を果たしているんだ。
関数を呼ぶためには
このプログラムの関数呼び出しシーケンスは、以下のようになるんだ。どんどん入れ子になっていくよね。
CPUは、指定したプログラムコードがある場所(アドレス)にジャンプする命令を持っている。funcA()からfuncB()を呼び出すときには、funcB()のプログラムコードがあるアドレスにジャンプする命令を実行すればいいってわけだ。これはイメージしやすいよね。だけど、ただジャンプするだけでは問題が出てしまうんだ。
funcA()では、変数a1に100を代入してからfuncB()を呼び出す。funcB()での処理が終わって、funcA()に戻ってきたときにも、もちろんa1=100であってほしいわけだから、これはどこかに覚えておかないといけないよね。さらに、どこにジャンプすればfuncB()からfuncA()に戻れるのか?ということも覚えておかないといけないんだ。
ただし、funcB()を抜けたら、変数b1,b2の値はもう覚えておく必要はなくなる。これらが有効なのはfuncB()の中だけだから。これをスタック構造を使って解決していているんだ。実際にどうやっているのかを説明していこう!
スタックメモリ
通常、プログラムの実行単位(タスク、スレッド)ごとにスタックメモリという特別なメモリ領域が割り当てられているんだ。スタックメモリとは、そのタスクや関数内だけで使われる変数やアドレス情報なんかを置いておくためのメモリ領域だ。タスクを終了する、または関数から抜けると、スタックの内容も破棄される。
ちなみに、グローバル変数は、他のタスクや関数からもアクセスできるようにスタックメモリとは別の領域に配置されているんだ。この中にスタックを構築するんだけど、CPUは、その一番上のアドレスをスタックポインタ(SP)レジスタに保持している。pushしたり、popしたりすると、SPレジスタが移動するわけなんだ。
あと、多くのシステムではフレームポインタ(FP)レジスタというものが用意されているんだけど、SPレジスタやFPレジスタの具体的な使い方をみていこう。funcA()からfuncB()を呼び出すところに着目して説明するね。
関数を呼び出すとき
CPUは、funcA()の処理に入ってくると、変数a1があるから、これをスタックにpushする。厳密にはa1の値が格納されているメモリのアドレスになるんだけど、ここでは簡単にして、単にa1としておくね。
SP=1 FP=0 [a1
次に、funcB()を呼び出すわけだけど、ここでfuncB()から戻ってきたときのアドレス(リターンアドレスという)をスタックにpushする。リターンアドレスは、funcB()のすぐ後ろの行だと思ってもらえばいいかな。funcB()が戻ってきたら実行されるべきコードということになるよね。
ここでは仮にretBとしよう。
SP=2 FP=0 [a1 retB
次に、FPレジスタを更新する。まず、現在のFPレジスタの値をスタックにpushして、現在のSPレジスタの値をFPレジスタにコピーする。
SP=3 FP=3 [a1 retB 0
これでfuncB()を呼び出す準備ができたんだ。このようなスタックの状態で、funcB()のアドレスにジャンプする。funcB()では、さらに変数b1,b2が宣言されているから、これらもスタックに積まれる。
SP=5 FP=3 [a1 retB 0 b1 b2
呼び出し元の関数に戻るとき
funcB()でログ表示が終わると、ここから呼び出し元のfuncA()へ戻るんだ。 さて、どうやって戻ろうか?
そうだね。 スタックにpushしておいたretBを取り出してジャンプすればよさそうだね。だけど、ここで問題が出てくるんだ。 retBを取り出すためには何回スタックをpopすればいいんだろう? funcB()で宣言する変数の数は、いつも2個とは限らないから、retBを取り出すまでのpop回数は分からないということになるよね。
ここでFPレジスタの登場なんだ! このときのために、funcB()にジャンプする前にSPレジスタの値を保存しておいたんだよ。FPレジスタに保存しておいた値をSPレジスタに設定するとどうなるんだろう。
SP=3 FP=3 [a1 retB 0
スタックのトップがまき戻ったイメージだね。
この時点でb1, b2はスタックの管理外になってしまうことに注意しないといけない。ここでpopすると、0が取り出せる。 これはfuncA()におけるFPレジスタの値だったから、FPレジスタに書き戻そう。
SP=2 FP=0 [a1 retB
もう1回popするとやっとretBが得られるんだ。
SP=1 FP=0 [a1
これでfunA()を処理していたときの状態に戻せたよね。 あとはretAにジャンプすれば完了だ。
ちょっとややこしかったかな?ポイントは、C言語のような関数ベースのプログラムの動作には、スタック構造が重要な役割を果たしているということなんだ。ちなみに、ここでは関数内で宣言されたAuto変数だけをスタックに積んだけど、実際には関数に渡された引数も積まれることがあるんだ。次回は、スタックメモリに関して注意すべき点を説明するよ。
さて、ここでちょっとブレイクタイム。
「アルゴリズムとデータ構造」の書籍を紹介してと言う、質問をもらったので、ビリーのオススメを紹介しよう。質問を送ってくれた人、どうもありがとう!
『Cによるアルゴリズムとデータ構造』
関西学院大学教授/工学博士 茨木俊秀 著・オーム社 発行
この本は、アルゴリズムの基本概念をしっかりと解説してくれている入門書だよ。
-
- 第1回 組込みシステムのこれから
- 第2回 IoTの成功はセキュリティ次第
- 第3回 組込みでもGPUやFPGAと早めに親しんでおこう
- 第4回 電子産業の紅白歌合戦、CEATECで垣間見えた未来
- 第5回 小口開発案件の集合市場、IoTの歩き方(上)
- 第6回 小口開発案件の集合市場、IoTの歩き方(下)
- 第7回 徹底予習:AI時代の組込みシステム開発のお仕事
- 第8回 いまどきのセンサー(上):ありのままの状態を知る
- 第9回 いまどきのセンサー(下):データを賢く取捨選択する
- 第10回 組込みブロックチェーンの衝撃(上)
- 第11回 組込みブロックチェーンの衝撃(下)
- 第12回 エネルギーハーベスティングの使い所、使い方
- 第13回 「人を育てる」から「道具を育てる」へ、農業から学ぶAI有効活用法
- 第14回 CPS時代に組込みシステム開発に求められることとは
- 第15回 次世代車のE/Eアーキテクチャに見る組込みの進む道
- 第16回 RISC-Vが拓く専用プロセッサの時代
- 第17回 振動計測の大進化で、熟練エンジニアのスキルを広く身近に
-
- 零の巻:組込みというお仕事
- 壱の巻:2進数と16進数を覚えよう!
- 弐の巻:割り込みとポーリング
- 参の巻:printf()が使えない?
- 四の巻:これにもIntelが入ってるの?
- 五の巻:Endianってなに?
- 六の巻:マルチタスクとは
- 七の巻:スタックってなあに?(1)
- 七の巻:スタックってなあに?(2)
- 八の巻:メモリを壊してみましょう
- 九の巻:コードが消える?~最適化の罠~
- 拾の巻:例外が発生しました
- 拾壱の巻:コードサイズを聞かれたら
- 拾弐の巻:キャッシュは諸刃の剣
- 拾参の巻:デバイスにアクセスするには
- 拾四の巻:セキュリティってなに?(1)
- 拾四の巻:セキュリティってなに?(2)
- 拾四の巻:セキュリティってなに?(3)
- 拾五の巻 :DMA対応と言われたら(1)
- 拾五の巻 :DMA対応と言われたら(2)
- 拾六の巻:ヒープとスタック
- 拾七の巻:フラグメンテーション
- 拾八の巻:CPU起動とブートローダ
- 拾九の巻:kmとKByteの「kとK」
- ビリーへの質問:DMAとキャッシュの関係
- ビリーへの質問:スタックオーバーフローについて
- ビリーへの質問:CPUレジスタについて