setjmp/longjmpでタスク切り替えしてみた

# はじめに

ソフトを開発するときに、とりあえずコードがどう動くのか確認したいということがあります。
TOPPERS/ASPなどを使った組込み開発では、配線やポート設定などの準備で、やりたい事までの道のりが長くなりそうで、気が重くなります。
そんな時は、実機を使わずにVisual Studioなどでコードを書いて、Windowsアプリとしてデバッグしてから、実機に入れてみたりしています。
ただし、TOPPERS/ASPなどRTOSを使ったソフトだと、マルチタスクで動作さてデバッグすることは出来ません。
そこで、setjmp/longjmpを使用してTOPPERS/ASP3のディスパッチャーを実装してWindowsのソフトでマルチタスク動作をさせてみました。

# setjmp/longjmpとマルチタスク

RTOSでマルチタスクを実現するには、ディスパッチャーと呼ばれているCPUコンテキストの切り替え処理が必要です。
ディスパッチャーをsetjmp/longjmpで実装した例はすでにあり、「Mac OS Xシミュレーション環境簡易パッケージ」でも使われています。

<https://www.toppers.jp/asp3-e-download.html>

またTOPPERS/ASPをLinuxに移植したQiitaの記事を発見したので紹介します。

<https://qiita.com/morioka/items/a186fff4db1eabb7e7de>

