メモリ破壊の典型パターンと抑止方法

メモリを破壊してしまうプログラムはコード上ではエラーを起こさないため、コンパイル時に発見することは不可能です。しかし、いざ実行した際にエラーとなり、最悪の場合にはシステム全体に影響し機器が動作しなくなる事態を招きます。メモリ破壊につながる原因もさまざまなため、機器に実装してしまってから原因を特定し、改善するのは非常に難しくなっています。

そのため、設計、構築時からメモリ破壊を起こさないように注意が必要です。本記事では、メモリ破壊につながってしまう典型的なパターンと破壊リスクのつぶし方を列挙します。

バッファオーバーフロー

プログラム実行時に、一時的にデータを保存するメモリストレージ領域がバッファです。使える領域はプログラムごとに定義しますが、バッファ容量を超えたデータが送られてくると隣のメモリに上書きすることによって容量を補います。宣言した配列サイズをオーバーしてしまい、結果的に関係のない領域のデータを書き換えてしまうなど、データの不正を招く原因になります。

この無関係なデータに上書きしてしまう状態はバッファオーバーフローと呼ばれ、悪用されるとソフトウェアの破壊が行われるおそれがあります。バッファからデータを溢れさせることにより、隣接するメモリへ攻撃に使用可能なコードを上書きすることができるようになり、そのコードを利用することによってデータの改ざんやプログラムの破壊を起こすという、サイバー攻撃の一種です。

メモリの初期化と解放後アクセス

プログラムで使う変数は、意図しない動作を防ぐために初期化してから用いるのが一般的です。コード上で初期化されていない変数を使うことにより、不正領域へのアクセスになる可能性があります。たとえば、初期化されていない変数は、その時点でメモリに保存されている値を読み込んでしまいます。この変数が実行時のカウンタになっていると、無限ループやバッファオーバーフローの原因になりかねません。

また、メモリの解放後にそのメモリを指すアクセスポインタを保持し続けるケースも同様です。一見有効なデータが入っているように見えますが、このデータは次にいつ変更されるか分からない未定義の値です。それゆえ任意のコードを実行可能な状態を作り出すこともあり、不正使用されるリスクが高まります。

同一メモリへの同時アクセス

マルチスレッドの場合、同じカウンタ変数を別のスレッドが同じタイミングで更新するときなどに競合(同時アクセス)が発生します。たとえば、同じ処理をする別々のスレッドで、一つずつカウントアップするカウンタ変数を使用している場合、同時に2つのスレッドが値を更新してしまったら一つ多くカウントアップしてしまいます。結果としてデータが飛んだ不正な処理になります。

マルチスレッドを利用する際には、メモリにおける排他処理の設計が重要です。データの更新時にはロックを取得し、処理の実行後にロックを解放することによって、二重更新の可能性を防ぎます。ただし、排他処理であっても同期に失敗し、ロックを解放しないまま処理を終了してしまうと、次のロックを取得できずに処理はそこで止まります。排他処理はソフトウェア開発において非常に重要な処理になりますが、特に組み込みシステムでは、パフォーマンスへの影響も大きくなります。この観点は設計時に非常に重要になります。

どれもシングルスレッドで動いているときには問題が起きないため、コードから発見するのは難しいエラーです。

メモリ破壊に対するガード機能不足

プログラミングの段階で、メモリをガードする機能が不足していると、メモリ破壊が発生しやすくなります。データ長が長すぎて桁あふれを起こさないように、転送バイト数の上限を設定するだけでもバッファオーバーフローは防げます。ループカウンタの値に気を付け、インクリメントしたときに領域外に書き込まないなど、コーディングのレベルでメモリ破壊を防ぐ意識が必要です。

メモリを破壊するのは内部のプログラムだけではありません。外部からの悪意のある攻撃にさらされることもあります。書き込み用のアドレス空間をランダムに再配置すると、重要な実行コードの位置が分からなくなるため、破壊されるリスクが下がります。そもそもメモリの特定領域を非実行領域としてマーキングし、その領域で発見されたコードは実行しないような処理にすることも可能です。

近年では組み込み機器もネットワークに接続されることが非常に多くなりました。サイバーセキュリティに対する対策も法的に必要になってきています。近い将来、外部からの攻撃によるメモリ破壊を十分に防ぐような設計も必須となっていくかもしれません。

ログの有効性

メモリ破壊も含めて、システム内で何が起きているか分からないと問題が長期化する傾向にあります。特にメモリ破壊に関してはエラーが発生している箇所と、エラーを検出している箇所が離れている場合が多く、適切にログを出力していないと原因を究明することは非常に困難です。原因究明ができなければ再発防止策を打つこともできず、同じ不具合を繰り返すことになりかねません。誰が読むか、どのようなメッセージがあれば対応できるかを想定してログを設計することが重要です。

また、不具合が起きる前から予測して、ログを仕込んでおくような工夫も、開発納期を守るためには重要になります。

致命的なエラーが発生した場合だけではなく、メモリの確保、メモリの解放、異常の検出など状態が変わったときには最小限でもログを残していくことを意識すると、ログから処理を追跡しやすくなりエラー解析に役立ちます。

デメリットとして、ログを動作させること自体は、システムのパフォ-マンスを下げることにもつながります。トレードオフを考えつつ、不具合が起きたときに困らないよう、準備をするようにしましょう。

組み込みソフトの世界 トップへ戻る