組み込みソフト開発101 ~ テストとデバッグ

テストの分類

テスト対象で分類すると、組み込みソフト開発で経験するテストは次の4種類だと思います(呼び方は会社やチームによって異なります)。

  • 単体テスト
  • プログラムテスト
  • 結合テスト
  • システムテスト

単体テスト

1つの関数を対象にしたテストです。ユニットテストとも呼ばれます。密接に関連する複数の関数をまとめてテストするケースもあり、そのときはモジュールテストと呼ばれます。いずれにしろ、1人のプログラマーに閉じた世界で行われることが多いです。

例えば自分が書いたmy_func()という関数をテストするには、my_func()を呼ぶためのコードが必要です。これをテストドライバと呼びます。また、my_func()の中で別の関数(これも自分が書いた関数で、仮にget_randとします)を呼んでいる場合、本物のget_rand()の代わりになるコードが必要です。これをテストスタブと呼びます。多くの場合、テストドライバもテストスタブも、テスト実施者が開発します。

ドライバとスタブ

テスト内容は、関数の機能だけでなく、関数のI/F仕様(引数とreturn値)や実装内容を意識して決めます。網羅性が重視され、様々な条件の元で、様々な引数を使って関数を呼び出します。また、この段階ではまだ、実機ではなくホスト環境上でテストを実施します。

プログラムテスト

1つのアプリ(例えば携帯電話のソフトならメールやブラウザ)や1つの機能ブロックを対象にしたテストです。1つのチームが担当する範囲くらいの規模になります。

単体テストのときと同じく、テスト対象を利用する部分(上位レイヤ側)と、テスト対象が利用する部分(下位レイヤ側)には、本物のコードではなく、テスト用に書いたコードを使います。

テスト内容は、上位・下位レイヤとのI/F仕様を意識して決めます。ここでも網羅性を重視し、異常系も含めた全仕様をカバーする必要があります。もしホスト環境上にシミュレータがあれば、実機ではなくシミュレータ上でテストを実施するケースが多いでしょう。

結合テスト

テスト対象はプログラムテストのときと同じですが、この段階では、上位レイヤと下位レイヤにも本物を使います。つまり、ここで初めて他チームのプログラムと結合することになります。よって結合テストでは、他チームとのI/F仕様の誤解が発覚することが多いです。

I/F仕様には、現実には起き得ない挙動まで含まれていることがあります。そういった挙動は、テストドライバやテストスタブを使わない結合テストではテストできません。よって結合テストのテスト内容は、プログラムテストよりも小規模になると思います。大事なのは、上位・下位レイヤに本物を使うところです。網羅性よりもリアル性が重視されます。

またこの段階では、基本的に実機でテストを実施します。

システムテスト

製品に搭載されるソフト全てを対象にしたテストです。結合テストでも本物のコードを使っていましたが、システムテストでは、周辺環境にも本物を使います(例えば携帯電話なら実網や実サービスを使う)。フィールドテストと呼ぶ場合もあるかもしれません。

またテスト内容も、本番環境を意識したものになります。例えば、複数のユースケースを組み合わせたり、連続使用するといったテストが増えます。

穴あけテスト

最後に、穴あけテストというのを紹介しておきましょう。初めて実機上で動かすときや、複数の部署や会社が作ったモジュール同志を初めて結合して動かすときのテストのことです。フェーズで言うと、結合テストやシステムテストの直前に位置します。何しろ初めてなので、結合テストやシステムテストを始める前に、超基本的なユースケースくらいは動くことを確認しておきたい、というわけです。なので、項目数は少なく、テスト仕様書も用意しません。

穴あけテストは、少数精鋭で行われます。ここでトラブった場合、なかなか原因が見つからないことが多く、全体工程のボトルネックになってしまうリスクも高いです。発想力や幅広い知識などが必要とされ、デバッグの腕の見せ所とも言えます。逆に、穴あけの経験を積めば、他の場面でのデバッグで苦労することは少なくなると思います。

「穴あけ」という呼び方は一般的なものではなく、ググってもほとんどヒットしませんが、私自身はこれまで2つの現場(2社)で耳にしたことがあります。由来は良く分かりませんが、感覚的にはトンネル工事と近いかもしれません。あるいは、ソフトウェア階層の最上位のレイヤから、最下位のレイヤまで関数コールが伝わる様子を、ドリルで穴を掘り進めて行くのとダブらせているのかも。テストが「通る」とか、テストを「通す」という表現とも相性が良いですね。

テスト・デバッグ環境

ICE

