コンテンツにスキップ

エキスパート

ハードウェア テスト

このレッスンでは、 ハードウェア依存関係があるソフトウェアをテストするテスト ドライバーの作成方法を、順を追って説明します。


学習時間の目安: 10 分

このレッスンを終了すると、以下のことができるようになります。

  1. ハードウェアがソフトウェアと通信する可能性があるケースを説明する。
  2. ハードウェアに依存するソフトウェアをテストするための戦略を検討する。
  3. サンプル ハードウェア テスト ドライバーを順を追って説明する。

ソフトウェアがハードウェアと出会うとき

先に、プログラムは入力を読み取り、出力を生成するものであると大雑把に定義しました。入力と出力のどちらか、または両方がハードウェアによって処理されることがよくあります。

ソフトウェアは以下の例のようにセンサーから入力を読み取る場合があります。

  1. 温度センサーから読み取りを行うデジタル温度計
  2. ジャイロスコープから読み取りを行う Fitbit
  3. iPhone の GPS から読み取りを行う Pokémon GO
  4. LIDAR から読み取りを行う自動車のバックアラーム システム
  5. AIS トランスポンダーから読み取りを行う船舶に搭載された船舶追跡システム
  6. 高度計から読み取りを行う航空機に搭載された高度警報システム

ソフトウェアは以下のような装置や周辺機器から入力を読み取る場合もあります。

  1. 一般的な USB デバイス

  2. 接触: キーボード、マウス、ジョイパッド

  3. 音声: マイクロフォン
  4. 動画: ウェブカメラ

  5. 車両または航空機のハンドル

ソフトウェアがハードウェアに出力を送信する場合もあります。

  1. 画面上のグラフィック表示
  2. 自動車の車線逸脱防止支援システム
  3. 無人航空機の自動操縦
  4. パワー グリッドの電力スパイク防止
  5. 水処理施設での水圧維持
  6. 製造工場でのロボット アームの制御

これらのシステムはクリティカルかつ/または高可用性システムであることが多いため、システムがクラッシュしないことを保証する十分なテスト ケースを持つことが重要です。これらのケースはすべて、ソフトウェアをテストしようとすると、ハードウェア インターフェイスをエミュレートまたは置換する必要があります。そうすると、Mayhem でソフトウェアを解析できるようになります。

テスト ドライバーの戦略

テスト ドライバーを作成する際、Mayhem (および他のソフトウェア テスト ツール) はテスト対象ソフトウェアに単一の入力があると仮定します。そのため、テストを実行するにあたって、ハードウェア データを単一の入力ファイルまたはネットワーク接続として表現する方法を見つけることがゴールになります。ソフトウェアが実際にハードウェアと「会話」する方法によって、ゴールの達成は容易である場合も困難である場合もあります。

ハードウェアとソフトウェアが「会話」する主な方法はいくつかあります。

  1. 物理的接続を通じて (JTAG、serial、USB、802.11x など)
  2. ダイレクト メモリ アクセス (DMA) を通じて

これらは直接リンクである場合もあります。たとえば、ある種の組み込みプラットフォーム (Nintendo Entertainment System など) では、RAM の特定のメモリ位置に書き込むと、実際には自動的に APU または GPU への DMA が行われます。しかし、現在の多くのシステムでは、ハードウェアとソフトウェアのリンクはカーネル自体によって管理されます。カーネルはデバイス固有の詳細を抽象化したり、適切なリソース管理を保証したりするためのインターフェイスをユーザーランド アプリケーションに提供します。

Mayhem のソフトウェア解析は、ハードウェアやカーネルのインターフェイスにはアクセスできない隔離された環境で実行されます。それでも、すでに説明したとおり、ハードウェアをエミュレートしたり、ハードウェアの機能を類似物で代替したりすることで、ソフトウェアをテストできます。どちらの場合でも、テスト ドライバーを作成するには、通常、デバイスのデータを実際にキャプチャしたものが必要です。

ダイレクト メモリ アクセスを処理する

DMA は、テスト用にアドレス領域の内部表現を作成するだけで、簡単に対処できる場合もあります。残念ながら、通常の操作で起こるアクセス対象メモリ領域への変更をエミュレートする必要があるために、非常に対処が困難な場合もあります。結果として、テストを実施するために必要な変更は、他の問題に対処する場合に比べて大がかりではないことが多いが、より複雑になりがちです。

