はじめに
ソフトを開発するときに、とりあえずコードがどう動くのか確認したいということがあります。
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レジスタに復帰する操作になっています。
setjmp
でp_runtsk
へCPUレジスタの内容を保存し、longjmp
でp_schedtsk
に保存されたCPUレジスタを復帰すれば、ディスパッチャーと同じ動作になります。
/* 実行中のタスクのコンテキストを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
で準備したタスクと、次のタスクとの間にスタック領域を確保する必要があるので、ローカル変数でをスタック領域を確保するようにしました。
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用は下記で、
typedef struct __JUMP_BUFFER
{
unsigned long Ebp;
unsigned long Ebx;
:
} _JUMP_BUFFER;
64bit用は下記になります。
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
で切り替えています。
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
マルチスレッドでデバッグしたい場合は、こちらをお勧めします。