コンテキスト周りのポートを考える

  1. コンテキストとは
  2. コンテキストのスイッチ
  3. コンテキストの作成

2002.02.02 記 新井

コンテキストとは

HOS V4をSHで動かすにあたって第一に、そして最低限必要な事は、コンテキス トの作成/スイッチ処理の移植である。なんと言っても、まずはそのコンテキ ストとやらの実体が何なのかを知らなければどうしようもない。

コンテキスト、context [名] 1. (文章などの)前後関係、コンテクスト、文脈 2. (事件等の)周囲の事情、環境、背景〜アンカー英和辞典 学習研究社刊、 ということで、この場合はつまるところ「プログラム実行の環境」を指すこ とになるのだろう。この場合の環境とは?少し乱暴かもしれないが、プロセッ サの全内部状態と言ってしまう。

ここでの全内部状態とは何か。ステータスレジスタ(SR)やプログラムカウンタ (PC)をも含むまるっきり全部である。プロセッサの全内部状態と言っても可だ。

尤も、実際に全レジスタ、全内部状態の保存/復帰を考慮する必要が無い場合 もある。

コンテキストの操作が、サブルーチンコール(とリターン)により発生する場合、 プロセッサによってはコール/リターン命令がPC保存、復帰をスタックを使っ て自動的に行なうので、その種のもの(H8、MC68000等)ではスタックポインタ (SP)さえ適切に保存、復帰されれば、PCについての特別な記述は無用である。

また、C言語によるプログラムならば、関数呼出によって破壊されることが許 容されたレジスタがあるので、それらに関する記述は無用となる。

関数呼出ならSRを考慮する必要がないのは考えるまでもない。

割り込みや何らかの例外処理によって生じるコンテキストの操作なら、言葉通 りの全レジスタの保存が必要になるが、それについては割り込みの実装の段階 まで置いておこう。


コンテキストスイッチとは

取り敢えずこれだけの知識を持って、H4020116.lzh中のH8/300H用プロセッサ 抽象化コンポーネント、src/h8/hospac.srcを見てみよう。

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のポートが出来てしまった…いや、確かにまだ例外処理が実装さ れてないんで、実用性とかなんとかって言われるとアレなんですけど。

この項終り