Intel x86 Assembly Language & Microarchitecture
リアル対プロテクトモード
サーチ…
リアルモード
インテルがオリジナルのx86、8086(および8088派生物)を設計したとき、16ビットプロセッサが16ビット以上のアドレスにアクセスできるようにセグメンテーションが含まれていました。コードセグメント( CS )、データセグメント( DS )、エクストラセグメント( ES )およびスタックセグメント( SS )の4つを定義した16ビットのセグメントレジスタに対して、 。
使用するSegment Registerには、命令がCode Segment、 PUSH 、 POPがスタックセグメントを暗示し、単純なデータ参照がデータセグメントを暗示したものが含まれていますが、これは他のSegmentのメモリにアクセスするために上書きできます。
実装は単純でした。すべてのメモリアクセスに対して、CPUは暗黙の(または明示的な)セグメントレジスタを取り、それを左に4つシフトしてから、指定されたアドレスを追加します。
+-------------------+---------+
Segment | 16-bit value | 0 0 0 0 |
+-------------------+---------+
PLUS
+---------+-------------------+
Address | 0 0 0 0 | 16-bit value |
+---------+-------------------+
EQUALS
+-----------------------------+
Result | 20-bit memory address |
+-----------------------------+
これにより、さまざまな手法が可能になりました。
- コード、データ、スタックのすべてに相互にアクセスできるようにする(
CS、DS、SSすべて同じ値を持つ)。 - コード、データ、スタックを互いに完全に分離してください(
CS、DS、SSは4K(またはそれ以上)のすべてが互いに分離しています - それは16で乗算されるので64Kです)。
それはまた、奇妙な重複と奇妙なもののすべての種類を許可しました!
80286が発明されたとき、このレガシーモード(現時点では「リアルモード」と呼ばれていました)がサポートされましたが、「保護モード」(qv)という新しいモードが追加されました。
注目すべき重要なことは、リアルモードでは次のとおりです。
- セグメントレジスタ内に正しい値を入れ、16ビットアドレスにアクセスするだけで、どんなメモリアドレスもアクセス可能でした。
- 「保護」の程度は、プログラマーがさまざまな目的のために異なるメモリー領域を区切り、間違ってデータを誤って書き込むことを困難にし、それを可能にしました。
言い換えれば...まったく保護されていない!
保護モード
前書き
80286が発明されたとき、レガシー8086セグメンテーション(現在は「リアルモード」と呼ばれていました)をサポートし、「保護モード」という新しいモードを追加しました。このモードは、32ビットや64ビットのアドレッシングなど、さまざまな改良が加えられて以来、すべてのx86プロセッサに搭載されています。
設計
保護されたモードでは、シンプルな「アドレスをシフトされたセグメントレジスタ値に追加する」が完全に終了しました。彼らはセグメントレジスタを保持しましたが、アドレスを計算するのではなく、アクセスするセグメントを定義したテーブル(実際には2つのうちの1つ)にインデックスを付けるために使用しました。この定義は、セグメントがメモリ内のどこに(ベースとリミットを使用して)どこに記述されたかだけでなく、どのタイプのセグメント(コード、データ、スタックまたはシステム)であっても、どのタイプのプログラムにアクセスできるか(OSカーネル、 、デバイスドライバなど)。
セグメントレジスタ
各16ビットセグメントレジスタは次の形式をとります:
+------------+-----+------+
| Desc Index | G/L | Priv |
+------------+-----+------+
Desc Index = 13-bit index into a Descriptor Table (described below)
G/L = 1-bit flag for which Descriptor Table to Index: Global or Local
Priv = 2-bit field defining the Privilege level for access
グローバル/ローカル
グローバル/ローカルビットは、アクセスがディスクリプタのグローバルテーブル(驚くべきことにグローバルディスクリプタテーブル、またはGDTと呼ばれる)、またはローカルディスクリプタテーブル(LDT)のいずれにあるかを定義する。 LDTのアイデアは、すべてのプログラムが独自のディスクリプタテーブルを持つことができたことです。OSは、グローバルセットのセグメントを定義し、各プログラムはローカルコード、データ、スタックセグメントの独自のセットを持ちます。 OSは、異なるディスクリプタテーブル間のメモリを管理する。
記述子テーブル
各ディスクリプタテーブル(グローバルまたはローカル)は、8,192個のディスクリプタからなる64Kのアレイであり、それぞれが記述していたセグメントの複数のアスペクトを定義した8バイトのレコードである。セグメントレジスタの記述子インデックスフィールドは8,192の記述子に許されています:偶然ではありません!
ディスクリプタ
Descriptorは次の情報を保持しました。新しいプロセッサーがリリースされたときにDescriptorのフォーマットが変更されましたが、同じ種類の情報がそれぞれ保持されていました。
- ベース
これは、メモリセグメントの開始アドレスを定義したものです。 - 限定
これはメモリセグメントのサイズを定義しました。彼らは決定を下さなければなりませんでした:0x0000のサイズは0サイズなので、アクセスできませんか?または最大サイズですか?
代わりに、彼らは第3の選択肢を選んだ:限界フィールドは、セグメント内の最後のアドレス可能な場所であった。つまり、1つのセグメントを定義することができました。またはアドレスサイズの最大サイズです。 - タイプ
伝統的なコード、データ、スタック(下記参照)など、複数の種類のセグメントがありましたが、他のシステムセグメントも定義されています。- ローカルディスクリプタテーブルセグメントは、アクセス可能なローカルディスクリプタの数を定義します。
- タスクステートセグメントは、ハードウェア管理コンテキストの切り替えに使用できます。
- プログラムがオペレーティングシステムを呼び出すことを可能にする制御された「コールゲート」 - 慎重に管理されたエントリポイントを介してのみ。
- 属性
関連するセグメントの特定の属性も維持された。- 読み取り専用と読み取り/書き込みの比較。
- セグメントが現在存在しているかどうか - オンデマンドメモリ管理が可能かどうか。
- このセグメントにどのレベルのコード(OS vsドライバ対プログラム)がアクセスできるか。
ついに真の保護!
OSが単純なプログラムからアクセスできないセグメント内のディスクリプタ・テーブルを保持していた場合、どのセグメントが定義され、どのメモリが割り当てられ、それぞれがアクセス可能であったかを厳密に管理することができます。プログラムは、それが好きなSegment Registerの値をどんな値でも作ることができますが、 Segment Registerに実際に読み込むことが大事でしたら!... CPUハードウェアは提案されたDescriptorの値が多数のルールのどれかを破ったことを認識します。要求を完了するのではなく、誤ったプログラムを処理できるようにオペレーティングシステムに例外を発行します。
この例外は通常、Microsoft Windowsで有名な一般保護例外の世界である#13でした...(Intelのエンジニアは誰でも迷信に思っていますか?)
エラー
発生する可能性のあるエラーの種類は次のとおりです。
提案された記述子索引が表のサイズよりも大きかった場合。
提案されたディスクリプタがコード、データまたはスタックではなくシステムディスクリプタであった場合、
提案された記述子が要求元のプログラムよりも特権を持っていた場合、
提案されたディスクリプタがNot Readable(コードセグメントなど)としてマークされていても、実行されずにReadであると試行された場合。
提案された記述子が存在しないとマークされた場合。
最後の命令は、プログラムにとって致命的な問題ではないことに注意してください。つまり、OSはフラグに注意し、Segmentを復帰させ、Presentとしてマークし、フォルト命令が正常に進行するようにします。
あるいは、記述子がSegment Registerに正常にロードされた可能性がありますが、それを使って今後のアクセスがいくつかのルールのうちの1つを破りました。
- セグメントレジスタには、GDTの
0x0000ディスクリプタインデックスがロードされました。これはハードウェアによってNULLとして予約されていました。 - ロードされたDescriptorがRead-Onlyとマークされているが、Writeが試みられた場合
- アクセスの一部(1,2,4またはそれ以上のバイト)がセグメントの限界外だった場合。
保護モードへの切り替え
保護モードに切り替えるのは簡単です。制御レジスタに1ビットを設定するだけで済みます。しかし、CPUが手を放り込まずに、プロテクトモードに留まり 、次の作業を知らないためにリセットすることなく、多くの準備が必要です。
つまり、必要な手順は次のとおりです。
グローバルディスクリプタテーブルのメモリ領域は、最低3つのディスクリプタを定義するように設定する必要があります。
- 0番目の
NULLディスクリプタ。 - コードセグメントの別の記述子。
- データセグメントのための別の記述子。
これは、データとスタックの両方に使用できます。
- 0番目の
グローバルディスクリプタテーブルレジスタ(
GDTR)は、この定義されたメモリ領域を指すように初期化する必要があります。GDT_Ptr dw SIZE GDT dd OFFSET GDT ... lgdt [GDT_Ptr]CR0のPMビットを設定する必要があります。mov eax, cr0 ; Get CR0 into register or eax, 0x01 ; Set the Protected Mode bit mov cr0, eax ; We're now in Protected Mode!現在のリアルモード値を削除するには、セグメントレジスタをGDTからロードする必要があります。
jmp 0x0008:NowInPM ; This is a FAR Jump. 0x0008 is the Code Descriptor NowInPM: mov ax, 0x0010 ; This is the Data Descriptor mov ds, ax mov es, ax mov ss, ax mov sp, 0x0000 ; Top of stack!
ただ保護モードにCPUを取得するには、これは最低限であることに注意してください。実際にシステム全体を準備するには、もっと多くのステップが必要です。例えば:
- 上部メモリ領域を有効にして、
A20ゲートをオフにする必要があります。 - 割り込みは確実に無効にする必要がありますが、保護モードに入る前にさまざまなフォルトハンドラを設定して、処理の初期段階でエラーを発生させる可能性があります。
このセクションの元の著者は、プロテクトモードに入ってそれを操作するチュートリアル全体を書いています。
Unrealモード
非現実的なモードは 、インテルとAMDプロセッサの両方がどのようにセグメントを記述するために情報をロードして保存するかという2つの事実を利用します。
プロセッサは、 移動中にフェッチされたディスクリプタ情報をプロテクトモードのセレクタレジスタにキャッシュします。
これらの情報は、セレクタ・レジスタ自体のアーキテクチャ上不可視の部分に格納されます。実モードでは、セレクタ・レジスタはセグメント・レジスタと呼ばれますが、それ以外は同じレジスタ・セットを指定し、不可視部分も持っています。これらの部分は固定値で埋められていますが、ロードされたばかりの値から導出された基底に対してです。
このような見方では、リアルモードはプロテクションモードの特殊なケースに過ぎません。ベースとリミットのようなセグメントの情報は、GDT / LDTなしでフェッチされますが、セグメントレジスタの隠し部分から読み取られます。
保護モードを切り替えてGDTを作成することにより、所望の属性、例えば0のベースおよび4GiBの制限を有するセグメントを作成することが可能である。
このような属性がキャッシュされるセレクタ・レジスタの連続的なロードによって、リアル・モードでスイッチバックし、32ビット・アドレス空間全体にアクセスするセグメント・レジスタを持つことができます。
BITS 16
jmp 7c0h:__START__
__START__:
push cs
pop ds
push ds
pop ss
xor sp, sp
lgdt [GDT] ;Set the GDTR register
cli ;We don't have an IDT set, we can't handle interrupts
;Entering protected mode
mov eax, cr0
or ax, 01h ;Set bit PE (bit 0) of CR0
mov cr0, eax ;Apply
;We are now in Protected mode
mov bx, 08h ;Selector to use, RPL = 0, Table = 0 (GDT), Index = 1
mov fs, bx ;Load FS with descriptor 1 info
mov gs, bx ;Load GS with descriptor 1 info
;Exit protected mode
and ax, 0fffeh ;Clear bit PE (bit0) of CR0
mov cr0, eax ;Apply
sti
;Back to real mode
;Do nothing
cli
hlt
GDT:
;First entry, number 0
;Null descriptor
;Used to store a m16&32 object that tells the GDT start and size
dw 0fh ;Size in byte -1 of the GDT (2 descriptors = 16 bytes)
dd GDT + 7c00h ;Linear address of GDT start (24 bits)
dw 00h ;Pad
dd 0000ffffh ;Base[15:00] = 0, Limit[15:00] = 0ffffh
dd 00cf9200h ;Base[31:24] = 0, G = 1, B = 1, Limit[19:16] = 0fh,
;P = 1, DPL = 0, E = 0, W = 1, A = 0, Base[23:16] = 00h
TIMES 510-($-$$) db 00h
dw 0aa55h
考慮事項
- セグメントレジスタがリロードされると、同じ値であっても、プロセッサは現在のモードに従って隠し属性をリロードします。これは上記のコードが "拡張"セグメントを保持するために
fsとgsを使用する理由です:そのようなレジスタはさまざまな16ビットサービスによって使用/保存/復元される可能性は低くなります。 -
lgdt命令はGDTへのfarポインタをロードせず、代わりに24ビット(32ビットにオーバーライド可能)のリニアアドレスをロードします 。これは近くのアドレスではなく、 物理アドレスです (ページングを無効にする必要があるため)。それがGDT+7c00h理由です。 - 上記のプログラムは、
cs/ds/sstp 7c00hを設定し、位置カウンタを0から開始するブートローダ(MBRの場合、BPBはありません)です。ファイルのオフセットXのバイトは、セグメント7c00hのオフセットXにあり、線形アドレスは7c00h + Xです。 - 保護モードでの短時間の往復にIDTが設定されていないため、割り込みを無効にする必要があります。
- コードは6バイトのコードを保存するためにハックを使います。
lgdtによってロードされた構造体は、... GDT自体にヌル記述子(最初の記述子)に保存されます。
GDTディスクリプタの説明については、 インテルマニュアルボリューム3Aの 3.4.3を参照してください。