1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

がじぇるねGRAdvent Calendar 2021

Day 10

GR-ROSEのIOレジスタをWindowsで疑似動作させてみた

Last updated at Posted at 2021-12-09

はじめに

ソフトを開発するときに、とりあえずコードがどう動くのか確認したいということがあります。
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変数を使ったアクセスになるように書き換えます。

iodefine.h
#define	BSC			(*(volatile struct st_bsc      *)0x81300)
#define	CAC			(*(volatile struct st_cac      *)0x8B000)
:

上記の部分を下記のように書き換えておきます。

iodefine.h
extern unsigned char *IORegister;
#define	BSC			(*(volatile struct st_bsc      *)&IORegister[0x81300 - 0x80000])
#define	CAC			(*(volatile struct st_cac      *)&IORegister[0x8B000 - 0x80000])
:

デバッグ対象のコードをapp_mainとして、実装部に下記のように宣言しておきます。

main.c
// デバッグ対象のコード
int app_main();
// 疑似IOレジスタ領域
unsigned char *IORegister;
// 疑似IOレジスタ領域サイズ
const SIZE_T IORegisterSize = 0x100000;

大枠

大まかな流れをmain関数に実装してみました。
始めに、疑似IOレジスタ領域IORegisterに仮想メモリをVirtualAllocAPIでPAGE_GUARD属性付きで取得します。
デバッグ対象のコードを__try/__exceptで囲って、例外をPageFaultExceptionFilter関数で処理するようにします。
例外の情報はGetExceptionInformationAPIで取得して、例外を処理する関数に引数として渡します。
対象のコードが終了したら仮想メモリの後片付けをVirtualFreeAPIで行って終了です。
これで、対象のコードで疑似IOレジスタ領域にアクセスすると例外が発生し、PageFaultExceptionFilter関数が呼ばれるハズです。

main.c
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として、例外発生元のコードに戻って処理を継続します。

main.c
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)を解析する程度で、とりあえず動きます。下のコードでは省きました。

main.c
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

これは、下記のコンテストに応募した作品の開発を進めるために作ったものです。

https://algyan.connpass.com/event/224849/

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?