例えば、Windows用のソフトをVisual C++で開発する場合、ソースコードデバッガを使えば、ブレークポイントを設定したり、変数の値を参照/変更することができます。これをこのまま組み込みソフトに当てはめようとすると、組み込み機器上で動作するソースコードデバッガが必要になります。しかしこれは、メモリ容量的にも技術的にも現実的ではありません。そこで、ICE(アイス、In-Circuit Emulator)を使います。

ICEは、ブレッドボード(実機の筐体に組み込まれる前の基板)と組み合わせて使うデバッグ機器です。ICEにはブレッドボード上のCPUの動きをモニタしたりROM/RAMをエミュレートする機能があり、ユーザは、ICEを接続したホストマシン上で動くソースコードデバッガを使ってICEを制御することができます。

例えばプログラムのどこかにブレークポイントを設定すると、ICEはROM上の当該箇所の命令コードを特殊な命令コードに書き換えます。CPUがその命令コードを実行すると、CPUが一時停止したような状態になるのです。ユーザがブレークポイントを解除すると、命令コードも元のコードへ戻します。

フラッシュメモリとキャッシュ

フラッシュメモリ(flash memory)は、ROMの一種で、比較的簡単に書き換え可能なメモリです。NAND型とNOR型があります。現代の組み込み機器では、(頻繁に書き換える必要のない)プログラムをNORフラッシュに保存し、(頻繁に書き換える)ユーザ設定情報などNANDフラッシュへ保存することが多いようです。

フラッシュメモリには電源を切ってもデータが消えない(RAMなら消えてしまう)という利点がありますが、RAMに比べると読み書き速度が遅いのが難点です。そこで、フラッシュの内容の一部を一時的にRAM上に置いておき、できるだけフラッシュではなくRAMの読み書きで済ませることにより読み書き速度を上げる手法が良く使われます。この目的で使われるRAMをキャッシュと呼びます。

プログラムをデバッグしていると、書き換えたつもりのデータが、どういうわけか書き換わってない、という現象に遭遇することがあります。こういう場合は、キャッシュを疑ってみましょう。そのデータはフラッシュ上のデータなのかもしれません。フラッシュ上のデータを書き換えたつもりでも、実はキャッシュが書き換えられただけなのかも。その値は、しばらく待てば、そのうちフラッシュにも反映されるのですが、反映される前にブレッドボードをリセットしてしまったのかもしれません。

テストスタブ/テストドライバ

単体テストの項でも説明しましたが、プログラムをテストするときに、本物の上位レイヤと下位レイヤの代わりに使われるのがドライバとスタブです。

ドライバを使うと、テスト対象の関数を様々な条件で呼ぶことができます。例えば、わざと不正な引数を指定して呼んだり、テスト対象の関数が参照するグローバル変数の値を自由に変えたうえで呼んだり、続けざまに1000回繰り返して呼ぶこともできます。また、テスト対象の関数のreturn値や出力引数の値を記録して期待値と比較し、テスト結果を画面やファイルに出力することもできます。

スタブを実装するのは骨が折れるので、可能な範囲で本物を使っても良いでしょう。しかしスタブは、異常系のテストで力を発揮します。スタブの挙動は自由にプログラムできるので、わざとエラーコードをreturnさせたり、内部で時間をつぶしてスローな動作をエミュレートすることができます。また、呼ばれたときの時刻や引数をログに保存してデバッグに役立てることもできるでしょう。

自動化

テストは、1度やればOKというわけにはいきません。理想としては、プログラムを少しでも修正したら全てのテストをやり直すべきです。またバグを発見した場合は、何故それを事前にテストで発見できなかったのかを分析し、テスト項目を強化すべきでしょう(その上で、バグ改修後に再テストする)。

同じテストを何度も繰り返すことを考えると、テストは自動化しておきたいところです。実機を使う結合テストやシステムテストの場合、ユーザの操作や外部とのデータ通信などの要素が絡むので自動化は難しいのですが、単体テストやプログラムテストは比較的自動化しやすいはずです。全テスト項目を一気に実行できるようにするには、ドライバやスタブの開発に結構な労力がかかるのですが、長い目で見るとその苦労はきっと報われます。すでに動いているプログラムに手を加えるには勇気が必要なので、汚いコードを改善したくてもなかなか手が出せないところですが、自動テスト環境を持っていると比較的安心して手を加えることができます。 CUnitなど、テストを自動化するための関数ライブラリも一般に公開されているので活用すると良いでしょう(ライブラリが手を貸してくれるところは、ごくわずかですが)。

カバレッジ