DMA をエミュレートする場合、読み取り/書き込みが通常どおり動作できるよう、メモリ領域をプロセス内にマップできる場合があります。たとえば、アドレス 0x005c0000-0x005fffff に DMA バッファーがある場合、実際の DM 領域を初期化する代わりに、(MAP_FIXED を使用して) メモリ領域をそこに mmap できる場合があります。デバイスの動作時にメモリ領域にあるべきデータを投入することも重要です。どちらの処理も、プログラム実行のできるだけ早期、DMA 領域へのアクセスが行われる前に行うべきです。

状態を示すためにデバイス自体がメモリの特定アドレスを変更する可能性があるため、別個の関数を用意してそれらの変更も行う必要があります。このロジックを追加するべき場所を特定するのは難しい場合もあります。一般的に、修正を行うのに適した場所は、イベント ループの先頭 (DMA 領域へのアクセスが行われる前) またはイベント ループの末尾 (新しいイベントを生成し、次のアクセスに備えて DMA 領域をセットアップするため) です。

API 呼び出しを処理する

Linux での ioctl や Windows での DeviceIoControl などの API 呼び出しは、多くの場合、一連の関数に置き換えることが可能です。ioctl の結果は、ある程度固定されている場合があります (たとえばデバイス名の取得など)。テスト (Mayhem など) が制御できる入力ファイルから読み取りを行う必要がある場合もあります。さらには、ハードウェアのロジックをエミュレートし、入力ファイル、データ ファイル、ハードコーディングされた値などを使用して期待されるレスポンスを返す必要がある場合もあるでしょう。

シリアルまたは USB データの読み取り/書き込みを処理する API への呼び出しは、通常は比較的容易にテストできます。その理由は主に、そういった API はすでにファイルまたはネットワーク ソケットの読み取り/書き込みという形を取っていることが一般的だからです。そのため、API 呼び出しを実際のファイル/ネットワークの読み取り/書き込みに変えるのが最良の戦略です。難しいのは、タイミング要件 (ボーレートなど) を課す必要があるかどうかを判断し、必要がある場合はどのように要件を課すかです。

入力テスト ケースとして Mayhem に与えることができるよう、ハードウェアからキャプチャした実際のデータを用意することは、テスト ドライバーに期待通りの動作をさせるためだけでなく、Mayhem が有用な解析をすばやく行うことができるようにするためにも非常に重要です。API 呼び出しを回避するために加えた変更によっては、キャプチャされたデータに追加、削除、変更が必要な場合もあることに注意してください。

他の問題を処理する

テスト時に他の問題が発生する場合もあります。たとえば、ハードウェアとやり取りするシステムの多くは、デバイスの状態やデータのタイミングに依存します。これらの問題にどのように対処するかについて、一般的なガイドを提供するのは困難ですが、おそらく「入力ストリームにコード化できないか?」という観点から始めるのがよいでしょう。

たとえば、ソフトウェアが特定のメッセージを受信するまでに 2 または 3 「タイム ステップ」の間待機する場合、「タイム ステップが経過した」ことを示す新しいメッセージ タイプを作成できないでしょうか? 場合によっては、このメッセージに明示的な相対的タイムスタンプも含めることができれば、いっそう好都合です。デバイスの状態変化についても同じことが言えます。状態遷移をコード化するメッセージを「送信」または「受信」することは可能でしょうか?

もし可能なら、Mayhem は入力を変化させて、さまざまな予期された、または予期されていない状態遷移を探索できるでしょう。こうすることで、(手作業で作成した単体テストでは一般的であるように) 予期される少数の状態だけをテストするのではなく、可能性があるすべての状態をじゅうぶんにカバーできます。

サンプル テスト ドライバー

ファイル: hid-example.c

Tip

ファイルは Linux カーネル ソース ツリー samples/hidraw/hid-example.c にもあります。

以下では、Linux カーネル チームのサンプル HIDRAW デバイスのコードをテスト ドライバーを使用してテストする手順を説明します。テスト ドライバーを作成する過程で、このファイルの一部を再現し、変更します。

入力ソースを特定する

