"WindowsOS内部のアーキテクチャのすべて"の第4章の要約、読書メモとなります。
本記事では以降、"WindowsOS内部のアーキテクチャのすべて" を "Windows OS Internal Architecture" として "WOIA" と略します。
1982年、アドレスバスを24bitに増やし、プロテクトモードを搭載した 80286が発表される。
プロテクトモードやセグメントディスクリプタによるアドレス指定、CPUの動作レベル(「特権」「リング0」~「リング3」)についても80286が発祥となっており、まずはIntel 80286の機能について簡単に紹介する。
続いてWindows3.xを取りあげ、80286上でどのようにプロテクトモードを活用したかをまとめる。
80286のスペック:
アドレスバス | 24bit |
汎用レジスタ | 16bit |
外部データバス | 16bit |
内部データバス | 16bit |
bit幅に注目する限り、アドレスバスが24bitに増えた以外は8086と変わらないように見える。
しかし80286は以下の2大機能が登場した事で、続く80386の基礎を作った重要なCPUである。
24bit(16MB)のアドレス幅全てを使うにはプロテクトモードを使う必要がある。プロテクトモードでは、後述のセグメントディスクリプタを使ったアドレス指定方式により、bit幅の組み合わせ上は(=論理上は)1GBまでのアドレスを16MBの物理メモリに割り当てることが可能になっている。
プロテクトモードでのメモリや割り込みベクタ管理に必要な"ディスクリプタテーブル"(Descriptor Table)が登場したのも80286からである。
さらに、プロテクトモードで扱うデータの随所で「Privilege Level」を導入することでCPUの動作レベルを4段階に分け、アクセス出来るメモリや実行可能な命令を動作レベル毎に制限することが可能になった。これこそが「プロテクト」と呼ばれる所以である。
なお、80286ではリアルモードからプロテクトモードへ移行することは出来たがその逆、プロテクトモードからリアルモードへ戻ることは出来なかった(リセットする必要があった)。
以下、Web上で入手出来た80286(M80C286)のデータシートに沿いつつ、汎用レジスタの構成や上記新機能の概要と関連する命令セットなどを簡単に紹介していく。
基本的に8086/8088のレジスタ構成と同じである。全て16bitで構成されている。
プロテクトモードやタスク切り替え機能の追加に伴い、"MSW"(Machine Status Word)レジスタが追加されている。
+---------+ +---------+ | AH | AL |=AX : ACCUMULATOR | S P | STACK POINTER +---------+ +---------+ | BH | BL |=BX : BASE | B P | BASE POINTER +---------+ +---------+ | CH | CL |=CX : COUNT | S I | SOURCE INDEX +---------+ +---------+ | DH | DL |=DX : DATA | D I | DESTINATION INDEX +---------+ +---------+ +---------+ +---------+ | C S | CODE SEGMENT | I P | INSTRUCTION POINTER +---------+ +---------+ | D S | DATA SEGMENT | FLAG | STATUS FLAGS +---------+ +---------+ | S S | STACK SEGMENT | MSW | MACHINE STATUS WORD +---------+ +---------+ | E S | EXTRA SEGMENT +---------+
MSWレジスタは次のような構成になっている。
15 3 2 1 0 +----+- -+----+----+----+----+ MSW: | | ... | TS | EM | MP | PE | +----+- -+----+----+----+----+ TS : Task Switch EM : Processor Extnsion Emulator MP : Monito Processor Extension PE : Protection Enable
タスク切り替えではTSビットを使い、プロテクトモードへ切り替えるにはPEビットを1にする。初期状態ではPEビットは0、つまりリアルモードで動作するようになっている。
MSWレジスタを読み書きするために、以下の命令が追加されている。
LMSW : Load Machine Status Word from register/memory SMSW : Store Machine Status Word
その他、プロテクトモードで使われる「ディスクリプタテーブル」のアドレスを格納する為の内部レジスタが複数追加されているが、これらは専用の命令を使って操作するようになっている。
初期状態、つまりリアルモードでのアドレス指定は8086/8088と同じである。
16bitのセグメントレジスタ(CS, DS, SS, ES)を4bit左シフトしてセグメントアドレスとし、オフセットアドレスと組み合わせる。
CS, SSレジスタは特定のポインタレジスタと組み合わせる。
CS : Code Segment → IPと組み合わせる。 SS : Stack Segment → SP, BPと組み合わせる。BPからの相対アドレスを指定する時に使われる。
DS, ESレジスタについてはメモリ上のデータを指定する時に汎用的に使われる。
このように大きく「実行コード」「データ」「スタック」の3種類にメモリ領域を分割するCPUの機能は、プログラムの構成にも影響を及ぼしている。特にOSが実行可能ファイルをロードしてメモリ上に展開する際に、実行可能ファイルのフォーマットおよびその解析・展開処理でかならずこの3種類のメモリ領域が関わってくる。
リアルモードでのアドレス指定では、8086/8088と同様にアドレスバスを20bitまでしか使えない。つまり指定可能な物理メモリの上限は1MBのままである。
CPUが関わるメモリレイアウトについては、80286起動時点ではリアルモードであり、8086/8088のメモリレイアウトと同一である。
0 - 3FFhまでが割り込みベクタとしてCPU予約領域となり、FFFF0hがCPUリセット後に最初にフェッチされる命令のアドレスとなる。
プロテクトモードを利用することで、割り込みベクタ領域を任意の領域に指定することが可能となった。(もちろん、プロテクトモードでも従来通り0 - 3FFhを割り込みベクタ領域として指定出来る)
80286ではアクセスコントロールを強化する為に"特権レベル"(Privilege Level)という仕組みが導入されている。
メモリアクセスや実行アドレスの切り替え(JMP, CALL, 割り込み)において、CPUの現在の特権レベルと、アクセスするメモリやジャンプ先のアドレスの特権レベルを調べ、適切なアクセスコントロールを行う。また実行出来る命令の種類についても、現在の特権レベルで実行可能な命令かチェックされるようになる。
80286以降では次の4つのレベルが存在する。
レベル0 : "KERNEL"レベル レベル1 : "SYSTEM SERVICE"レベル レベル2 : "OS EXTENSIONS"レベル レベル3 : "APPLICATIONS"レベル
"レベル"は"リング"と呼ばれる時もあり、「リング0」~「リング3」と呼ばれる場合もある。
レベルの数が小さいほど使える機能が多く、アクセス権限も強くなる。レベル0ではCPUの全機能を利用出来る。ハードウェアやCPUの機能の全てにアクセスするOSのカーネル部分は、一般的にレベル0で動作する。ユーザープログラムは一般的にレベル3で動作する。
レベルの数が大きい特権レベルは、それより数の小さい特権レベルの機能・メモリにはアクセス出来ない。レベル3の状態では、レベル0のメモリにはアクセス出来ない。その逆は可能である。
80286以降のプロテクトモードでは、メモリアドレスが出てくるところでは必ず、後述の「セグメントディスクリプタ」が使われる。セグメントディスクリプタの中にはそのメモリセグメントの特権レベルを表す2bitのフィールドが存在する。メモリアクセスやJMP/CALL/割り込みによりジャンプする場合には、CPUによりセグメントディスクリプタ中の特権レベルが自動的にチェックされることになる。
プロテクトモードにおいても、セグメントレジスタでセグメントを指定し、オフセットを加える仕組みはリアルモードと同じである。しかしセグメントレジスタによるセグメントの指定方式が大きく異なる。
セグメントディスクリプタは64bit=8バイトで構成され、以下のようなbitフィールドになっている。
+--------------+--------------+ 7 | (RESERVED) | 6 +--------------+--------------+ 5 |P|DPL|S|TYPE|A|BASE (23 - 16)| 4 +--------------+--------------+ 3 | BASE (15 - 0) | 2 +--------------+--------------+ 1 | LIMIT (15 - 0) | 0 +--------------+--------------+ 15 7 0
これを必要とするセグメントの分だけ用意した、セグメントディスクリプタの一覧が「セグメントディスクリプタテーブル(Segment Descriptor Table)」である。セグメントディスクリプタテーブルは、リアルモードの段階で用意しておき、その先頭アドレスをLGDT(Load Global Descriptor Table)/LLDT(Load Local Descriptor Table)によりCPUに登録しておく。
LGDT/LLDT命令では、セグメントディスクリプタの領域をBASE+LIMIT形式で指定したメモリ領域を指示する。
LGDT命令の例(LLDTの場合も指定するメモリ領域の構造は同じ) +--------------+--------------+ 5 | (RESERVED) | BASE (23-16) | 4 +--------------+--------------+ 3 | BASE (15 - 0) | 2 +--------------+--------------+ 1 | LIMIT (15 - 0) | 0 ------> LGDT +--------------+--------------+ | 15 7 0 | | メモリ: | | (...) | | +---+------+<------+ | | | DT-n | | | | G +------+ | +---[ CPU ]-----------|---------+ | | ... | | | | | | D +------+ | | V | | | DT-2 | | | +--------------------------+ | | T +------+ +------| GDT LIMIT (16bit = 64KB)| | | | DT-1 | | +--------------------------+ | +---+------+<-------------| GDT BASE 24bit物理メモリ | | | (...) | | +--------------------------+ | +-------------------------------+
CS, DSなどのセグメントレジスタでは、プロテクトモードにおいてはセグメントディスクリプタテーブル上のインデックス番号を使ってセグメントディスクリプタを指定する。セグメントディスクリプタを選択することから、プロテクトモードにおけるCS/SSなどのセグメントレジスタのことを「セレクタ(Selector)」呼ぶ場合もある。
セレクタの16bitフィールドは、次のような構造になっている。
+-----------+----+-----+ | INDEX | TI | RPL | +-----------+----+-----+ 15 3 2 0
以下に、CS(Code Segment)でINDEXが"2"を示した時の、セグメントセレクタとGDTの関係を示す。
| (...) | +---+------+ | | DT-n | | G +------+ | | ... | Segment Selector (ex: CS, Code Segment) | D +------+ +-----------+----+-----+ | | DT-2 |<-------| |1|0| TI | RPL | | T +------+ +-----------+----+-----+ | | DT-1 | 15 3 2 0 +---+------+ | (...) |
Segment SelecotrのINDEXサイズは13bit、つまり8192個のセグメントディスクリプタを指定出来るのは前述の通りである。
従って、一つのディスクリプタテーブルには8192個のセグメントディスクリプタが格納出来る。ディスクリプタは一つにつき8バイトなので、一つのディスクリプタテーブルの最大サイズは
8192個 x 8バイト = 65536 = 64KB
となり、丁度LGDT/LLDTで指定可能なディスクリプタテーブルの最大LIMIT値(16bit)と同じになる。
「1GBを16MBの物理メモリにマッピングできる」という表現は、単純に
セグメントセレクタの INDEX フィールド → 13bit 同 TIフィールド → 1bit オフセット → 16bit
の計30bit幅だけ「セグメントセレクタ:オフセット」のbit幅があるので、"論理上は" 1GBを表現出来る、という意味である。
これまで紹介したのは、主にCS/SS/DS/ESで使われるセグメントを指定する場合のディスクリプタテーブルの構造である。
80286ではこの他に、割り込みハンドラやタスク切り替えで使われる"System Segment Descriptor", "Gate Descriptor"というディスクリプタが存在する。割り込みベクタ用に"Interrupt Descriptor Table"を登録することも出来る。これらのディスクリプタテーブルの操作用に"LIDT(Load Interrupt Descriptor Table)"命令などが追加されている。
これらについては、"WOIA"の要約である本記事では取り扱わない。これら割り込みやタスク切り替えに使われる特殊なディスクリプタテーブルについては、OS開発やアセンブラプログラミングの話題に属するからである。
一点だけ注意しておくとしたら、プロテクトモードでのソフトウェア割り込み(Gate Descriptor)を使うと特権レベルを変更出来る事だろう。リング3で動作していたプログラムが、ソフトウェア割り込み(INT命令)をかけることでリング0に切り替わり、OSカーネルの機能を実行することができる。80286以降のプロテクトモードに対応したOSでは、基本的にこの仕組みで「システムコール」を実現している。
アドレスバスが24bitになり、プロテクトモードが導入され、セグメント自体は1MBを超えて16MBまでに配置することが出来るようになった。
しかしオフセットを指定する汎用レジスタ群が16bitのままだったため、80286の時点でも「一セグメントあたり64KBの壁」は存在している。
これは80286対応のWindows 3.xのメモリ管理方式に影響を与えている。
以上で80286プロテクトモードに関する必要な予備知識はまとめ終えた。いよいよ、1980年代後半~1990年代前半の流れとからめてWindows 3.xの内部構造を俯瞰していく。
1980年代後半~1990年代前半は、8086/8088/80286/80386(1988年発売)搭載マシンが同時に存在した時代であり、OSもDOS/Windows2.x/Windows3.xが矢継ぎ早に発表されると同時に次々と新バージョンが投入、なおかつ旧バージョンもまだまだ現役というめまぐるしい時代だった。1987年には386DXを搭載した IBM Personal System/2 Model 80 (8580-xx) が発売されている。
なお、Windows3.x以前まではMS-DOSの拡張製品として販売されていた為、MS-DOSとWindowsをそれぞれ購入する必要があった。
1989年発表。Windos 2.10のマイナーアップデート版。
ここでWindows 2.x系列について少しだけ整理しておく。
1990年発表。CPUに応じた三つのモードで動作するようになっていた。
HIMEM.SYSがXMS仕様2.0に対応。EMM386.SYS導入。
Windows 2.x/3.x/95に至まで、基本的にはまずDOSが立ち上がり、その後Windowsが起動する仕組みになっている。この場合、HIMEM.SYSはDOSカーネルをExtended Memory(HMA含む)以降にロードするだけの役割である。しかしEMM386についてはプロテクトモードのメモリを操作する為、Windows3.0のStandard Modeと競合してしまう。このためEMM386.SYS/EMM386.EXEはWindowsを使う時は無効化する必要があった。
参考:
以降はWOIAに沿ってWindows3.xの構造を追っていくが、こうしてWindows2.xまで俯瞰してみると、Windows/386をベースにWindows3.xが出来たようにも見えてくる。協調型マルチタスクや、仮想86機能によるDOSアプリのマルチタスク機能についてはWindows/386がベースとなっているようだ。
Windows 3.xまではMS-DOSと別で販売されていた。このため、パソコンを起動して最初に立ち上がるのはあくまでもリアルモードのMS-DOSであった。
Windowsを立ち上げるには、MS-DOSのコマンドプロンプトから"WIN.COM"を実行する。
WIN.COMが実行されると、CPUが80286の場合はDOSX.EXEというプログラムを実行し、CPUが80386以上の場合はWIN386.EXEが実行される。以降は80286用である"DOSX.EXE"について話を進める。
MS-DOS -> "WIN.COM" --+-- 80286 --> "DOSX.EXE" ---> 286版のWindows 3.xが起動 | +-- 80386 --> "WIN386.EXE" -> 386版のWindows 3.xが起動
WIN.COMまではCPUはMS-DOS、つまりリアルモードで動作している。DOSX.EXEが実行されると、その内部でPEビットをONにし、プロテクトモードに移行する。同時に各種セグメントディスクリプタテーブルを作成・登録し、プロテクトモードでのメモリ管理の準備を整える。
なお、Windows全般(Win3.x/9x/NT/2000以降含む)でシステム起動時にセグメントディスクリプタテーブルを作成し、登録している。
DOSX.EXEは引き続きメモリ上に常駐するが、その特権レベルは0、つまりリング0で動作することになる。
Windows 3.x起動完了後のメモリレイアウトは次のようになっている。
+---------------------------+ 16MB | Win16アプリケーション領域 | +---------------------------+ | Windows 3.x システム | | (DOSX.EXE, 他) | +---------------------------+ 1MB | VRAM/ROM/BIOS領域 | +---------------------------+ 640KB | (ユーザ領域) | | Win16API | | (KRNL286,USER,GDI) | +---------------------------+ | MS-DOSシステム領域, | | 割り込みベクタ | +---------------------------+ 0KB
Windows 3.xになり、これまでのソフトウェア割り込みによるシステムコールに代わり、"Win16 API"という仕組みが広く使われるようになった。以降、WindowsアプリケーションはOSの機能を使うにはこの "Win16 API" (プログラミングレベルでは、Microsoftが提供するライブラリ関数として呼び出す)を使うことになる。
Win16APIに話を進める前に、DPMIについて簡単に触れておく。
1984年頃からメモリ不足を解消する為に様々な技術仕様とプログラムが開発された。当初はバンク切り替え方式の専用メモリカードを使ったExpanded Memoryとそのデバイスドライバが使われていたが、1984年の時点で既に80286を搭載したIBM PC ATが発売されており、翌1985年には80386が発表されている。つまりプロテクトモードによる1MBを超えるメモリアクセスが技術的には可能となっていた。1987年、80286以降のプロテクトモードでアクセス出来るメモリを使う為の技術仕様、EMS 4.0 (Lotus, Intel, Microsoft, AST合同)が策定される。このころからプロテクトモードの機能を使ってDOSを拡張する、"DOS extender"と呼ばれるタイプのソフトウェアが広まっていく。
しかし80386以降の仮想86機能を使ったEMSでは、既存の"DOS extender"と共存させる上で幾つかの問題が発生した。
Wikipediaより抜粋:
これらの問題を解決して、仮想86機能によるEMSとDOS extenderを共存させる為の技術仕様が、 Quarterdeck Office Systems と Phar Lap Software, Inc. の間で策定された。
これが1989年発表の "Virtual Control Program Interface", VCPI, Version 1.0 である。EMS 4.0 規格の int 67h ファンクションコールを拡張する形でEMSマネージャーにVCPI サーバが実装された。
しかしVCPIを使ったプログラムではプロテクトモードでもリング0で動いてしまうという問題があったり、翌1990年に発表されたWindows 3.0の386 enhancedモードではVCPIがサポートされなかった事情もあり、Windows 3.0で実装されたDPMIに押される形でVCPIは廃れていってしまう。
"DOS Protected Mode Interface"(DPMI)とは、DOSプログラムがプロテクトモードで動作し、プロテクトモードで提供される機能を利用する為の技術仕様である。
1989年にドラフト(草案)が発表され、1990年にVersion 0.9が発表される。同年、DPMI 0.9を実装したWindows 3.0が発表される。
1991年に0.9で不足していた部分を解消したVersion 1.0が発表されたが、Microsoftは対応していない。そのため、多くのDPMIベースのDOS extenderもVersion 0.9に準拠した。
VCPIはINT 61hをベースに拡張しているが、DPMIはINT 31hを中心として拡張しており、仕様的には別系統に属する。
DPMIは基本的に仮想86機能を使ってDOSアプリケーションをプロテクトモードで動作させるが、部分的にプロテクトモードで動作させることも仕様としては可能だった。そのため「DPMI = 仮想86機能」というのは若干不正確に思われる。
またDPMIでは「クライアント・サーバー」という図式がより明確化された。例えば、DPMI 0.9の仕様書には次のような図が掲載されており、Client/Serverという関係を強く印象づけている。
Figure 1. Application/Extender/Client/Host/OS structure +----------------------------------------------------------+ | | | +----------------------------------------------------+ | | | | | | | Application Code | | | | | | | +----------------------------------------------------+ | | | | +----------------------------------------------------+ | | | Extender Base (including APIs) | | | | -------------------------------------------------- | | | | DPMI | | | | client | | | +------------+ | | | | VCPI | | | | client | | | +------------+ | | | | XMS | | | | client | | | +------------+ | | | | Top-down | | | | client | | | +-------------+ | | | +----------------------------------------------------------+ +------------+ | | | | | |------------+ | | | | DPMI | | | host | VCPI |------------+ | | | | | | | | | |------------| XMS |-------------+ | | EMS | | Top-down | | | | | (Int 15h) | +----------------------------------------------------+ +----------------------------------------------------+ | | | Operating System (e.g. DOS) | | | +----------------------------------------------------+
上図で"OS"部分が"e.g. DOS"とあるとおり、OSとしてDOSだけを想定していたわけではない(事実、最初の実装はWindows 3.0)。
DPMIをコールするアプリケーション(DPMI Client)側からは特にOSを意識することはなく、DPMIを実装したDOS extenderあるいはOS(Windows)がDPMI Server(host)として実際の処理を行う。そう言う意味では、DPMIはプロテクトモードのメモリ管理や機能に特化した仮想環境と呼ぶことも可能である。
参考資料:
Wikipediaショートカット:
"Win16 API"は16bitプログラム用の実装であり、"Win16"という呼称も"Win32"が登場してから付けられたようだ。
Windows 1.0の時代から存在していたらしく、Windows 1.0の時点で "GetMessage()", "GlobalAlloc()" 等のAPIが提供されていたようだ。
しかし、Windows 2.0までは明らかにリアルモードのOSであり、プロテクトモードのメモリ管理や特権レベル機能を活用した構造にはなっていない。
Windows 3.xになって前述のDPMIに沿った形に整理された。
参考資料:
MS-DOSまではCPUのソフトウェア割り込みを使ってMS-DOS.SYSの提供するOSの機能を呼び出していた(「システムコール」or「ファンクションコール」)。
一方のWin16 API、特に80286以降に対応したWindows 3.xにおいては、次のような仕組みになっている。
+--------------------------+ | Win16 API Application |<-- RING-3 +--------------------------+ | V +--------------------------+ | Win16 API (=DPMI Client) |<-- RING-3 | (KRNL286.EXE, USER.EXE, | | GDI.EXE, その他) | +--------------------------+ | [DPMI CALL] | V +--------------------------+ | Windows 3.x Kernel |<-- RING-0 | (=DPMI Server/Host) | | (DOSX.EXE, 他) | +--------------------------+ | V +--------------------------+ | CPU, Hardwares | +--------------------------+
ファイルやネットワーク入出力(KRNL286.EXE), グラフィックコントローラへの描画命令(GDI.EXE)は、OSが直接ハードウェアとやり取りするわけではなく、デバイスドライバが間に入る格好になる。
Windows 3.xまでは、アプリケーションがOSに制御を戻すことでマルチタスクを実現する「協調型マルチタスク」(ノンプリエンプティブ)になっていた。
アプリケーションA~Cが動いていた時、もしもアプリケーションBがハングアップしてしまいOSに制御を戻さなかった場合は、アプリケーションA、Cには永久に制御が渡らなくなってしまう。つまりどれか一つでもハングアップしてしまうと、他のアプリはもとよりOSにも制御が渡らなくなり、システム全体がハングアップしてしまう。
Windows9x以降はOS側が強制的に実行権を取得し、一つのアプリケーションがハングアップしても他のアプリやOSに制御が戻せるような、「プリエンプティブマルチタスク」になっている。
Windows 3.xでは搭載するCPUによって3種類のモードが提供されていた。
80286で動作する。
80386で動作する。
80286においては、プロテクトモードによりセグメントを16MBまでの領域に配置することが出来た。しかしオフセットが16bitのままだったので、1セグメント64KBの壁は存在していた。
これにより、Win16アプリケーションにおいても64KBの壁が存在していた。
問題になったのはKRNL286.EXE/USER.EXE/GDI.EXEも64KBの壁に縛られていた点で、マルチタスクにより複数のWin16アプリケーションが動作する場合、その分だけのリソースがKRNL286.EXE/USER.EXE/GDI.EXEの各64KB内で埋まってしまう。
各64KBの中で複数アプリのリソースをやりくりしなければならなかったため、リソース不足が頻繁に発生した。
このように80286のプロテクトモードを利用した Windows 3.x (Standard Mode)はDPMIを活用したユーザー空間とOS空間の分離モデルを定着させた。しかし64KBの壁により、リソース不足による不安定を招いた。
この後登場する80386により、64KBの壁が打破されると共に仮想86機能によるDOSプログラムの完全な分離が実現される。また、Windows9xにおいてはWindows NTから移植されたWin32APIが導入されると共に、プリエンプティブマルチタスクが実現される。これには80386で登場する仮想メモリ機能が活用されている。