
先週まで10日ほど入院、頭の中もすっかりリセットされてしまいました。何やってたんだっけ状態。「ぐだぐだ」の投稿再開すべしと思い至ったものの、過去回を見てみればアレマ「とってもメンドクセー」あたりです。まあ、リハビリ兼ねてゆるゆると復活を目指すしかないみたいです。というわけで今回はコールゲートね。なんだそれ。
※「ぐだぐだ低レベル プログラミング」投稿順indexはこちら
※実機動作確認(といってもエミュレータなんだけれども)には以下を使用させていただいております。
コール・ゲート
前回やっていたことを見返してみると、ようやく各種のデスクリプタどもを列挙し、その先ということでプロテクトモード下の制御転送(JMP、CALL、RET命令など)に入るぞ、と予告してました。一番メンドイところに差し掛かったところで入院してしまったわけね。間が悪いぜ。
プロテクト・モード下の制御転送をバッサリ2分してしまうと、「メンドクセーやつ」と「普通のやつ」に2種類に分かれます(リアル・モードも似たようなもんだけど。)
-
- メンドクセーやつ、セグメント間(インター・セグメント)
- 普通のやつ、セグメント内(イントラ・セグメント)
です。32ビット以降で「リニア・アドレス」のみを使っている現状の多くのシステムは後者の「イントラ」の世界で「オフセット・アドレスのみ」で済む素直な(ちょろい?)世界です。前者の「インター」の世界は32ビット以降では内部に隠されているのです。デスクリプタがモロミエになるのは16ビットのプロテクトモードばかりということになります。
リアルモードでのインター・セグメント・ジャンプは、「セグメント:オフセット」という組でのジャンプでした。プロテクトモードでもその建付けは変りませぬ。ただしプロテクトモードでの「セグメント」値は物理アドレスとは無関係なセレクタ値であり、デスクリプタ・テーブルを参照するためのインデックスでしかなく、間接的にベースアドレス、リミットを指定するところが異なります。よって、「普通」に実行中のコードセグメントから別のコードセグメントにジャンプして~という場合、セグメント(セレクタ)とオフセットを引数とするJMP命令なりCALL命令を発行すればリアル・モードの時と同様にジャンプすることができます。裏側ではセグメント・セレクタをインデックスにメモリ上のデスクリプタテーブルへのアクセスがおこり、プロセッサ内のデスクリプタ・キャッシュ・レジスタへのベースアドレスなどのロードが起こるのですが、これはプログラマからは直接見えない「オートマッチック」な動作デス。そのベースアドレスに命令の引数のオフセット・アドレスが足し合わされて「飛ぶ」ということになります。
ただし「普通」という条件がヤヤコシいです。実行中のコードセグメントの特権(カレント・プリビレッジ・レベル、CPL)が、飛び先のコードセグメントの特権(DPL)と「等しい」という縛りがあるからです。コードセグメントを直接のターゲットにJMPしたりCALLしたりできるのは同一レベルの特権を持つ実行可能なセグメントのみであると。
特権レベルが異なる場合は、相手の特権が上でも下でも呼び出し禁止です。なんていったって特権レベル毎に異なるスタック・セグメントが割り当てられていることを思い出しましょう。同じレベルであればスタックの入れ替え不要です。しかし、特権が変わると入れ替え必須です。コードセグメントをターゲットとする単なるインターセグメントのJMPやCALLではスタックの入れ替えまではやってもらえません。
しかし、OSとかデバドラとか考えると、より高い特権のサービスルーチンに何か仕事をお願いして~ということが必ずあります。そのため、
コールゲートというデスクリプタを介して制御転送先を呼び出す
仕組みが存在します。コールゲートもまたデスクリプタなので、どちらかのデスクリプタ・テーブルに用意しておく必要があります。コールゲートの場合、以下の縛りがかかります
-
- コール・ゲート・デスクリプタ自体に特権が設定されており、カレントの特権レベルがそれより偉く(数値は小さい)ないと呼び出せない。
- 一方、コール・ゲート・デスクリプタが呼び出す先のコード・セグメントの特権レベルはカレントよりも偉く(数値は小さく)ないとダメ。
ちょっと込み入ってますが、これにより「特権チェック」を効かせたうえで、特定の特権の高いルーチンへの遷移を可能としとるわけです。さらに縛りは続きます。
-
- コール・ゲート・デスクリプタ内に制御先頭先のコード・セグメントの開始アドレス(オフセット)が書き込まれている。呼び出し元が勝手なオフセット・アドレスに飛びこみて~などと思っても許してくれない(そんなことしたら何かよからぬことをされてしまうから。)そこに書かれているアドレスに必ず飛ぶ。
- 特権の遷移にともないスタックセグメントとスタックポインタは自動的に切り替えられる(前々回出てきたタスク・ステート・セグメント内に保持されているスタック設定を使う。勿論、今の値も自動でセーブされる。)スタックを切り替えるだけだと、引数パラメータなどをスタック渡しすることができないので、コール・ゲート・デスクリプタ内に記述したカウント数だけ、自動でパラメータをスタック間コピーしてくれる。至れり尽くせり?
このような仕組みで、特権の低い方(数字は大)から高い方(数字は小)への遷移が実現されます。
一方、特権のあるサービス・ルーチンからの戻りは「セグメント間」RET命令で可能。しかし、この際も以下のような縛りが適用されます。
-
- 必ず特権高い方から低い方に戻る
- 先ほどの逆でスタックは自動で切り替えられる
- DSの値とESの値は無効化される
制御転送なので、当然CSにはカレント・セグメントが設定されます。また、特権遷移にともなう自動的なスタック切替でSSも設定されます。しかし、RET前に使っていたDSやESの値は、特権の高いメモリ・セグメントの値を保持している可能性が高いです。そのまま戻ると特権の低い呼び出し元に保護すべきものが見えてしまう可能性があります。そこでDSとESのセレクタ値に0が強制代入されます。値0はデスクリプタの0番ではなく、「ヌル」セレクタという特殊なセレクタです。ヌルセレクタを使ってメモリアクセスしようとすると、例外が起こります。よゐこは、RETから戻ったらDS、ESに自分で適正な値をロードしないとなりません。
分かったような、分からぬような。。。頭のリハビリになっているのか?