最初のステップは、入力ソースは何かを判断することです。このサンプルでは、入力のソースは次の 1 つだけです: /dev/hidraw0これは行 53 で開かれています。

53
54
55
56
57
58
fd = open("/dev/hidraw0", O_RDWR|O_NONBLOCK);

if (fd < 0) {
    perror("Unable to open device");
    return 1;
}

ここで、2 つの変更を行う必要があります。

  1. 入力デバイス /dev/hidraw0 を入力ファイルに入れ替えます。そうすると、解析時に Mayhem からのシンボリック入力をプログラムに入力できるようになります。
  2. Mayhem のようなソフトウェア テスト システムは、テスト実行中に入力が変更されないことを前提としているケースが多くあります。このプログラムはデバイスに書き込みも行うため、出力用に別のファイル記述子を開きます。これはテストに必須というわけではありませんが、一般的に、これら 2 つを分離するのはよいプラクティスです。なぜなら、後で問題が見つかったときに開発者がデバッグするのが容易になるからです。

修正方法の 1 例は次のとおりです。

53
54
55
56
57
58
59
60
61
62
63
64
65
fd = open(argv[1], O_RDONLY);

if (fd < 0) {
    perror("Unable to open input file");
    return 1;
}

out = open("output", O_WRONLY);

if (out < 0) {
    perror("Unable to open output file");
    return 1;
}

こうすると、1 つの具体的な入力ファイル (引数 1) と、別個の出力ファイル (output) を使用できます。

Note

Linux API マニュアルによると、O_NONBLOCK は、ファイル アクセスでは何の働きもないため、削除しました。また、行 45 の変数宣言に out ファイル記述子を追加する必要があります。

このようにテストを実行することの別の影響として、デバイス照会の結果をデバイス データ読み取りと同じ入力ファイルから取得する必要があります。実際は、これはよくない選択です。デバイスの情報をデータ ファイルとは別に保存し (device_name.datdevice_desc.dat など)、個別に読み取るほうがよいでしょう。

Note

サンプルでこのようにテストすることを選択したのは、説明を簡単にするためです。テストのためにアプリケーションをより詳細に制御したい場合、上記サンプルよりも変更をモジュール化するべきです。

インクルードを修正する

ハードウェアに関連するすべてのコードを取り除くことが目的であるため、libc などの共有ライブラリ以外の #include 指令を不要にします。このサンプルでは、ソース コードから次の行 (14-15) を削除します。

14
15
#include <linux/input.h>
#include <linux/hidraw.h>

しかし、これらのファイルの一部の情報は必要です。特に、後でコードを置き換えるには、linux/hidraw.h の一部の #define マクロが何であるかを知る必要があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct hidraw_report_descriptor {
    __u32 size;
    __u8 value[HID_MAX_DESCRIPTOR_SIZE];
};

struct hidraw_devinfo {
    __u32 bustype;
    __s16 vendor;
    __s16 product;
};

/* ioctl interface */
#define HIDIOCGRDESCSIZE    _IOR('H', 0x01, int)
#define HIDIOCGRDESC        _IOR('H', 0x02, struct hidraw_report_descriptor)
#define HIDIOCGRAWINFO      _IOR('H', 0x03, struct hidraw_devinfo)
#define HIDIOCGRAWNAME(len)     _IOC(_IOC_READ, 'H', 0x04, len)
#define HIDIOCGRAWPHYS(len)     _IOC(_IOC_READ, 'H', 0x05, len)
/* The first byte of SFEATURE and GFEATURE is the report number */
#define HIDIOCSFEATURE(len)    _IOC(_IOC_WRITE|_IOC_READ, 'H', 0x06, len)
#define HIDIOCGFEATURE(len)    _IOC(_IOC_WRITE|_IOC_READ, 'H', 0x07, len)

#define HIDRAW_FIRST_MINOR 0
#define HIDRAW_MAX_DEVICES 64
/* number of reports to buffer */
#define HIDRAW_BUFFER_SIZE 64

上の 2 つの struct の定義をプログラムにコピーする必要があります。残りは、テスト ドライバーの作成過程で置換できます。struct 定義を使用するには、hid.h にある HID_MAX_DESCRIPTOR_SIZE の値も必要です。

