const外し
C言語は何でもできるプログラミング言語であるという説明を見かけることがあります。時にそこには「自分で自分の足を撃ち抜くことすらできる」という揶揄が含まれていることがあります。そんな何でもありのC言語の仕様の一つであるポインタのconst外しで不具合を招いた事例です。
組込みシステムに限ったことではありませんが、組込みシステムにおいてはハードウェアの制御パラメータやセンサーの変換テーブルなど多くの定数テーブルを扱います。この定数テーブルを変数として扱って不具合を引き起こした例です。
不具合発覚のきっかけはこうです。ポインタを介して変数を書き換えたにもかかわらず、実機で動作確認するとファームウェアの挙動が変化しない、という現象でした。
何が起こっていたかというとROM(フラッシュメモリ)に配置したアドレス領域をポインタを介して書き換えようとして失敗していました。
typedef struct TAG_DEVICE_SPEC {
unsigned char device_type;
unsigned char coff_a;
unsigned char coff_b;
} DeviceSpec;
const DeviceSpec device_spec_table[3] = {
{ 0, 0x2a, 0x51},
{ 1, 0x7f, 0x02},
{ 2, 0xc6, 0x02}
};
int drive_device(DeviceSpec* drive_spec) {
/* 中略 */
device_spec->coff_a = 0x00; /* ROMエリアの書き換え */
device_spec->coff_b = 0x00; /* ROMエリアの書き換え */
/* 中略 */
}
/* 中略 */
/* ポインタからconst修飾を外す不適切なキャスト */
result = drive_device((DeviceSpec*)(&device_spec_table[device_id]));
グローバル・スコープにて定義したconst変数(含、配列)をメモリのどこに配置するかはリンカ設定に依存します。今回はROMアドレスに割り付けて、RAMへは展開しない設定になっていました。ソースコースを穴が開くほど眺めても原因が掴めなかったのですが、ハードウェアデバッガ(JTAG-ICE)で変数アクセスを追跡することではじめて原因に思い至りました。
論理的に考えれば定数テーブルを途中で書き換えていることに気が付くべきだったのですが、実際のソースコードでは構造体の一部の要素のポインタを関数呼び出しの間で引きずり回していたため気が付くのに時間がかかりました。
なぜconstを外すキャストを記述したのかと担当者に尋ねたところ、「コンパイラのワーニングを解消するため」という衝撃的な答えが返ってきました。本来コンパイラのワーニングは、不適切な記述をしていることに気づかせてくれるためにあるはずなのですが、件の担当者はワーニングの件数を減らすことが目的化して、自分で自分の足を打ち抜く真似をしていました。
constを外したポインタキャストはしばらくの間、問題を引き起こさなかったのですが、機能拡張、機能追加で、関数の間をポインタで引きずり回した結果、別の担当者がポインタが指す先を不用意に書き換えようとしてトラップにハマりました。
適切なアドレスをポインタで指し示しさえすれば、memory-mapped レジスタであろうと、リアルタイムOSの管理領域であろうと、自由自在にアクセスできるC言語の仕様は一見便利に思えますが、いざとなれば、自分でconst修飾してRead-Onlyにしたつもりの定数を裏で書き換えることができる扱いの難しいプログラミング言語であることを改めて思い知らされました。
この記事が気に入ったらサポートをしてみませんか?