
前回は、スタックメモリを使った関数コールの仕組みまでを説明したけど、覚えてるかな?
今回は、スタックメモリに関する注意点から説明をはじめよう。やっと組込み向けの内容になるんだ!
スタックオーバーフロー
マルチタスク環境では、複数のタスクが同時並行的に動作したよね。これはつまり、それぞれのタスクは別々の関数を独立して実行しているということなんだ。
このことを実現するために、スタックメモリはタスクごとに別々に割り当てられるんだ。ここで、ひとつ関数を考えてみよう。特定のファイルから配列のバッファにデータを読み出すというソースコードだよ。


C言語の書式としては正しいし、コンパイルもエラーにならない。きっと、実行してもちゃんと想定通りの動作結果になるはず。だけど、この関数にはシステム異常を引き起こす可能性が潜んでいるんだ。どこが問題かわかるかな?
そう、答は buf[ ] という配列の大きさなんだ。
char型で要素数が4096個だから、buf[4096]は4096バイト(4KB)のメモリを占有することになる。この関数を「スタックメモリを1KBしか持っていない」タスクAがコールしたら どうなるだろう。前回の内容を思い出してほしい。read_file()関数に入ったときに「Auto変数がスタックメモリに積まれる」んだったよね。
CPUはスタックのトップをスタックポインタ(SP)というレジスタに保持するだけで、スタックメモリがどれくらい確保されているのかは全く関知していない。buf[ ]のサイズ分を積んでしまうと、SPが指すアドレスは、当然、タスクAに割り当てられたスタックメモリの範囲をはるかに超えたところになってしまう。

さらに、変数sizeやopen(), read()等の関数で宣言されているAuto変数は、その後ろに積まれていくから、どんどんはみ出していくんだ。
ここで、buf[]にデータを読み込んだり、sizeに値を代入したりすると、「別の目的で使用されているメモリ」を壊してしまうことになってしまう。その結果、システムが正しく動作しなくなったり、最悪はプログラム例外を起こしてシステムがハングアップすることが予想される。この現象を「スタックオーバーフロー」というんだ。
スタックオーバーフローを起こさないためには
スタックオーバーフローが起こるかどうかは「タスクのスタックサイズ」に依存する。つまり、同じ関数でも「大きなスタックメモリを持つタスクB」で実行すれば表面化しないんだ。これも問題をやっかいにしている一因だね。
WindowsやLinuxなどの汎用OSでは、プロセス(スレッド)に割り当てられる標準のスタックサイズが大きい(1MBなど)から、実はスタックオーバーフローの問題はほとんど起こらないんだ。だけど、限られたメモリ資源で動作する組込みシステムでは、大きなスタックメモリを潤沢に割り当てることはできない。場合によっては、タスクの関数コールシーケンスを詳細に解析して、スタックメモリをギリギリまでチューニングしなければならないケースもあるくらいだよ。
だから、この問題は、WindowsやLinuxのシステムで動作していた プログラムを組込みシステムに移植したときに、表面化しやすいと言えるね。

スタックオーバーフローによって引き起こされる現象は、メモリのどこが壊されるかによって、単なる表示の文字化け程度から、例外発生によるシステム全体のハングアップまで、実に様々だ。
現象を見ただけでは、なかなかスタックオーバーフローが原因とは突き止められないものだよ。そういう意味でも、非常にやっかいな設計上のバグの一つといえるんだ。スタックオーバーフローを起こさないためには、プログラミング上の注意点があるから、それを説明しよう。
プログラミング上の注意
スタックオーバーフローを起こさないソースコードを書くときの注意点をあげてみよう。
- 大きなデータをAuto変数として宣言しない
- 再帰呼び出しを使わない
(1)大きなデータをAuto変数として宣言しない
前半で例にあげたように「大きな配列をAuto変数で宣言する」のは、直接的にスタックオーバーフローを引き起こす原因になるから、避けるべきなんだ。だけど、配列の要素数が少なければよいのかというとそうでもない。
例えば、long型配列の場合はchar型配列の4倍のメモリが必要になる。配列がどれくらいのメモリを占有するかを意識する必要があるということだね。このような「大きな配列」については、ソースコードを見れば一目瞭然だから、コードレビューを実施することで、事前に潰すことができるんだ。じゃあ、こんな場合はどうだろうか。