#define HID_MAX_DESCRIPTOR_SIZE     4096

さまざまな ioctl 呼び出しで使用されるサイズや型を理解するには、ioctl.h から一部の定義を探す必要もあります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#define _IOC_NRBITS 8
#define _IOC_TYPEBITS   8
#define _IOC_SIZEBITS  14

#define _IOC_NRSHIFT    0
#define _IOC_TYPESHIFT  (_IOC_NRSHIFT+_IOC_NRBITS)
#define _IOC_SIZESHIFT  (_IOC_TYPESHIFT+_IOC_TYPEBITS)
#define _IOC_DIRSHIFT   (_IOC_SIZESHIFT+_IOC_SIZEBITS)

#define _IOC(dir,type,nr,size) \
    (((dir)  << _IOC_DIRSHIFT) | \
    ((type) << _IOC_TYPESHIFT) | \
    ((nr)   << _IOC_NRSHIFT) | \
    ((size) << _IOC_SIZESHIFT))

#define _IOC_READ  2U

#define _IOC_TYPECHECK(t) (sizeof(t))

#define _IOR(type,nr,size)  _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))

次のセクションのために、これらを参照できるようにしておきます。

次に、bus_str 関数を正常にコンパイルできるよう、input.h から 4 つのバス型をコピーします。

1
2
3
4
#define BUS_USB         0x03
#define BUS_HIL         0x04
#define BUS_BLUETOOTH   0x05
#define BUS_VIRTUAL     0x06

最後に、ドライバー テストの過程で不要になる 30-33 行を削除します (実は、これらは古いシステムとの互換性を提供するために重複している hidraw.h のコードです)。

API 呼び出しを置き換える

すでに説明したとおり、現代のシステムでは、一般的にカーネルがハードウェア自体を制御して、ハードウェアとやりとりするユーザーランド コードには、単にインターフェイスを提供します。このサンプルでは、システム コール (ioctl) を使用してハードウェアとやりとりしています。Mayhem は、これらの ioctl を利用できない隔離された環境でソフトウェアを実行するため、何らかの利用可能なもので呼び出しを置き換える必要があります。

多くの場合、一番よいのは、呼び出しをファイルの読み取りおよび書き込みに変えることです。すでに、デバイスではなく実際のファイルを開くよう変更してあるので、置き換えは非常に簡単です。

プログラム内の ioctl の呼び出しは、以下ですべてです。

// line 65
res = ioctl(fd, HIDIOCGRDESCSIZE, &desc_size);

// line 73
res = ioctl(fd, HIDIOCGRDESC, &rpt_desc);

// line 84
res = ioctl(fd, HIDIOCGRAWNAME(256), buf);

// line 91
res = ioctl(fd, HIDIOCGRAWPHYS(256), buf);

// line 98
res = ioctl(fd, HIDIOCGRAWINFO, &info);

// line 114
res = ioctl(fd, HIDIOCSFEATURE(4), buf);

// line 122
    res = ioctl(fd, HIDIOCGFEATURE(256), buf);

それぞれの ioctl は読み取り、書き込み、あるいはその両方を行う可能性があるので、それぞれをコンテキストの中で見て、どのように置き換えるべきかを把握します。それには、ioctl への第 2 引数に注目し、上記のヘッダー ファイルから抜粋した情報を確認する必要があります。調べた結果は次のようになります。

  • 行 65: サイズ sizeof(int) の読み取り (_IOR)
  • 行 73: サイズ sizeof(struct hidraw_report_descriptor) の読み取り (_IOR)
  • 行 84: サイズ 256 (sizeof(buf)) の読み取り (_IOR)
  • 行 91: サイズ 256 (sizeof(buf)) の読み取り (_IOC_READ)
  • 行 98: サイズ sizeof(struct hidraw_devinfo) の読み取り (_IOC_READ)
  • 行 114: サイズ 4 の読み取りおよび書き込み (_IOC_READ|_IOC_WRITE)
  • 行 122: サイズ 256 (sizeof(buf)) の読み取りおよび書き込み (_IOC_READ|_IOC_WRITE)

これらのマッピングができたので、ioctl 呼び出しを適切な read および write 呼び出しに変換できるようになりました。

// line 65
res = read(fd, &desc_size, sizeof(int));

