
前回の連載中に「スタックアンダーフローというのは何ですか?」という質問をもらったので、今回はそれから説明していくね。
スタックアンダーフロー
前回の「スタックってなあに?(2)」で説明したように、スタックオーバーフローとは、スタックメモリのサイズを超えてpushしてしまうことだったよね。これに対しスタックアンダーフローは、
スタックが空の状態で、さらにpopしようとした。
場合のことを指すことになる。
通常、C言語などの関数呼び出しに使われているスタック構造では、こういった状況は起こらないはずなんだ。なぜなら、「スタックが空」というのは「タスクが関数を何も呼びだしていない状態」であり、これをさらにPopさせる手段がないからなんだ。もし起こるとすれば、言語処理系(コンパイラなど)に問題がある可能性が考えられる。
ただし、アセンブラ言語で書かれたプログラムなら起こりえるんだ。これは言語として関数呼び出しの構造を持っていないため、スタックのpush/pop制御をプログラマが意識して行う必要があるからなんだ。アセンブラ言語については、またの機会で詳しく取り上げる予定だよ。
バッファ・オーバーフロー
「セキュリティホールが見つかりました」というニュースを耳にしたことがあると思うんだけど、知っているかな。セキュリティホールとは、悪用される可能性のある「ソフトウエアの穴」が空いていることを言うんだ。その中でも最も多いのが「バッファ・オーバーフロー」という類のもの。
バッファ・オーバーフローが発生すると、任意のコードを実行される恐れがあって、非常に危険な脆弱性とされているんだ。これはどういうものなのかわかるかな。組込みにも非常に重要だから、簡単なコード例で説明するね。
コード例(1)

以下のソースコード(overflow1.c)を見てみよう。

このソースコードをLinuxでコンパイルして実行した結果が下のようになるよ。$が、bashのプロンプトを示している。

ソースコード中では配列a[]に対しては何も操作していないにもかかわらず、内容が書き換わってしまっているよね。どうして、こんなことが起こったのだろうか?
原因は、配列b[]に対するstrcpy()なんだ。b[]は8バイト分の領域として宣言されているけど、strcpy()で「それを超える」16バイト分(15文字+終端文字'\0')の書き込みをしているためにa[]の内容が侵害されてしまったんだ。
ここで新たな疑問が浮かんでくるね。配列サイズを超えて書き込むのが良くないのはわかるけど、なぜb[]の領域を超えて書き込みをしたら、a[]が壊れてしまうのだろうか?
スタック領域

それは、a[], b[]がどちらもスタック領域に配置されているためなんだ。関数の内部で宣言する自動(Auto)変数は、スタック領域と呼ばれるメモリ上に確保されるのは覚えているよね。スタック領域では、その名前が示すとおり、LIFO(Last In First Out)構造で、上に上に(アドレスが減る方向に)積みあがるように変数を配置していく。このため、上記の例では、以下のように先に宣言されたa[]の上にb[]が積まれる形になるんだ。

ここで、b[]に"00000000abcdefg"の16バイトを書き込むとどうなるだろうか。b[]の後ろには連続してa[]の領域があるから、溢れてはみ出した"abcdefg"がa[]のデータを上書きしてしまうわけなんだ。

コード例(2)
これを応用して、もうひとつソースコード例を見てみよう。

今度は、配列cmd[]に設定されたlsコマンドを実行するというプログラムだよ。
コマンドライン引数として文字列が渡された場合は、配列tmp[]にコピーしている。配列cmd[]に対しては、何も操作をしていない。Linuxでコンパイル・実行した結果は、下のようになるよ。引数がない、あるいは8文字未満の場合は、lsコマンドが実行される。ところが、引数に「任意の8文字+任意のコマンド」を渡すと、任意のコマンドを実行することができてしまうんだ。

どこに問題があるかはもうわかるよね。どちらも「配列サイズを考慮せずにstrcpy()している」のが不具合の原因なんだ。strcpy()という関数の内部では、配列サイズのチェックは行ってくれないから、使う側で注意してあげないといけない。
メモリ破壊のバグは危険!
バッファ・オーバーフローの仕組みはもうわかったかな?前述の2例は、バッファ・オーバーフローにより別の配列データを壊すというものだったけど、例えばb[-1]のように負のインデックス番号を与えればバッファ・アンダーフローも簡単に起こすことができる。これらにより、実際にどんなトラブルが引き起こされるかは「壊した部分に何があったか」によって異なるんだ。
Windows, Linuxなどの汎用OSは、メモリ保護機能を持っていて、ユーザ・アプリケーションがOSカーネルを使っているメモリ領域を簡単にアクセス(書き換え)できないようになっているんだ。アプリケーションのプロセスでメモリ破壊を起こしても、たいていの場合は例外メッセージ(Segmentation Faultなど)を表示してアプリケーションが終了する程度の被害で済むんだ。
この場合、OSカーネルとしては生きているから、システム全体が停止するまでにはならない。でも、組込み機器で使われるOSでは、メモリ保護機能を持たないものが多く、これらのOSの場合は、アプリケーションからOSカーネルのメモリ領域をいとも簡単に壊すことができてしまう。そうすると、システム全体がいとも簡単に吹き飛んでしまうから、ユーザからみると突然、機器が固まってしまうことになるよね。組込み機器ではあってはならないことだ。
メモリ破壊によるトラブルは、組込み技術者が一度は必ずハマるといっても過言ではないくらい身近なものなんだ。でも、バッファ・オーバーフローのような配列の範囲を超える書き込みは、コンパイル時にはエラーどころか警告もでないため、気づかないことが多い。

また、組込み機器が動作中に、メモリのどの部分がいつ・どのように壊れたのかを特定して改善をするのは「至難の業」で、調査・デバッグに大変な時間と労力を費やすことになってしまう。そういう意味では、最も危険なバグといえるだろうね。
組込み向けのソフトウエアは、このようなメモリ関連のバグを埋め込まないよう、特に意識して設計・コーディングされているんだよ。
-
- 第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レジスタについて