MY_DATAという構造体にパラメータを設定して送信するというイメージだよ。ここで、MY_PACKET構造体が以下の形なら、特に問題はないだろうね。

だけど、例えば下のようなUnicode文字列を含む構造体だったとしたらどうだろう?一般的なUnicodeは1文字が2バイトで表現されるから、unsigned shortの配列としてもっているよね。


結果として、この構造体のサイズは1KBを超えることになるんだ。このように構造体/共用体のサイズは、一見しただけではわからない。構造体/共用体をAuto変数として宣言する場合は、どれくらいのメモリを占有するのか内部構造も含めて頭に入れておく必要があるんだ。
一般に、大きなデータを扱う場合は、次のような方法でスタックメモリ以外の領域に実体が置かれるように設計する。
- malloc()等を使ってヒープメモリから確保する
- グローバル変数として宣言する
- Auto変数宣言にstatic修飾子をつける
「どこに確保されるのか?」については、また別の回で話すつもりだから、待っててね。コード例1をグローバル変数を使って修正したものは下のようになる。Auto変数は、それを指すポインタだけになるんだ。 ポインタ変数は32-bit CPUでは4バイトだから、スタックメモリを圧迫しない。

コード例2では実体であるg_pktが無くなることはないけど、実体を動的に確保した場合には、「ポインタが指している先のメモリがちゃんと存在するか?」などのチェックをする必要が出てくるんだ。
(2)再帰呼び出しを使わない

再帰呼び出しとは、関数の中から自分自身の関数を呼び出す構造をいう。これが、なぜ問題となるかは・・・もうわかるよね?
関数コールの再帰レベルが深くなるに従って、Auto変数がどんどんスタックに積み上がっていくためだね。Auto変数が8バイト分しか宣言されていない関数でも、再帰的に100回コールされたら、 少なくとも800バイト以上のスタックメモリを消費してしまうことになるんだ(極端な例だけど…)。
また、再帰呼び出しは、スタックの問題だけではなく、終了条件を一歩間違えるといつまでも関数を抜けてこない「無限ループ」に陥りやすいという性質もあるから、一般に組込みシステムでは「ご法度」とされているんだ。アルゴリズムの関係上、どうしても仕方ない場合を除いて、有限回数のループ処理として実装するべきだろうね。
スタックトレース
スタックメモリの情報は、デバッグ時にとても役に立つんだよ。スタックの中を解析すれば「関数コールのシーケンス」がわかるからなんだ。あるタスクが不具合で止まってしまったときに、
- どの関数のどのあたりを実行していたのか?
- どのようなパスを通ってその関数がコールされたのか?
- 引数の値は?
などが分かったら問題個所を特定しやすいよね。
組込み開発で実際のターゲットボード上のデバッグを行う「デバッガ」と呼ばれる機器には、プログラムの実行を止めたときに、タスクのスタックメモリの内容を関数名で表示させる機能を持ったものがあるんだ。このような機能は「スタックトレース」と呼ばれていて、特に不具合の解析時には大変有効なツールだよ。

『スタックってなあに?』はこれで終るけど、最初に話していた、「スタックがあふれて例外になっていたんですよ」「このタスクのスタックサイズはどれくらいにすればいいですか?」「スタックのトレースはとれないのか」・・・・・って話にもついていけるようになったよね!
次回は、ビリーへ質問してくれた内容に答えて、メモリのオーバーフローとアンダーフローについての説明をするね!!
-
- 第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レジスタについて