
前回まで8086になく80186/80286で拡張されている命令の練習を実施。カテゴリの残りまだあるのですが、INS、OUTSは練習するのがメンドイです。一方他は8086でもあった命令にオペランドを追加したものどもで花がないっす。そこで今回から80286にて「追加されたもの」プロテクト・モードへ入りたいと思います。
※「ぐだぐだ低レベル プログラミング」投稿順indexはこちら
※実機動作確認(といってもエミュレータなんだけれども)には以下を使用させていただいております。
286プロテクト・モード
現在のパソコンで稼働しているx86(x64)は、まず間違いなく「プロテクト・モード」で動作しており、8086や80186のころの「リアル・モード」は使われてません(奥深いところに残ってはいる筈だけれども。)
初代の「プロテクト・モード」を登載したのは誰よ、と問えば、答えは80286です。16ビットの時代の末尾を飾るマシンであります。しかし、何度か書きましたが、286の「プロテクト・モード」はx86の黒歴史であるかのごとく奥底に封印されとります(まあ、リアルモード同様奥深いところに残っている筈だけれども。最近のマシンでは復活させると呪いがかかるという噂?まあ、ペナルティってことね。)
8086と80186に存在せず80286に存在する命令、というのは全てプロテクト・モードのための命令群です。そいつらをちょいと触ってみるためには、プロテクト・モードの背景知識が必須。そのため今回はメンドクセー、プロテクトモードのメモリアクセスについておさらい。
セグメント・レジスタへのロード
同じオペコードの命令の動作が、リアル・モードとプロテクトモードで異なるものがあります。その多くが「セグメント・レジスタ」の操作を伴う命令どもです。例えば、
MOV DS, AX
上記は、AXレジスタの内容16ビットを、DSセグメントレジスタに転送する命令です。レジスタ値の表面だけみていたらリアル・モードとプロテクト・モードでその動作に違いはありません。
しかし、命令の裏側で起こっていることを眺めると、大きく違うことが分かります。
リアルモードでは、セグメント・レジスタに書きこんだ値は、物理メモリアドレスの上位16ビット(パラグラフなどと呼ばれることあり)そのものです。メモリにアクセスする場合には、セグメント・レジスタのLSB側に4ビットの0の下駄を履かせて、命令どもが生成するオフセットアドレス(16ビット幅)と加算することで、実メモリのアドレス20ビットを得ています。
リアルモードでは、特権がどうのとか、仮想記憶がどうのとかいうことはまったくありません。また、セグメントの大きさは最大64Kバイト、アセンブラ上はセグメントの大きさは小さくも「書け」ますが、実際のメモリアクセスではお隣のセグメントに突き抜けるようなオフセットアドレス使っても何も起きません。アクセスし放題。
一方プロテクト・モードでは、セグメント・レジスタにロードする値そのものが異なります。値は同じ16ビット幅ですが、セグメント・セレクタと呼ばれ、MSB側の13ビットはインデックスです。メモリ上合計8K個のデスクリプタと呼ばれる「構造」がならんだテーブルから、どのデスクリプタかを指定するための数値です。そしてデスクリプタを保持するテーブルには「グローバル」と「ローカル」の2種類があり。セグメント・セレクタのビット2は「グローバル」か「ローカル」かを識別するためのビットです。なぜ2つのテーブルがあるかについては次回以降で述べる予定の「タスク」というものまでお待ちくだされ。そしてビット1、0も後で触る予定ですが、特権(保護)に関するビットです。
リアル・モードであれば、セグメント・レジスタに新たな値がロードされたといってもドタバタ走り回る必要はないのですが、プロテクト・モードは異なります。ロードされたセレクタの値を使って、メモリ上に展開されている筈のデスクリプタ・テーブルを読みに行くことになります。
デスクリプタの内部にはざっくり3種のフィールドあり
-
- ARバイト
- ベースアドレス(24ビット)
- リミット(16ビット)
ARバイトには、デスクリプタの型(いろいろある)や特権レベルのビット以外に、セグメントの在不在を示すビットも含まれてます。ここに「不在」と書きこまれていると「まだ主記憶メモリ上には読み込まれていない(仮想記憶のファイルシステム内のどこかにある筈)」ということで、例外が発生し、「セグメント・スワップ」のハンドラが起動される、という仕組みに繋がります。OSが対応していれば、セグメントベースでの仮想記憶が構築できると。当時としてはカッケー機能だったな。いまじゃ誰も使ってないケド。
ベースアドレス部こそが、リアルモードのセグメントの値に相当する部分です。80286はアドレス線が24本、16Mバイトの主記憶だったので、その全てをカバーできるというわけです。24ビット、ケチクセーとか言ってはなりません。IBMのベストセラー大型機ですら16Mバイトの時代です。マイコンが猪口才な。
ここでテクあり、メモリアクセスの度にメモリ上のデスクリプタを読み出していると、メモリばかり読んでいて何もできませぬ。新たなセグメント値をロードするときに、メモリ上のデスクリプタを読み取って、オンチップのフツーは見えないレジスタ(デスクリプタ・キャッシュ)にロードしておくのです。よって、それ以降、実際にアドレス生成するときにはこのベースアドレスにオフセットを加えれば良いです。8086と同じじゃん。
一方、セグメンテーションといいつつ、8086ではしり抜けだったセグメントの大きさのチェックは、リミットと呼ばれるフィールドがデスクリプタ中に設けられたことで解決されてます。オフセット値はベースアドレスと加算されるだけではなく、同時にリミット値と比較もされてます。そしてリミット値を「越えてる」と判断されると例外発生。8086の時のように、お隣のセグメントにちょっかいかけるようなコードは書けませぬよし。ただし、リミットは最大64K、セグメントの最大サイズは8086も286も変わりませぬ。この制約が取り払われるためには80386を待たねばならず。
案の定というか、メモリアクセスのところだけで1回分使ってしまいました。まだ、ジャンプとかコールとか例外とかをハンドルするためのメンドクセー仕組みもあり、そいつらは次回だあ~。