【解説】12ステップで作る組込みOS自作入門 7thステップ
宣言通り、『12ステップで作るOS自作入門』の解説記事を書いていきます。
分かりづらいところを質問していただければ回答するつもりです。
もちろん間違っているところの指摘も大変助かります。
7thステップ「割込み処理を実装しよう」はあまり難しくありません。
(個人差はあるかもしれませんが)
目次
7thステップ解説内容
8thステップに向けて、主に以下の2点のみまとめます。
- 全体の流れ (8thステップに比べれば驚くほどシンプルです。)
- アセンブリ言語で何が書かれているか
(8thステップではシステムコールが登場します。
システムコールは割込みをうまく使って汎用レジスタなどを
退避/復元しているので流れだけでも理解しておくことが重要です。)
全体の流れ
シーケンス図を使って説明します。
まずは簡単に登場人物 (オブジェクト) の紹介です。
- main : main関数。
語弊を恐れずに書くと、8thステップでいうところの
単一のスレッド (1つのmain関数) で動いている状態です。 - ISR (Interrupt Service Routine) : 割込みハンドラ。
- IRQ (Interrupt ReQuest) : 割込み要求
IRQからISRへの線は「CPUに割込みが通知されて割込み処理が開始した」と解釈してください。
(注) シーケンス図上の注釈の色の意味は
黄色の注釈「実行される処理の補足説明」
緑色の注釈「その他の補足説明」
となっています。
シーケンス図はPlantUMLを使用して描いています。*1
アセンブリ言語で何が書かれているか
解説するのはintr_serintr関数 (シリアル送受信割込みで呼ばれる関数) です。
大きくわけて以下の4つの処理があります。
0. プログラムカウンタとCCRの退避
いきなり腰を折るようですみません。
ここはアセンブラ言語で書かれていません。
割込みが発生したらCPUが勝手にやってくれます。
H8/3069Fではプログラムカウンタ (24bit) とCCR (8bit) を
スタックポインタ (1単位32ビット) 上に積みます。(下方伸長)
1. 汎用レジスタの退避
汎用レジスタ (ER0~ER6) をスタック上にコピーして積みます。
コードでは以下になります。
_intr_serintr:
mov.l er6,@-er7
mov.l er5,@-er7
mov.l er4,@-er7
mov.l er3,@-er7
mov.l er2,@-er7
mov.l er1,@-er7
mov.l er0,@-er7
ここまででプログラムカウンタ、CCR、汎用レジスタ (ER0~ER6) の退避が
完了している状態となります。
これで割込みが発生する直前の処理を再開するために必要な情報が
すべて退避できました。
なお、汎用レジスタはER7までありますが、書籍にも書かれている通り
ER7はスタックポインタという特殊な意味を持っています。
「汎用レジスタを退避するならスタックポインタも
退避しなければならないのではないか?」
と思う方がいるかもしれません。
ステップ7ではスタックポインタを書き換えたりすることはないので
退避する必要がありません。
(書き換えるなら退避する必要があります。)
2. 割込みハンドラ呼び出し
関数呼び出し時にはER0が第1引数、ER2が第2引数となる仕様です。
下記のコード上の記載により適切な引数を準備した上でinterrupt関数が呼び出されます。
細かいことを言い始めるとアセンブリ言語の仕様にも言及しないと
いけなくなるのでC言語でいう関数呼び出しと考えてください。
(どうしても気になるのであればご自身で調べてください。
CASL IIとかがアセンブリ言語を一から学ぶのにはオススメです。)
mov.l er7,er1
mov.w #SOFTVEC_TYPE_SERINTR,r0
jsr @_interrupt
3. 汎用レジスタの復元
interrupt関数を抜けると
処理1の「汎用レジスタの退避」で逃がしたER0の値が
スタックポインタの一番上に来ている状態になります。
詳しい原理は割愛しますがコンパイラがそうなるように
機械語に変換してくれています。
(どうしても原理が気になる人はCASL IIとかで
CALL命令 (サブルーチン呼び出し) あたりを勉強してください。)
下記のコードが実行されると
汎用レジスタ (ER0~ER6) を割込み発生直前の値に戻せます。
mov.l @er7+,er0
mov.l @er7+,er1
mov.l @er7+,er2
mov.l @er7+,er3
mov.l @er7+,er4
mov.l @er7+,er5
mov.l @er7+,er6
4. プログラムカウンタとCCRを復元して処理を再開
RTE命令が実行されると
スタックポインタの一番上に積まれている値を使って
プログラムカウンタとCCRが復元されます。
割込みが発生したらプログラムカウンタとCCRがCPUによって
勝手に退避されるのでそれを戻すわけですね。
ともあれ、これでプログラムカウンタ、CCR、汎用レジスタ (ER0~ER7) が
元通りに戻ったので処理が続行できるわけです。
(スタックポインタもうまい具合に戻っています。)
rte
あとがき
ステップ7を書くだけで結構なボリュームになってしまいました。
正直ここまで理解できていなくても何となくわかりそうな部分もありますが
自分の知識で書ける分だけ書いてみました。
ちょっと文字が多くなってしまったのが反省点で、
「スタックポインタの動きがイメージできない」とかいうコメントがあれば
図を追加します。(本ブログの閲覧数的になさそうですが)
もしかしたらSlideShareみたいなツールを使った方が読みやすかったでしょうか?
こちらも要望があればチャレンジしてみます。
ステップ8の解説記事は来週末目標に書きます。
正直、スレッドごとのスタックの動きを書いたり、
スレッドの終了の仕方とかをプログラムベースで書いたりすると
来週末じゃ全然時間足りない気がします…。
ステップ8の難しさのポイントはたくさんあると思います。
例を挙げるとこんな感じだと思います (ほとんど実体験です) 。
- あらかじめ登録していた関数をコールバックする部分が多くて頭がついていかない (一番はこれ?)
- スレッドが複数あるがスタック領域が衝突しない理由がわかっていない
- 複数のスレッドがあるが割込みのたびにディスパッチしても問題ない理由がわからない
- レディーキューは実行可能なスレッド一覧なら実行中のスレッド (currentスレッド) が入っている理由がわからない
(それだと"実行可能な"という定義がおかしいのではないか?) - 最初のスレッドを作るときにシステムコールが呼べない理由がわからない
- bootstackとintrstackが同じなのに正しく動作する理由がわからない
私もやりましたが、関数が呼ばれる順番を書き出していって
(関数を抜けて戻っていく順番も書いた方がベターです)
どのようにプログラムが動いているか理解するのが一番良いとは思います。
(難しいステップなので疑問点はメモしておいた方がいいです。)