はじめに
ソフトを開発するときに、とりあえずコードがどう動くのか確認したいということがあります。
GR-ROSEなど組込み開発では、配線やポート設定などの準備で、やりたい事までの道のりが長くなりそうで、気が重くなります。
そんな時は、実機を使わずにVisual Studioなどでコードを書いて、Windowsアプリとしてデバッグしてから、実機に入れてみたりしています。
ただし、IOレジスタへのアクセスがあると、Windowsアプリとしては不正なメモリアクセスで例外となり、デバッグになりません。
そこで、IOレジスタのアクセスがあっても動くよう、Windowsの仮想メモリを利用して、IOレジスタの操作をフックして疑似動作させる仕組みを考えましたので紹介します。
Windowsの仮想メモリとアクセス例外
Windowsは仮想メモリを割り当てたり、読み込み専用などの仮想メモリの属性を変更するAPIがあります。また、割り当てた仮想メモリの属性で許可していないアクセスがあると例外が発生し、それを例外ハンドラで処理することが出来ます。
仮想メモリの属性にはPAGE_GUARD
というのがあり、この属性のあるメモリ領域にアクセスするとEXCEPTION_GUARD_PAGE
という例外が発生します。この例外をキャッチして、アクセスしたアドレスと内容が分かれば、IOレジスタをアクセスしたように振る舞うよう実装することが出来そうです。
PAGE_GUARD
については、こちらに説明があります。
例外の処理方法については、こちらに説明があります。
疑似IOレジスタの実装
疑似IOレジスタの実装の要点を書いてみました。
- 対象のコードをMicrosoftの構造化例外の構文
_try
/__except
で囲い、例外を受け取れるようにします。 - IOレジスタ領域として
VirtualAlloc
で仮想メモリを割り当てます。その時にPAGE_GUARD
属性を付けます。 - 対象のコードがIOレジスタ領域をアクセスすると、
EXCEPTION_GUARD_PAGE
例外が発生します。 - 例外を
__except
でキャッチし、例外の内容をGetExceptionInformation
で取得、IOレジスタのアドレスや値を特定します。 - アクセスしたIOレジスタのアドレスを元に、疑似的な動作を行います。
- 例外処理の戻り値を
EXCEPTION_CONTINUE_EXECUTION
にし、例外を無かったことにして継続実行します。
準備
GR-ROSEのIOレジスタ定義はiodefine.h
にアドレスが直接書かれているので、仮想メモリ領域を指すIORegister
変数を使ったアクセスになるように書き換えます。
#define BSC (*(volatile struct st_bsc *)0x81300)
#define CAC (*(volatile struct st_cac *)0x8B000)
:
上記の部分を下記のように書き換えておきます。
extern unsigned char *IORegister;
#define BSC (*(volatile struct st_bsc *)&IORegister[0x81300 - 0x80000])
#define CAC (*(volatile struct st_cac *)&IORegister[0x8B000 - 0x80000])
:
デバッグ対象のコードをapp_main
として、実装部に下記のように宣言しておきます。
// デバッグ対象のコード
int app_main();
// 疑似IOレジスタ領域
unsigned char *IORegister;
// 疑似IOレジスタ領域サイズ
const SIZE_T IORegisterSize = 0x100000;
大枠
大まかな流れをmain
関数に実装してみました。
始めに、疑似IOレジスタ領域IORegister
に仮想メモリをVirtualAlloc
APIでPAGE_GUARD
属性付きで取得します。
デバッグ対象のコードを__try
/__except
で囲って、例外をPageFaultExceptionFilter
関数で処理するようにします。
例外の情報はGetExceptionInformation
APIで取得して、例外を処理する関数に引数として渡します。
対象のコードが終了したら仮想メモリの後片付けをVirtualFree
APIで行って終了です。
これで、対象のコードで疑似IOレジスタ領域にアクセスすると例外が発生し、PageFaultExceptionFilter
関数が呼ばれるハズです。
int main()
{
int ret;
// 疑似IOレジスタ領域に仮想メモリを``PAGE_GUARD``属性付きで取得
IORegister = VirtualAlloc(NULL, IORegisterSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE | PAGE_GUARD);
if (IORegister == NULL)
return -1;
// 構造化例外を受け取れよう設定
__try
{
// 対象コードを実行
ret = app_main();
}
// 例外の内容を用意したPageFaultExceptionFilterで処理
__except (PageFaultExceptionFilter(GetExceptionInformation()))
{
return GetLastError();
}
// 仮想メモリを解放
VirtualFree(IORegister, 0, MEM_RELEASE);
return ret;
}
例外処理
疑似IOレジスタ領域へのアクセス以外の例外をキャッチした場合は、戻り値をEXCEPTION_CONTINUE_SEARCH
にして受け流します。
アクセスした命令のアドレスはExInfo->ExceptionRecord->ExceptionAddress
に入り、IOレジスタに相当するアドレスはExInfo->ExceptionRecord->ExceptionInformation[1]
に入り、例外発生時のCPUレジスタの内容はExInfo->ContextRecord
に入ります。
これらの情報から、IOレジスタへ書いた値や読んだ値を解析し処理するためのAnalizeCode
関数に渡します。戻り値は、アクセスした命令コードのサイズで、命令を実行したことにするために戻り番地に細工します。
仮想メモリのPAGE_GUARD
属性は、一度アクセスするとなくなってしまうので、VirtualProtect
で再度属性を付けます。
戻り値はEXCEPTION_CONTINUE_EXECUTION
として、例外発生元のコードに戻って処理を継続します。
INT PageFaultExceptionFilter(PEXCEPTION_POINTERS ExInfo)
{
// 対象の例外以外は
if (ExInfo->ExceptionRecord->ExceptionCode != EXCEPTION_GUARD_PAGE) {
// ここでは処理しない
return EXCEPTION_CONTINUE_SEARCH;
}
// IOレジスタにアクセスしたアドレスを特定して、疑似動作を行う
int codesize = AnalizeCode(ExInfo->ExceptionRecord->ExceptionAddress, (void *)ExInfo->ExceptionRecord->ExceptionInformation[1], ExInfo->ContextRecord);
// 解析出来なかった場合は
if (codesize <= 0) {
// 例外として処理
return EXCEPTION_EXECUTE_HANDLER;
}
// IOレジスタへアクセスしたCPU命令を飛ばして復帰するよう設定
ExInfo->ContextRecord->Rip += codesize;
// 仮想メモリ(疑似IOレジスタ領域)に再度PAGE_GUARDを設定
DWORD oldAttr = 0;
BOOL ok = VirtualProtect(IORegister, IORegisterSize, PAGE_READWRITE | PAGE_GUARD, &oldAttr);
// 失敗した場合は
if (!ok) {
// 例外として処理
return EXCEPTION_EXECUTE_HANDLER;
}
// 元のコードを継続実行
return EXCEPTION_CONTINUE_EXECUTION;
}
IOレジスタへのアクセスの解析
上記の例外処理から渡された情報で、IOレジスタのアドレスを取得し、疑似動作を行った後、命令サイズを返して関数を抜けます。
ここで、APIから返される値では、書いた値や読んだ値を渡す方法などが分からないので、動作するPCのCPUコード(x86)を読んで解析します。
数種類のx86コード(mov xx,yy
)を解析する程度で、とりあえず動きます。下のコードでは省きました。
int AnalizeCode(unsigned char *_opecode, void *_addr, PCONTEXT ContextRecord)
{
// 疑似IOレジスタ領域で無ければ無視
if ((_addr < (void *)IORegister) || (_addr > (void *)&IORegister[IORegisterSize]))
return 0;
// IOレジスタのアドレスを特定
uint32_t addr = (uint32_t)(_addr - (void *)IORegister) + 0x80000;
unsigned char *opecode = _opecode;
// 例外を起こしたx86命令を解析し、命令サイズをopecodeに設定
// 書き込みアクセスの場合、書き込もうとした値をソースレジスタから取得
// (ContextRecord->Raxなどから取得)
// IOレジスタの疑似動作を実行
// 読み込みアクセスの場合、IOレジスタの値をディストネーション・レジスタに設定
// (ContextRecord->Raxなどへ設定)
// 実行したことにする命令サイズを返却
return (int)(opecode - _opecode);
}
IOレジスタの疑似動作の実装
GR-ROSE向けのIOレジスタの疑似動作は、デバッグ内容に合わせて実装する必要があります。ここが一番大変な部分ですが、作っているものの対向処理を作ることで、理解が深まり成果物の品質が上がるのではないでしょうか…。
参考として、RX65NのIOレジスタ動作が実装されたSoftgunというシミュレーターを発見したので紹介します。
このシミュレーターはCPU命令を実行するものですが、いくつかのIOレジスタ動作の実装もあるので、これが流用出来ます。初めから作るよりは楽かもしれません。
おわりに
組込み向けのドライバ開発を、Windowsアプリとしてデバッグしながら作っていくことで、機材の準備などに気を取られずに、ある程度の所までは快適に進められると思います。
Visual Studioでのデバッグでは、ブレークポイントをたくさん貼れたり、スタック破壊の検出やメモリリークの検出ができたり、コールスタックを遡ったところの変数の値が確認できたりと、組込み機器でのデバッグより快適に行えるのでお勧めです。
GR-ROSEでTinyUSBを使ったアプリのデバッグのために、作ったものがありますので紹介します。
https://github.com/h7ga40/win_gr_rose
これは、下記のコンテストに応募した作品の開発を進めるために作ったものです。