開発者にとって最も恐ろしいものは何でしょうか。バグでしょうか?皆さんご存知のように、デバッグ段階のバグは恐ろしいものではありません。特にランダムで規則性のないINTバグは、その姿を見せることもなく消え去ります。
組み込みシステムの開発者も同様です。一般的に、組み込みシステムでは異常分析の方法が提供されており、特に強力なデバッグツールが利用可能です。これらのツールはPCでプログラミングする際に使用するものと同じで、Visual Studioシリーズのようなものです。しかし、特定の組み込みシステムや小規模なシステムでは、専用のデバッグツールが提供される場合があります。機能面ではMicrosoftが提供するVSほど強力ではありませんが、使い勝手もあまり良くありませんが、類似のデバッグ機能は提供されます。ここでは主にMicrosoftが提供するツールについて説明します。
現在、車載市場とPND市場ではWinCEシステムが広く使用されています。WinCE6.0システムでは、アプリケーションに深刻なエラーが発生した場合、システム標準の非常に迷惑なアプリケーションエラーダイアログボックスが表示されます。おおよそ「XXX.exeに深刻なエラーが発生したため、終了する必要があります」というメッセージが表示されます。
このような問題をどのように解決するのでしょうか?
デバッグシリアルポートに接続できるか、VS2008などのデバッグツールに接続できる場合、エラー発生時の例外情報を取得すれば、例外の原因を分析できます。しかし、デバイスが量産段階にあり、ログ出力用のシリアルポートやデバッグUSBポートに接続できない場合、例外情報をキャプチャする方法はあるのでしょうか。
この問題を根本的に解決できない場合、システムがそのエラーダイアログボックスを表示しない方法を考えた人もいます。アプリケーションを「優雅に」終了させる(ネット上での表現)ため、つまりプログラムがエラーダイアログを表示せずに終了するために、WinCEが提供するカーネルコードを変更しようと試みた人もいますが、これはオープンソースではない部分に属するため、この方法も実行不可能です!
この問題を解決する根本的な方法もちろん、コードの品質を向上させ、品質保証(テスト)を強化し、可能な限りバグを開発段階で排除することです。開発段階では、以下の最初の方法のような多くのデバッグツールを使用できるためです。
デバッグツールに頼れない場合、例外情報を取得する方法はありますか?もちろんあります。以下の第二番目と第三番目の方法です。なぜ最初の方法を説明する必要があるのでしょうか?それは、最も基本的な情報を提供しており、後の2つの方法の両方で使用される基礎となるからです。ここで、私は第三の方法を特に推奨します。なぜなら、その処理は比較的独立しており、WinCEシステムで効果的であり、既存のコードに統合して例外キャプチャを実装するのに便利だからです。
**最初の方法:**出力LOGシリアルポートが利用できる場合、シリアルポートからの例外出力とMAPファイルを一緒に分析することで、関数レベルまでエラーの発生箇所を特定できます。そのため、デバッグ時には必ず対応するバージョンのMAPファイルを一緒に保持し、後続の異常問題の分析に使用する必要があります。
以下のテストコードを考えてみましょう:
1 void SampleCrashFunction(void)
2 {
3 int *nullPointer = NULL;
4
5 RETAILMSG(1,(L"-----------------------------%d\r\n",nullPointer));
6 *nullPointer = 0;
7 RETAILMSG(1,(L"-----------------------------%d,%d\r\n",nullPointer,*nullPointer));
8 }
9
10 void TriggerCrashFunction(void)
11 {
12 SampleCrashFunction();
13 }
14
15 void CDeviceAppDialog::OnTimer(UINT_PTR timerId)
16 {
17 // TODO: ここにメッセージ処理コードを追加するか、または既定値を呼び出します
18 if(1 == timerId)
19 {
20 KillTimer(1);
21 TriggerCrashFunction();
22
23 // 他の機能
24 }
25 CDialog::OnTimer(timerId);
26 }
WinCE6.0とWinCE7.0で実行すると、シリアルポートの出力内容は基本的に同じですが、WinCE7.0ではエラーダイアログが表示されません。
シリアルポートに出力されるクラッシュ情報は以下の通りです:
Exception 'Data Abort' (0x4): Thread-Id=0780000a(pth=c08e24e0), Proc-Id=077e000a(pprc=c088da7c) 'DeviceApp.exe', VM-active=077e000a(pprc=c088da7c) 'DeviceApp.exe'
PC=00011738(DeviceApp.exe+0x00001738) RA=4002ac4c(coredll.dll+0x0001ac4c) SP=0004f6a8, BVA=00000000
Exception 'Raised Exception' (0x116): Thread-Id=0780000a(pth=c08e24e0), Proc-Id=00400002(pprc=8360b5e0) 'NK.EXE', VM-active=077e000a(pprc=c088da7c) 'DeviceApp.exe'
PC=eff6ed60(k.coredll.dll+0x0001ed60) RA=8052a62c(kernel.dll+0x0000e62c) SP=d9bbf3b4, BVA=ffffffff
対応するMAPファイルから、SampleCrashFunction関数でエラーが発生したことがわかります(PCポインタ0x00001738 + MAPファイルのPreferred load addressオフセット)。この例ではエラー発生位置は:0x00001738 + 00010000 = 00011738です:
DeviceApp
Timestamp is 539fa9e3 (Tue Jun 17 10:37:23 2014)
Preferred load address is 00010000
......
0001:000006a8 ?InitInstance@CDeviceAppApp@@UAAHXZ 000116a8 f DeviceApp.obj
0001:00000708 ?OnCbnDropdownCombo1@CDeviceAppDialog@@QAAXXZ 00011708 f DeviceAppDialog.obj
0001:00000714 ?SampleCrashFunction@@YAXXZ 00011714 f DeviceAppDialog.obj
0001:0000075c ?BeginModalState@CWnd@@UAAXXZ 0001175c f i DeviceAppDialog.obj
0001:00000768 ?EndModalState@CWnd@@UAAXXZ 00011768 f i DeviceAppDialog.obj
0001:00000774 ??_GCComboBox@@UAAPAXI@Z 00011774 f i DeviceAppDialog.obj
これにより、WinCE7.0システムはNULLポインタへの代入などの例外に対して何らかの処置をしていることがわかります。少なくとも、非常に迷惑なダイアログボックスを表示せず、その後の他の機能の実行にも影響を与えません。WinCE6.0で同様のダイアログが表示された場合、アプリケーションは終了します。
第二の方法:__tryと__exceptを使用する。オープンソースのマルチメディアプレーヤーMediaPlayerClassicでは、以下のような使用方法があります。
まず2つのマクロを定義し、重要な処理スレッドコードを定義されたこれら2つのマクロ内に含めることで、2つのマクロ間のコードで発生する例外をキャプチャします。この方法には欠点がありますが、コード量が大きくなると、これら2つのマクロの呼び出しを多く追加する必要があります。
1#define EXCEPTION_START __try {
2#define EXCEPTION_END ;} __except (HandleException(_exception_info())) {}
WinCEでの使用方法は、PC上のSEH(構造化例外処理)と同じであることがわかります。以下に示します:
1__try
2{
3 // 保護されたコード
4}
5__except ( expression )
6{
7 // 例外ハンドラコード
8}
以下はMediaPlayerClassicの重要なスレッドの例外処理コードです(MediaPlayerClassicスレッドのコードは完全には記載されていません、興味のある方はMediaPlayerClassicのソースコードをご覧ください)。ここでは、定義された2つの例外処理マクロがスレッドのすべてのコードを含んでいます。
1 static int ProcessingThread(player_base* context)
2 {
3 int resultCode = SUCCESS_NONE;
4
5 #ifdef MULTITHREAD
6 EXCEPTION_START
7
8 while (context->Window)
9 {
10 ......
11 if (context->ExecuteProcessing)
12 {
13 processstate State;
14 State.Fill = context->Fill;
15
16 context->Timer->Get(context->Timer,TIMER_TIME,&State.Time,sizeof(tick_t));
17
18 //DEBUG_MSG1(DEBUG_PLAYER,T("処理時間:%d"),State.Time);
19
20 resultCode = context->Format->Process(context->Format,&State);
21
22 if (resultCode == SUCCESS_SYNCED)
23 {
24 ......
25 }
26 else if (context->Fill && (resultCode == SUCCESS_END_OF_FILE || resultCode == SUCCESS_BUFFER_FULL
27 || (resultCode == SUCCESS_NEED_MORE_DATA && (context->NoMoreInput || State.BufferUsedAfter >= context->CurrBufferSize2-2))))
28 {
29 ......
30 }
31
32 ......
33 }
34 ......
35 }
36
37 EXCEPTION_END
38 return 0;
39 }
この実装方法で最も重要なのは、HandleException()関数での例外情報の分析と記録方法です。
しかしMediaPlayerClassicでは、例外情報の取得がMediaPlayerClassicのソフトウェアフレームワークと結合しています。この部分のコードを他のプロジェクトに移植する場合、有用なコードを分離する必要があります。これは実際には比較的簡単です。
EXCEPTION_POINTERSに関連するコードを取り出すだけです。
1 int HandleException(void* exceptionData)
2 {
3 EXCEPTION_POINTERS* Data = (EXCEPTION_POINTERS*)exceptionData;
4
5 // 関連しないコードを削除 - この部分のコードはMediaPlayerClassicのコードなので、整形していません
6 {
7 {
8 const uint8_t* ContextRecord = (const uint8_t*) Data->ContextRecord;
9 EXCEPTION_RECORD* Record = Data->ExceptionRecord;
10
11 switch (Record->ExceptionCode)
12 {
13 case STATUS_ACCESS_VIOLATION: Name = T("アクセス違反"); break;
14 case STATUS_BREAKPOINT: Name = T("ブレークポイント"); break;
15 case STATUS_DATATYPE_MISALIGNMENT: Name = T("データ型アラインメント違反"); break;
16 case STATUS_ILLEGAL_INSTRUCTION: Name = T("不正な命令"); break;
17 case STATUS_INTEGER_DIVIDE_BY_ZERO: Name = T("整数除算ゼロ"); break;
18 case STATUS_INTEGER_OVERFLOW: Name = T("整数オーバーフロー"); break;
19 case STATUS_PRIVILEGED_INSTRUCTION: Name = T("特権命令"); break;
20 case STATUS_STACK_OVERFLOW: Name = T("スタックオーバーフロー"); break;
21 default: Name = T("不明"); break;
22 }
23
24 if (Record->ExceptionCode == STATUS_ACCESS_VIOLATION)
25 {
26 if (Record->ExceptionInformation[0])
27 Name = T("書き込み先");
28 else
29 Name = T("読み取り先");
30 }
31
32 //...... 重要なのはEXCEPTION_POINTERS構造体の関連メンバーを処理すること
33 // その他関連情報、実行可能ファイル名などは必要に応じて取得する
34 }
35 }
**第三の方法:**AddVectoredExceptionHandler()関数を使用する。
WinCEでこの関数を使用するには、TlHelp32.hヘッダーファイルとtoolhelp.libライブラリファイルを含める必要があります。この関数はWinCEの非公開APIであるため、ヘルプはPC上のものを参考にしてください。
この関数を使用すると、WinCEシステムにベクタ例外ハンドラを登録し、例外が発生するとこのハンドラが呼び出されます。
関数のプロトタイプは以下の通りです(MSDN)、各パラメータの具体的な意味についてはMSNを参照してください。ここでは翻訳は行いません。
1PVOID WINAPI AddVectoredExceptionHandler(__in ULONG FirstHandler, __in PVECTORED_EXCEPTION_HANDLER VectoredHandler);
以下のコードは、AddVectoredExceptionHandler()関数の使用方法を示しています: (1) AddVectoredExceptionHandler(1,MyVectoredExceptionHandler);
(2) 例外ハンドラの定義
1 LONG WINAPI MyVectoredExceptionHandler(struct _EXCEPTION_POINTERS *exceptionInfo)
2 {
3 typedef ULONG (WINAPI *lpGetThreadCallStack)(HANDLE,ULONG,LPVOID,DWORD,DWORD);
4 /* 使用時にいくつかのヘッダーファイルを含める必要があります。これらのヘッダーファイルはWinCEのインストールディレクトリから取得する必要があります。
5 OSバージョン: Windows CE 5.0以降。
6 ヘッダー: Pkfuncs.h。
7 */
8 typedef struct _CallSnapshotEx
9 {
10 DWORD dwReturnAddr;
11 DWORD dwFramePtr;
12 DWORD dwCurProc;
13 DWORD dwParams[4];
14 }CallSnapshotEx;
15 // ダンプ情報を出力
16 ......
17 // SPスタックを出力
18 ......
19 ULONG *stackPointer = (ULONG *)exceptionInfo->ContextRecord->Sp;
20 // スレッドスタック呼び出しを取得
21 HMODULE hCore = LoadLibrary(L"coredll.dll");
22 if(NULL != hCore)
23 {
24 lpGetThreadCallStack pGetThreadCallStack = (lpGetThreadCallStack)GetProcAddress(hCore,L"GetThreadCallStack");
25 if(NULL != pGetThreadCallStack)
26 {
27 }
28 }
29 // プロセス内DLL情報を取得
30 MODULEENTRY32 CurrentModule;
31 HANDLE hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE,GetCurrentProcessId());
32 if((HANDLE)-1 != hSnapShot)
33 {
34 // Module32FirstとModule32Nextを呼び出してModuleを列挙
35 }
36 }
プログラムの異常原因を分析するために他の情報が必要かどうかは、MSDNで構造_EXCEPTION_POINTERSの各メンバーの説明を参照してください。その後、有用な情報をSDカードなどの永続的なストレージデバイスに書き込みます。これにより、分析に使用するリソースを見つける必要がなくなります。また、この記録されたファイルの情報はシリアルポートからの例外情報よりもはるかに大きいです。同時に、必要に応じてアプリケーション(プロセス)の関連情報も入力できます。
この例外キャプチャ機能のコードをLIBとしてパッケージ化し、使用するプログラムから呼び出すことができます。
PC Windowsでのプログラムクラッシュ(実行時の深刻なエラー)を検知する方法と比較して、WinCEでは比較的少なく、実際に使用されているものはさらに少ないです。
PCでは、上記の第二と第三の方法に加えて、以下の3つの核心的な関数を使用してプログラムの異常を検知できます: SetUnhandledExceptionFilter(HandleException)、未処理の例外が発生した場合に呼び出される関数をHandleExceptionに設定します。この関数はWinCEでは使用できません、100%の代替関数はありません。 _set_invalid_parameter_handler(HandleInvalidParameter)、無効なパラメータ呼び出しが発生した場合に呼び出される関数をHandleInvalidParameterに設定します。 _set_purecall_handler(HandlePureVirtualCall)、純粋仮想関数呼び出しが発生した場合に呼び出される関数をHandlePureVirtualCallに設定します。
バグ解決方法を最大限に活用することは、開発者が成長する必須の道です。なぜなら、開発者の仕事はコーディングだけでなく、前段階の設計と後段階の製品問題の修正も含まれるからです。
優れたアプリケーションは、MediaPlayerClassicのように、例外が発生した際にユーザーに正しく通知すべきです。このようなプログラムのクラッシュも「優雅な」終了に近づいています。同時に、開発者が異常を分析するための情報も提供します:ファイルに記録します。