コンテキスト、context [名] 1. (文章などの)前後関係、コンテクスト、文脈 2. (事件等の)周囲の事情、環境、背景〜アンカー英和辞典 学習研究社刊、 ということで、この場合はつまるところ「プログラム実行の環境」を指すこ とになるのだろう。この場合の環境とは?少し乱暴かもしれないが、プロセッ サの全内部状態と言ってしまう。
ここでの全内部状態とは何か。ステータスレジスタ(SR)やプログラムカウンタ (PC)をも含むまるっきり全部である。プロセッサの全内部状態と言っても可だ。
尤も、実際に全レジスタ、全内部状態の保存/復帰を考慮する必要が無い場合 もある。
コンテキストの操作が、サブルーチンコール(とリターン)により発生する場合、 プロセッサによってはコール/リターン命令がPC保存、復帰をスタックを使っ て自動的に行なうので、その種のもの(H8、MC68000等)ではスタックポインタ (SP)さえ適切に保存、復帰されれば、PCについての特別な記述は無用である。
また、C言語によるプログラムならば、関数呼出によって破壊されることが許 容されたレジスタがあるので、それらに関する記述は無用となる。
関数呼出ならSRを考慮する必要がないのは考えるまでもない。
割り込みや何らかの例外処理によって生じるコンテキストの操作なら、言葉通 りの全レジスタの保存が必要になるが、それについては割り込みの実装の段階 まで置いておこう。
00:; ----------------------------------------------- 01:; 実行コンテキストの切替 02:; void hospac_swi_cnt( 03:; T_HOSPAC_CTXINF *pk_pre_ctxinf, /* 現在のコンテキストの保存先 */ 04:; T_HOSPAC_CTXINF *pk_nxt_ctxinf /* 切り替えるコンテキスト */ 05:; ) 06:; ----------------------------------------------- 07:_hospac_swi_cnt: 08: push.l er2 09: push.l er3 10: push.l er4 11: push.l er5 12: push.l er6 13: mov.l er7,@(0,er0) ; スタックポインタ保存 14: mov.l @(0,er1),er7 ; スタックポインタ復帰 15: pop.l er6 16: pop.l er5 17: pop.l er4 18: pop.l er3 19: pop.l er2 20: rts |
開発環境である秋月のCでは、er0に第一引数、er1に第二引数、第三引数以降 はスタックにより渡され、関数中ではer2〜er6を破壊してはならない仕様であ る(と読みとれる。実物も資料も無いので推定)。 ここでやっている事は、
8〜13行がこの関数を呼び出したコンテキストを保存する処理、即ち8〜12行で er2〜er6をスタックに保存、13行でそのスタックポインタer7を *pk_pre_ctxinf番地に保存する。
で、14〜19行はつまり*pk_pre_ctxinfのかわりに*pk_nxt_ctxinfから、8〜 12行の逆の操作でコンテキストを復帰するということだ。
この。
![]() |
hospac_swi_cnt(*pre_ctxinf, *nxt_ctxinf); により、関数に実行が移った瞬間の コンテキスト情報とスタック。 但し、 現在のコンテキスト = コンテキストA 切替え先のコンテキスト = コンテキストB この関数呼出における戻り番地は、 コンテキストAのスタックに積まれている。 |
![]() |
12行まで来た所。 スタックに保存しなければならない レジスタの格納が完了。 この時のスタックポインタ、er7=spAを 次の13行で*pre_ctxinfが指す番地に 保存する。 |
![]() |
15行まで来た所。 *nxt_ctxinfが指す番地に 保存されていたスタックポインタ spBををer7に復帰。 |
![]() |
20行まで来た所。 er6〜er2にはそれぞれ、 er6_B〜er2_Bが復帰されている。 これでrtsが実行されると、戻る 先は「コンテキストAにおいて hospac_swi_cnt()した次」 ではなく、「コンテキストBにおいて hospac_swi_cnt()した次」になる。 |
どうだろう、hospac_swi_cntを呼び出す前と後で、PCを含む全レジスタ (er0,er1は破壊される前提でコードが吐かれているので、プログラム実行には 全く影響が無い)〜コンテキストが交替しているではないか。
これと等価なことを、gccの仕様とSH4に合わせて書いてみよう。
gcc-2.95.2/gcc/config/sh/sh.hより、
/* Register allocation for the Hitachi calling convention: r0 arg return r1..r3 scratch r4..r7 args in r8..r13 call saved r14 frame pointer/call saved r15 stack pointer ap arg pointer (doesn't really exist, always eliminated) pr subroutine return address t t bit mach multiply/accumulate result, high part macl multiply/accumulate result, low part. fpul fp/int communication register rap return address pointer register fr0 fp arg return fr1..fr3 scratch floating point registers fr4..fr11 fp args in fr12..fr15 call saved floating point registers */ |
であるから、第一引数はr4、第二引数はr5、第三引数はr6、第四引数はr7で渡 され、関数内で保存されるべきレジスタはr8〜r14でありスタックポインタは r15だと分かった。FPUレジスタは取り敢えず無視しよう。
H8と異なり、SH4ではサブルーチンコール命令が戻り番地をスタックではなく、 PRレジスタに格納する。従って、戻り番地をプログラムでコンテキストの情報 としてスタックに保存しておく必要がある。
SH4のrts命令はいわゆる遅延分岐であり、実際の分岐は次の命令を実行した後 に発生することになる。
これだけ分かればもう書けたも同然。
01:! ----------------------------------------------- 02:! 実行コンテキストの切替 03:! void hospac_swi_cnt( 04:! T_HOSPAC_CTXINF *pk_pre_ctxinf, /* 現在のコンテキストの保存先 */ 05:! T_HOSPAC_CTXINF *pk_nxt_ctxinf /* 切り替えるコンテキスト */ 06:! ) 07:! ----------------------------------------------- 08:_hospac_swi_cnt: 09: mov.l r14,@-r15 10: mov.l r13,@-r15 11: mov.l r12,@-r15 12: mov.l r11,@-r15 13: mov.l r10,@-r15 14: mov.l r9,@-r15 15: mov.l r8,@-r15 ! r8-r14を保存 16: sts.l pr,@-r15 ! 戻り番地を保存 17: mov.l r15,@(0,r4) ! スタックポインタ保存 18: mov.l @(0,r5),r15 ! スタックポインタ復帰 19: lds.l @r15+,pr ! 戻り番地を復帰 20: mov.l @r15+,r8 ! r8-r14を復帰 21: mov.l @r15+,r9 22: mov.l @r15+,r10 23: mov.l @r15+,r11 24: mov.l @r15+,r12 25: mov.l @r15+,r13 26: rts 27: mov.l @r15+,r14 |
先ほどのH8におけるhospac_swi_cntの流れと同様に図に示すと、
![]() |
hospac_swi_cnt(*pre_ctxinf, *nxt_ctxinf); により、関数に実行が移った瞬間の コンテキスト情報とスタック。 但し、 現在のコンテキスト = コンテキストA 切替え先のコンテキスト = コンテキストB SH4では、戻り番地はレジスタPRに格納され、 自動的にスタックに保存されない。 この時点でPR = 戻り番地A となる。 |
![]() |
16行まで来た所。 スタックに保存しなければならない レジスタの格納を完了している。 ここでスタックにPR(戻り番地A)を プッシュする。 この順番決めには何の理由もない。 |
![]() |
18行まで来た所。 *nxt_ctxinfが指す番地に 保存されていたスタックポインタ spBをr15に復帰。 後は一気に復帰あるのみ。 |
![]() |
26行まで来た所。 r8〜r14にはそれぞれ、 r8_B〜er14_Bが復帰されていて、 PRには戻り番地Bが入っている。 これでrtsが実行されると、戻る 先は「コンテキストAにおいて hospac_swi_cnt()した次」 ではなく、「コンテキストBにおいて hospac_swi_cnt()した次」になる。 |
となる。これでOKだ。
コンテキストスイッチの流れの図から、スタックに積まれるべきコンテキスト 情報の形式は分かる(…て、順序が逆のような気もしないでもないが)。 H8/300H版、SH4版について改めて見直すと、
![]() |
スタックに積まれるべきコンテキスト情報 |
戻り番地?これから作成するコンテキストは始まってもいないではないか、と いう疑問は当然自然である。「戻り番地」とは、これから作成するコンテキス トにおいて最初に飛ぶべき「行き先番地」と読み替えれば納得できるだろうか。
じゃあhospac_cre_cnt_asmの第三引数であるtask ; 実行アドレスをそのまま 「行き先番地」として積むかというと、そうはいかない。何故なら、taskの引 数であるexinfの受渡しが出来ないからだ。引数の受渡しに使われるer0〜er1 は保存/復帰されるコンテキスト情報の内には入っていないのだ。入っていた としても、exinfはhospac_cre_intの第四引数であるのに対して、taskにとっ ては第一引数。これをなんとかする仕組みをtaskにジャンプする前にかまさな ければならない。
H8/300H版がどうなっているか見てみよう。
; ----------------------------------------------- ; 実行コンテキストエントリーアドレス ; ----------------------------------------------- ctx_entry: mov.l er2, er0 ; 実行時パラメータを第一引数に設定 jmp @er3 ; 実行アドレスにジャンプ ; ----------------------------------------------- ; 実行コンテキストの作成 ; void hospac_cre_cnt_asm( ; T_HOSPAC_CTXINF *pk_ctxinf, /* 作成するコンテキスト */ ; VP sp, /* スタックポインタ */ ; void (*task)(VP_INT), /* 実行アドレス */ ; VP_INT exinf /* 実行時パラメータ */ ; ) ; ----------------------------------------------- _hospac_cre_cnt_asm: push.l er2 mov.l #ctx_entry,er2 ; 実行エントリーポイントの設定 mov.l er2,@-er1 mov.l @(12,er7),er2 ; 実行時パラメータの取り出し mov.l er2,@-er1 ; 実行時パラメータ格納 (er2の退避分) mov.l @(8,er7),er2 ; 実行アドレス取り出し mov.l er2,@-er1 ; 実行ドレスを格納 (er3 の退避分) sub.l #12,er1 ; er4〜er6 までの退避分 mov.l er1,@(0,er0) ; スタックポインタの格納 pop.l er2 rts |
![]() |
hospac_cre_cntによる新しいコン テキストの作成動作(H8/300H版) |
実行アドレスと実行時パラメータをコンテキスト情報から復帰されるer2,er3の 位置に入れておき、「戻り番地」としてctx_entryを指定している。rtsで ctx_entryに飛ぶと、er2をer0にコピー=実行時パラメータが正しく第一引数 が収まるべきレジスタに収まり、er3が指し示すアドレスへジャンプ= task( exinf)とほぼ等価な状態になる。
ほぼ等価の意味する心は…スタックに安全な戻り番地が積まれていない点が task(exinf)と異なるのだ。だが、それはどうでもいいことなのだ。何故なら ば、taskとは*決してreturnしない*関数だからだ。では終了時にはどうなるの かというと、他のタスクにスイッチするか、それがなければカーネルのアイド ルコンテキストにスイッチすることで、無事にその寿命を全うする仕組みになっ ているのだ。
これでこいつもほぼ理解した(つもりになった)。もう十分。
SH4では32bitの即値をレジスタに直接ロードする事が出来ない事と、jmpも遅 延分岐だということ以外は、もう説明の必要はないだろう。
! ----------------------------------------------- ! 実行コンテキストエントリーアドレス ! ----------------------------------------------- ctx_entry: jmp @r8 ! 実行アドレスにジャンプ mov r9,r4 ! 実行時パラメータを第一引数に設定 ! ----------------------------------------------- ! 実行コンテキストの作成 ! void hospac_cre_cnt_asm( ! T_HOSPAC_CTXINF *pk_ctxinf, /* 作成するコンテキスト */ ! VP sp, /* スタックポインタ */ ! void (*task)(VP_INT), /* 実行アドレス */ ! VP_INT exinf /* 実行時パラメータ */ ! ) ! ----------------------------------------------- _hospac_cre_cnt_asm: add #-20,r5 ! r14-10分コンテキストのstackを伸ばす mov.l r7,@-r5 ! 実行パラメータの格納(r9) mov.l r6,@-r5 ! 実行アドレスの格納(r8) mov.l .ctx_entry_addr,r0 ! 実行エントリポイントをコンテキストの mov.l r0,@-r5 ! スタックに保存 rts mov.l r5,@(0,r4) ! コンテキストのスタックポインタ保存 .align 4 .ctx_entry_addr: .long ctx_entry |
![]() |
hospac_cre_cntによる新しいコン テキストの作成動作(SH4) |
ああ、Cの仕様(関数呼出におけるレジスタ保存の規則と引数渡し)とスタック 操作の命令、サブルーチンコール/リターン命令の動作を知っているだけで、 Real Time OSのポートが出来てしまった…いや、確かにまだ例外処理が実装さ れてないんで、実用性とかなんとかって言われるとアレなんですけど。