// line 73
res = read(fd, &rpt_desc, sizeof(struct hidraw_report_descriptor));

// line 84
res = read(fd, buf, sizeof(buf));

// line 91
res = read(fd, buf, sizeof(buf));

// line 98
res = read(fd, &info, sizeof(struct hidraw_devinfo));

// line 114
write(out, buf, 4);
res = read(fd, buf, 4);

// line 122
write(out, buf, sizeof(buf));
res = read(fd, buf, sizeof(buf));

ioctl の呼び出しに加えて、ハードウェアの I/O につながる他の呼び出しが 2 つあるので、それらも調べる必要があります。

// line 136
res = write(fd, buf, 2);

// line 145
res = read(fd, buf, 16);

どちらもすでにファイル I/O になっているので、あとはそれぞれのケースで適切なファイル記述子を渡しているかを確認するだけです。このサンプルでは、変更する必要があるのは 1 つだけです。行 136 の fdout に置き換えます。

// line 136
res = write(out, buf, 2);

これで終わりです。最後の仕上げとして、main の末尾に out ファイルの close を追加し、ファイル先頭の #include から sys/ioctl.h を削除します。

Tip

hid-harness.c solution ファイル を参照し、正しく変更できたかを確認しましょう。

テスト ドライバーをビルドする

必要な変更をすべて終えたので、次はテスト ドライバーをビルドします。サンプルは次のコマンドを使用して簡単にコンパイルできるはずです。

gcc hid-harness.c -o hid-harness

アプリケーションをパッケージ化する

テスト ドライバーをビルドしたら、Mayhem で実行するためにパッケージを作成します。

mayhem package -o /tmp/hid-harness hid-harness

パッケージを使用する前に、空の output ファイルを追加する必要があります。

touch /tmp/hid-harness/root/path/to/output

Info

上記のサンプルの /path/to を自分の hid-harness 実行ファイルへのパスに置き換えてください。

その後、ハーネスをアップロードしてテストを開始できます。

mayhem run /tmp/hid-harness

✏️ まとめと振り返り

このレッスンでは、 ハードウェア依存関係があるソフトウェアをテストするテスト ドライバーの作成方法を学びました。


学習内容

1.ハードウェアがソフトウェアと会話する可能性があるケースを説明する。
  • ソフトウェアは以下の例のようにセンサーから入力を読み取る場合があります。

    1. 温度センサーから読み取りを行うデジタル温度計
    2. ジャイロスコープから読み取りを行う Fitbit
    3. iPhone の GPS から読み取りを行う Pokémon GO
    4. LIDAR から読み取りを行う自動車のバックアラーム システム
    5. AIS トランスポンダーから読み取りを行う船舶に搭載された船舶追跡システム
    6. 高度計から読み取りを行う航空機に搭載された高度警報システム
  • さらに、ソフトウェアがハードウェアに出力を送信する場合もあります。:

    1. 画面上のグラフィック表示
    2. 自動車の車線逸脱防止支援システム
    3. 無人航空機の自動操縦
    4. パワー グリッドの電力スパイク防止
    5. 水処理施設での水圧維持
    6. 製造工場でのロボット アームの制御
2.ハードウェアへの依存関係があるソフトウェアをテストするための戦略を検討する。
  • テストの際、Mayhem (および他のソフトウェア テスト ツール) はテスト対象ソフトウェアに単一の入力があると仮定します。そのため、テストを実行するにあたって、ハードウェア データを単一の入力ファイルまたはネットワーク接続として表現する方法を見つけることがゴールになります。ソフトウェアが実際にハードウェアと「会話」する方法によって、ゴールの達成は容易である場合も困難である場合もあります。
  • ハードウェアとソフトウェアが「会話」する主な方法はいくつかあります。
    1. 物理的接続を通じて (JTAG、serial、USB、802.11x など)
    2. ダイレクト メモリ アクセス (DMA) を通じて
3.サンプル ハードウェア テスト ドライバーをひととおり見る。
  • 次の操作を行う必要があります。
    1. 入力ソースを特定する。
    2. #include 指令を削除する
    3. 既存の API 呼び出しを置き換える。
    4. テスト ドライバーをビルドする 5.アプリケーションをパッケージ化する。