この説明でのWindows向け実装は上記と違うコードで行いました。下記のFuzzingテスト用に作ったもので、Windowsへの移植というよりは、テスト[治具](https://astamuse.com/ja/keyword/11557776)のようなものです。割り込み処理については考慮していません。

<https://github.com/toppers/TINETFuzzer>

# タスク切り替え処理

TOPPERS/ASPのディスパッチャーの処理は、現在実行中のタスク管理ブロック``p_runtsk``へPC(プログラムカウンタ)とSP(スタックポインタ)を保存し、次に実行したいタスク管理ブロック``p_schedtsk``に保存されたPCとSPをCPUレジスタに復帰する操作になっています。
``setjmp``で``p_runtsk``へCPUレジスタの内容を保存し、``longjmp``で``p_schedtsk``に保存されたCPUレジスタを復帰すれば、ディスパッチャーと同じ動作になります。

```target_kernel_impl.c
/* 実行中のタスクのコンテキストをsetjmpで保存 */
if (p_runtsk == NULL || setjmp(p_runtsk->tskctxb.TASK) == 0) {
  /* 次に実行したいタスクを実行中に設定 */
  p_runtsk = p_schedtsk;
  /* 実行したいタスクにlongjmpでジャンプ（すぐ上のsetjmpで戻り値は1） */
  longjmp(p_runtsk->tskctxb.TASK, 1);
}
```

``setjmp``の戻り値が``0``の時は、タスク切り替え動作のため``longjmp``します。
``setjmp``の戻り値が``1``の時は、上記の``longjmp``から戻ってきたので、処理を継続します。

割り込み処理でのタスク切り替えを考えない場合、ディスパッチャーの処理は難しくないと思います。

# スタック領域の確保

setjmp/longjmpを使ったマルチタスクでは、スタック領域について気にする必要があります。
各タスクのスタック領域は、Windowsがアプリに割り振ったスタック領域にある必要があるので、アプリのスタック領域を分割して、タスクのスタック領域となるようにします。
スタック領域の分割方法は、関数の呼び出し階層を積み上げることで全体のスタック領域を確保し、途中の関数のスタック領域をそれぞれのタスクのスタック領域として分割します。
タスクのメイン関数を呼び出す手前で``setjmp``して実行状態を保存し、``longjmp``で戻った時にタスクのメイン関数が呼び出されるうにします。``setjmp``を呼び出した関数を抜けずに、次のタスクのメイン関数を呼び出す処理を呼んで、スタック領域を積み上げます。
``setjmp``で準備したタスクと、次のタスクとの間にスタック領域を確保する必要があるので、ローカル変数でをスタック領域を確保するようにしました。

```target_kernel_impl.c
void task_start(T_TSKDAT* p_tcb);

void task_start_internal(T_TSKDAT *tskdat)
{
  TCB *p_tcb = tskdat->tcb;

  switch (setjmp(p_tcb->tskctxb.TASK)) {
  case 0: {
    p_tcb->tskctxb.exitcode = 0;
    if (tskdat->next != NULL)
      task_start(tskdat->next);
    else
      dispatch();
    break;
  }
  case 1: if (p_tcb != NULL) {
    unl_cpu();
    p_tcb->p_tinib->task(p_tcb->p_tinib->exinf);
    loc_cpu();
    task_terminate(p_runtsk);
    p_tcb->tskctxb.exitcode++;
    dispatch();
    break;
  }
  default:
    break;
  }
}

void task_start(T_TSKDAT *tskdat)
{
  intptr_t stack[TASK_STACK_SIZE] = { 0 };
  tskdat->tinib.stk = &stack[TASK_STACK_SIZE];
  tskdat->tinib.stksz = sizeof(intptr_t) * TASK_STACK_SIZE;

  TCB *p_tcb = tskdat->tcb;
  p_tcb->p_tinib = &tskdat->tinib;

  task_start_internal(tskdat);
}
```

``task_start``はタスクを起動するための関数で、タスクで使うスタック領域を確保して``task_start_internal``を呼び出します。さらに他のタスクがある場合``task_start``を再帰的に呼び出してSPが戻らないようにします。
全てのタスク用のスタックが積み上がったところで、``dispatch``を呼び出してタスクを切り替えを始めます。
``task_start``のローカル変数``stack``でタスク用のスタック領域を確保しているつもりです。``tskdat->tinib.stk``に設定しているのは、ローカル変数を使っていないとコンパイラ警告になるので、それを抑えるためです。
あまり大きなスタックサイズだとスタックオーバーフローになってしまうので、システム全体をアプリ化するのには向いていません。テストなどに必要な最小限のタスクの限定した使い方になります。

#  0xC0000028: 無効なスタックまたは境界不整列なスタックがアンワインド操作で検出されました

ここまでの対応で、Visual Studioのclang-clでビルドした32bitアプリとしては動いたのですが、64bitアプリでは、``longjmp``実行時に「0xC0000028: 無効なスタックまたは境界不整列なスタックがアンワインド操作で検出されました。」という例外が発生してしまいます。
これは、すでに破棄されたスタック領域を指すSPを設定しようとした時に発生するようで、間違った使い方と判断しているようです。
``setjmp``を呼び出した関数の呼び出し元の関数から``longjmp``で戻ろうとすると、すでに破棄されているスタック領域なので、壊れているスタック領域を使うことになります。
スタックが壊れていると関数の呼び元に戻るための情報も壊れていので、まともな動作になりません。
今回はタスクのスタック領域を確保しているつもりなので、回避する方法を探しました。
Microsoftのドキュメントなどには見当たらなかったのですが、下記の対策で例外が発生しなくなったので紹介しておきます。
MSVCの``setjmp.h``には``setjmp``で保存される内容が構造体として参照できるようになっています。

32bit用は下記で、

```setjmp.h
typedef struct __JUMP_BUFFER
{
  unsigned long Ebp;
  unsigned long Ebx;
:
} _JUMP_BUFFER;
```

64bit用は下記になります。

```setjmp.h
typedef struct _JUMP_BUFFER {
  unsigned __int64 Frame;
  unsigned __int64 Rbx;
  unsigned __int64 Rsp;
:
} _JUMP_BUFFER;
```

ここで、64bit用には``Frame``というメンバ変数が追加されていて、SPの内容が入っているようでした。
OS起動時の関数でSPの値をグローバル変数``frame``に保存して、``longjmp``の直前に``Frame``を``frame``で上書きすることで、例外は発生しなくなりました。
64bitアプリの時だけの処置なので、``_M_X64``の``ifdef``で切り替えています。

```target_kernel_impl.c
void dispatch()
{
  if (p_runtsk == p_schedtsk)
    return;

  if (p_schedtsk == NULL) {
    context = 1;
    target_custom_idle();
    context = 0;
  }

  if (p_schedtsk == NULL) {
    longjmp(SCHEDULER_EIXT, 1);
    return;
  }

  if (p_runtsk == NULL || setjmp(p_runtsk->tskctxb.TASK) == 0) {
    p_runtsk = p_schedtsk;
#ifdef _M_X64
    _JUMP_BUFFER* jb = (_JUMP_BUFFER*)p_runtsk->tskctxb.TASK;
    jb->Frame = frame;
#endif
    longjmp(p_runtsk->tskctxb.TASK, 1);
  }
}
```

# おわりに

ここで紹介したタスク切り替えの方法は、リリースするアプリケーションに入れるのには問題がありそうなのでお勧めできませんが、マルチタスクのコードを試験する治具ソフトとして使うには有用ではないかと思います。
RTOSのタスクをWindowsのスレッドとして実装すると、Visual Studioのマルチスレッドのデバッグ機能が利用できますが、Fuzzingテストのように大量の回数試行するようなプログラムには、シングルスレッドでマルチタスクを実現できるので、向いていると思います。

ちなみに、ITRONをWindowsに移植したものは下記のサイトにあります。

<https://www.kana-soft.com/soft/win_itron/index.htm>

マルチスレッドでデバッグしたい場合は、こちらをお勧めします。