5
2

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.

TOPPERSAdvent Calendar 2021

Day 10

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

Posted at

はじめに

ソフトを開発するときに、とりあえずコードがどう動くのか確認したいということがあります。
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://github.com/toppers/TINETFuzzer

タスク切り替え処理

TOPPERS/ASPのディスパッチャーの処理は、現在実行中のタスク管理ブロックp_runtskへPC(プログラムカウンタ)とSP(スタックポインタ)を保存し、次に実行したいタスク管理ブロックp_schedtskに保存されたPCとSPをCPUレジスタに復帰する操作になっています。
setjmpp_runtskへCPUレジスタの内容を保存し、longjmpp_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の直前にFrameframeで上書きすることで、例外は発生しなくなりました。
64bitアプリの時だけの処置なので、_M_X64ifdefで切り替えています。

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

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

5
2
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
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?