関数が始まってからreturnするまでの経路をパスと呼びます。普通、関数には分岐やループといった制御文が使われているので、パスは1本だけではありません。ifやswitchの分岐ごとに枝分かれしますし、forやwhileのループが1回も回らないこともあれば、途中でbreakしてループから抜けることもあります。よって、1回呼んだだけで関数の全てのコードをテストするのは不可能です。分岐条件やループ条件を踏まえたうえで、何度も関数を呼びながら、いろんなパターンをテストする必要があります。

特に単体テストでは、全てのパスを少なくとも1回は通ることが求められます。テストにおけるパスの網羅率をカバレッジと呼びます。gcovなどのツールを使うと、テスト実行中のパスを分析してカバレッジを計測することができます。ただし、カバレッジは目安に過ぎません。カバレッジが100%でも、すべてのバグを発見できるわけではないのです。むしろ、カバレッジを100%にするだけなら簡単と考えた方が良いでしょう。逆に、カバレッジ100%未満の場合は間違いなくテスト不十分です。

リモートデバッガ

ICEで実現していることをソフトでやってしまおうというのがリモートデバッガの目的です。ICEのように、自力でCPUをモニタすることはできないので、その役目を担う小さなモジュールをターゲット(実機やブレッドボード)上で動かします。この小さなモジュールをスタブと呼びます(テストスタブの「スタブ」と同じ)。ターゲットとホストマシンをシリアルケーブルなどでつなぎ、ホストマシン上のソースコードデバッガを使ってスタブを制御します。

デバッグの手がかり

再現性があってデバッガで追えるバグなら、デバッグには苦労しません。プログラムの理解度が低ければ時間がかかるかもしれませんが、それでも、お手上げ状態にはならないでしょう。やっかいなのは、不定期に発生する(つまり再現手順の分からない)バグです。リセットがかかり、その場所が特定できない(毎回、場所が違う)とか、デバッガで追うと再現しないというケースも調べにくいですね。

こんなときはコンパイラのバグを疑いたくなるものですが、残念ながらコンパイラより自分のコードにバグがあることの方が多いで、何とかして糸口を見つけなければなりません。いくつかのアプローチを挙げます。

コアダンプを見る

リセットするケースでは、コアダンプが取れるかもしれません。コアダンプは、リセットする直前にCPUが出力するCPU状態のスナップショットです。リセット時のPCレジスタ値が分かれば、プログラムのどこでリセットしたのかが分かります。また、コアダンプの解析ツールが、リセット時のコールスタックを表示してくれるかもしれません。コールスタックを見れば、タスク(スレッド)が実行していた関数と、その呼び出し元関数を遡って調べることができます。

スタックオーバーフローを疑う

関数が別の関数を呼び、それがさらに別の関数を呼ぶ。このネストが深く続いていくと、どんどんスタックが消費され、やがてスタック領域を使い果たしてしまいます。「ハードウェア」にも書きましたが、スタック領域はRAMの最後尾に置かれた固定サイズの領域です。通常、CPUは、スタック領域を使い果たしても警告してくれません。そのまま続けると、スタック領域をはみ出して隣接する領域のデータを上書きしてしまいます。この現象をスタックオーバーフローと呼びます。

このようなデータ破壊は、すぐに表面化するとは限らないのでやっかいです。あとになってから、破壊されたデータを(そうとは知らずに)誰かが読んで使っているうちに動作がおかしくなるわけです。

スタックオーバーフローを検出する1つの方法は、スタック領域を適当なパターンで初期化しておくことです。プログラムをひとしきり動かしたあとでスタック領域を調べれば、スタックサイズが足りているかどうかが分かります。

スタック領域はタスク(スレッド)ごとに1つずつ用意されます。また最適なスタックサイズはタスクごとに異なります。実のところ、最適なスタックサイズを見積もることは非常に困難なので、なるべく大きめに設定する方が良いでしょう。

ライトブレークを使う

スタックオーバーフロー以外にもデータ破壊は起きます。例えば、配列サイズをオーバーして書いてしまったり、不正なポインタ経由で書いてしまったり、メモリを確保する側とデータを書く側とで構造体のアライメントが食い違っていた場合などです。

データ破壊が起きるアドレスが特定できている場合は、デバッガのライトブレーク(write break)機能を使ってみましょう。ある番地にデータが書き込まれたときにブレークをかけることができます。

ソースコードレビュー

最後の手段というか、あんまり気が進みませんが、ソースコードレビューも役に立つかもしれません。

また、そのバグが特定のバージョンでのみ起きるのであれば、バグが再現しないバージョンとの差異を調べてみるのも良いでしょう。

Last modified:2012/01/30 21:45:13
Keyword(s):
References:[組み込みソフト開発101] [組み込みソフト開発101 ~ C言語]
This page is frozen.