Operating Systems Development Series
Kernel: Basic Concepts Part 1
by Mike, 2008, 2009

はじめに

ようこそ!:)

さて...ついにオペレーティングシステムの最も重要な部分までたどり着きました。カーネルです。

この用語は、このシリーズを通して、これまで何度も耳にしてきました。その理由は、この用語がいかに重要であるかということです。

カーネルは、すべてのオペレーティングシステムの中核をなすものです。カーネルが何であるか、そしてそれがオペレーティングシステムにどのような影響を与えるかを理解することは重要です。

このチュートリアルでは、カーネルの背後にあるもの、カーネルとは何か、そしてカーネルが何を担っているのかを見ていきます。 これらの概念を理解することは、良い設計を考え出す上で不可欠です。

準備はいいですか?

カーネル基本的な定義

OSのカーネルとは何かを理解するために、まず「カーネル」とは何か、その基本的な定義を理解する必要があります。辞書では、「カーネル」は「核心」、「本質的な部分」、あるいは「何かの本体」と定義されています。この定義をオペレーティング・システムの環境に適用すると、次のように簡単に述べることができます。

カーネルは、オペレーティング システムの中核となるコンポーネントです。

しかし、これは私たちにとってどのような意味を持つのでしょうか。OSのカーネルとは一体何なのか、なぜ気にする必要があるのか。

カーネルが必須であるという決まりはありません。カーネル」がなくても、特定のアドレスにプログラムをロードして実行することは簡単にできる。実際、初期のコンピュータシステムはすべてこの方法で始まっています。現代のシステムでも、この方法を採用しているものがあります。この顕著な例は、初期のカウンタゲームシステムで、そのゲーム機用に設計されたゲームの1つを実行するために、システムを再起動する必要がありました。

では、カーネルは何のためにあるのでしょうか?コンピュータ環境では、プログラムを実行するたびに再起動するのは非現実的です。ということは、各プログラム自体にブートローダが必要になり、ハードウェアを直接制御することになる。結局のところ、起動時にプログラムを実行する必要があるのなら、オペレーティングシステムというものは存在しないことになります。

必要なのは、複数のプログラムを実行する機能を提供し、そのメモリ割り当てを管理する抽象化レイヤです。また、OSなしで起動時に各プログラムを起動しなければならない場合、ハードウェアに対する抽象化を提供することができます。結局のところ、ソフトウェアは生のハードウェアの上で動いているのです。

キーワードは「抽象化」です。

カーネルの必要性

カーネルは、ハードウェア自体に対する主要な抽象化レイヤーを提供します。カーネルが通常Ring 0にあるのは、まさにこの理由からです。私たちはまだRing 0にいるので、すでにこのことを経験しています。

これはいいとして、他のソフトはどうするんだ?私たちはオペレーティング環境を開発していることを思い出してください。私たちの第一の目標は、アプリケーションや他のソフトウェアが安全に、かつ効果的に実行できる環境を提供することです。もし、すべてのソフトウェアをカーネルと一緒にリング0で動作させれば、カーネルは不要になりますね?もしそうだとしたら、リング0のソフトがリング0のカーネルと衝突して、予想外の結果を引き起こすかもしれません。結局のところ、彼らは皆、システムの全バイトを完全に制御しているのです。どんなソフトウェアでも、カーネルを上書きすることができるし、他のソフトウェアも何の問題もなく上書きすることができるのです。痛そう。

とはいえ、それは問題の始まりに過ぎない。プログラムやプロセスを切り替える共通基盤がないため、マルチタスクやマルチプロセシングは不可能だ。一度に実行できるのは1つのプログラムだけである。

基本的な考え方は、カーネルは必要不可欠であるということです。他のソフトウェアがすべてを直接制御するのを防ぐだけでなく、そのための抽象化レイヤーを作りたいのです。

カーネルが他のシステムのどこに、どのように位置づけられるかを理解することは、非常に重要なことです。

ソフトウェアの抽象化レイヤー

ソフトウェアには多くの抽象化があります。これらの抽象化はすべて、実装の詳細を隠すだけでなく、そこからあなたを守るために、コアと基本的なインターフェースを提供することを目的としています。すべてを直接コントロールすることは、一見クールに見えますが、そうすることでどれだけの問題が発生するか想像してみてください。

私が言っているのはどんな問題なのか、興味を持たれたかもしれません。エレクトロニクスは、私たちが指示したとおりに動くのが基本です。私たちは、ソフトウェアをハードウェアのレベルまで、場合によってはエレクトロニクスのレベルまでコントロールすることができます。これらのレベルでミスを犯すと、それらのデバイスに物理的なダメージを与える可能性があります。

ここでは、各抽象レイヤーを見て、私が言いたいことを理解し、カーネルがどこに位置するかを確認しましょう。

PModeの保護リングレベルとの関係

ブートローダ 3 チュートリアルでは、アセンブリ言語のリングを詳しく見てきました。また、これがプロテクトモードとどのように関係するのかも見てきました。

Ring 0 のソフトウェアが最も低い保護レベルであることを忘れないでください。これは、私たちがすべてを直接制御し、決してクラッシュしないことを期待されていることを意味します。もしRing 0のプログラムがクラッシュした場合、システムも一緒にクラッシュしてしまいます(Triple Fault)。

このため、他のすべてを直接制御できないようにするだけでなく、ソフトウェアを実行するために必要な保護レベルのみを与えるようにします。このため、通常は

  • カーネルはRing 0(「スーパーバイザーモード」)で動作します。
  • デバイスドライバは、ハードウェアデバイスに直接アクセスする必要があるため、Ring 1とRing 2で動作します。
  • 通常のアプリケーションソフトはRing3(「ユーザーモード」)で動作します。
さて、これらはどのように組み合わされるのでしょうか?もう少し詳しく見てみましょう。

レベル1:ハードウェアレベル

これは実際の物理的なコンポーネントです。マザーボード上のマイクロコントローラチップが、他のデバイス上のマイクロコントローラに低レベルのコマンドを送り、このデバイスを物理的に制御しているのです。どのように?それはレベル2で説明します。

ハードウェアの例としては、マイクロコントローラチップセット(「マザーボードチップセット」)、ディスクドライブ、SATA、IDE、ハードディスク、メモリ、プロセッサ(これはコントローラでもある--詳細はレベル2を参照)などがあります。

これは最も低いレベルであり、純粋な電子機器であるため最も詳細です。

レベル2:ファームウェアレベル

ファームウェアは、エレクトロニクス・レベルの上位に位置するものです。各ハードウェア機器やマイコンが必要とするソフトウェアが含まれています。ファームウェアの一例として、BIOSのPOSTがあります。

プロセッサはコントローラであり、他のコントローラと同様にファームウェアに依存していることを忘れないでください。プロセッサ内の命令デコーダは、1つの機械命令をマクロコードに分解するか、直接マイクロコードに分解します。

詳細については、チュートリアル7「システムアーキテクチャのチュートリアル」を参照してください。

マイクロコード

ファームウェアは通常、マイクロコードを用いて開発され、マイクロアセンブラで組み立てて記憶領域にアップロードするか(BIOS POSTなど)、さまざまな手段で機器の論理回路にハードウエアとして組み込まれる。

マイクロコードは通常、EEPROMのようなROMチップに格納されています。

マイクロコードは非常にハードウェアに特化したものです。新しい変更や改訂があるたびに、新しいマイクロコード命令セットとマイクロアセンブラを開発する必要があります。システムによっては、マイクロコードは回路内の個々の電子ゲートやスイッチを制御するために使われている。そう、それほど低レベルなのだ。

マクロコード

マイクロプロセッサやCPUのような複雑なシステムでは、マイクロコードは非常に低レベルであり、開発が困難な場合があります。また、コードだけでなく、マイクロプログラムも変更するたびに再インプリメンテーションしなければなりません。

このため、マイクロコードの上にマクロコードと呼ばれるより高度な言語を実装しているシステムもある。 マクロコードは抽象化されているため、マイクロコードよりも変更頻度が低く、移植性が高い。 また、抽象化されているため、より簡単に作業することができる。

しかし、まだ非常に低レベルです。より高度な機械語をマイクロコードに変換するための内部ロジック命令セットとして使用され、命令デコーダによって変換される。

レベル3:リング0 - カーネルレベル

ここが私たちの到達点です。Stage 2 ブートローダは、カーネルが実行できる環境を整えることだけに専念していました。

デバイスドライバやアプリケーションソフトウェアと、ハードウェアが使用するファームウェアの間を抽象化するのがカーネルである。

レベル4:リング1、リング2 - デバイスドライバ

デバイスドライバは、カーネルを経由してハードウェアにアクセスする。デバイスドライバは、特定のマイコンを直接制御する必要があるため、自由度が高く、コントロールしやすいことが必要です。しかし、コントロールしすぎると、システムがクラッシュする可能性があります。例えば、GDTを変更したり、独自に設定したりすると、すぐにカーネルがクラッシュしてしまいます。このため、これらのドライバがLGDTを使用して独自のGDTをロードできないようにする必要があります。このため、これらのドライバはリング0ではなく、リング1またはリング2のいずれかで動作するようにします。

例えば、キーボードデバイスドライバはアプリケーションソフトウェアとキーボードマイクロコントローラの間のインターフェースを提供する必要があります。ドライバは、コントローラに間接的にアクセスするためのルーチンを提供するライブラリとしてカーネルにロードされるかもしれません。

標準的なインターフェイスが使用されている限り、すべてのハードウェア依存を隠す限り、非常にポータブルなカーネルを提供することができます。

レベル5:リング3 - アプリケーションレベル

これは、ソフトウェアがある場所です。通常、ドライバはカーネルに直接アクセスすることはありませんが、システムAPIやデバイスドライバのインタフェースを利用します。

まとめ

本シリーズでは、Kernelの開発中にドライバを開発する予定です。これにより、オブジェクト指向を維持し、Kernelの抽象化レイヤーを提供することができます。

このように考えると、私たちがいるのはレベル0であることに気づきます。他のすべてのプログラムはカーネルに依存しています。なぜかというと、カーネルに注目すると......。

カーネル

カーネルはコアコンポーネントであるため、カーネルに依存するすべてのものの管理を行う必要があります。カーネルの主な目的は、システムリソースを管理し、他のプログラムがこれらのリソースにアクセスできるようなインタフェースを提供することです。多くの場合、カーネル自身は他のリソースに提供するインターフェイスを使用することができません。カーネルは、プログラミングの中で最も複雑で困難な作業であると言われています。

このことは、良いカーネルを設計し実装することは非常に困難であることを意味しています。

チュートリアル2では、過去の様々なオペレーティングシステムを簡単に見てきました。このチュートリアルでは、多くの新しい用語を太字にし、チュートリアルの最後にそれらの用語のリストを作りました。このチュートリアルの最後に、そのリストをまとめました。

まず、そのリストをもう一度見て、それがカーネルにどのように関連しているかを見てみましょう。太字のものはすべてKernelで処理されます。

  • メモリ管理
  • プログラム管理
  • マルチタスク
  • メモリ保護
  • 固定ベースアドレス - これはチュートリアル2で取り上げました。
  • マルチユーザ - これは通常、シェルによって実装されます。
  • カーネル- もちろん
  • ファイルシステム
  • コマンドシェル
  • グラフィカルユーザーインターフェイス(GUI)
  • グラフィカルシェル
  • リニアブロックアドレッシング(LBA) -チュートリアル2で説明しました。
  • ブートローダ -完成

上記のいくつかは、カーネルで使用される別のドライバとして実装することができます。例えば、WindowsはNTFSファイルシステムドライバとしてntfs.sysを使用します。

このリストはチュートリアル2で見たことがあるはずです。また、これらの用語のいくつかを取り上げました。太字の用語を見て、それらがカーネルにどのように関連しているかを見てみましょう。また、いくつかの新しい概念についても見ていきます。

メモリ管理

これはおそらく、あらゆるカーネルで最も重要な部分です。ご存知のように、カーネルはスーパーバイザーモード(リング0)なので、メモリの各バイトに直接アクセスできます。これは非常に強力ですが、特にマルチタスク環境では、複数のプログラムやデータがメモリを必要とするため、問題が発生することもあります。

私たちが解決しなければならない主要な問題の1つは、次のようなものです。メモリが足りなくなったらどうするか?

もう一つの問題は、断片化です。ファイルやプログラムをメモリの連続した領域に読み込むことは必ずしも可能ではありません。例えば、2つのプログラムを読み込んだとします。1つは0x0に、もう1つは0x900にあります。どちらのプログラムもファイルをロードするように要求しているので、データファイルをロードします。

ここで何が起こっているかに注目してください。これらのプログラムとファイルの間には、たくさんの未使用メモリがあります。さて、もし上記で収まりきらないような大きなファイルを追加したらどうなるでしょうか?このとき、現在の方式では大きな問題が発生します。現在実行中のプログラムや読み込んだファイルを壊してしまうので、特定の方法で直接メモリを操作することができないのです。

そして、各プログラムがどこにロードされるかという問題がある。各プログラムは、Position Indipendentにするか、再配置テーブルを提供する必要があります。これがないと、プログラムがどのベースアドレスでロードされることになるのかがわからない。

この点について、もう少し詳しく見てみましょう。ORG命令を覚えていますか?このディレクティブは、プログラムがどこからロードされるかを設定します。プログラムを別の場所からロードすると、プログラムは不正確なアドレスを参照し、クラッシュします。この理論を簡単に試すことができます。今、Stage2 は 0x500 でロードされることを期待しています。しかし、Stage1 の 0x400 にロードすると (Stage2 のORG 0x500を維持したまま)、トリプルフォールトが発生することになります。

このため、新たに2つの問題が発生します。プログラムをどこにロードすればいいのか、どうやって判断すればいいのでしょうか?バイナリイメージしかないのであれば、わからない。しかし、すべてのプログラムが同じアドレス、たとえば0x0から始まることを標準にすれば、知ることができる。これは有効ですが、マルチタスクをサポートするつもりなら、実現は不可能です。しかし、各プログラムに独自のメモリ空間を与えて、実質的に0x0から始まるようにすれば、これはうまくいくでしょう。結局のところ、各プログラムから見れば、実際の(物理)メモリでは異なっていても、すべて同じベースアドレスでロードされているのです。

必要なのは、物理メモリを抽象化する方法だ。もっと詳しく見てみましょう。

仮想アドレス空間(VAS)

仮想アドレス空間はプログラムのアドレス空間です。システムメモリとは関係ないことに注意する必要がある。これは、各プログラムが独立したアドレス空間を持つようにするためのものです。このため、あるプログラムが別のプログラムにアクセスすることはできない。

VASは 仮想的なものであり、物理メモリを直接使用しないため、ディスクドライブなどの他のソースをメモリであるかのように使用することができます。つまり、物理的にシステムに搭載されているメモリよりも多くのメモリを使用することができます。

これにより、「メモリが足りない」という問題を解決することができます。

また、各プログラムは独自のVASを使用するため、各プログラムは常にベース0x0000:0000で始まるようにすることができます。これにより、先に述べた再配置の問題や、メモリの断片化も解決され、各プログラムに連続した物理的なメモリブロックを割り当てる心配がなくなります。

仮想アドレスは、カーネルがMMUを介してマッピングします。これについては、もう少し後で説明します。

仮想メモリ概要

仮想メモリは、ハードウェアとソフトウェアの両方によって実装された特別なメモリアドレス指定方式で、連続しないメモリを連続するメモリのように動作させることができます。

仮想メモリは、仮想アドレス空間(Virtual Address Space)の概念に基づいています。各プログラムに独自の仮想アドレス空間を提供し、メモリ保護とメモリの断片化を防ぎます。

仮想メモリは、ハードディスクに保存されたページファイルを使用することで、システム内に実際に存在する以上のメモリを間接的に使用する方法を提供します。

仮想メモリはハードウェアレベルで処理されるため、動作させるにはハードウェアデバイスコントローラを介してマッピングされる必要があります。これは通常、MMUを通じて行われます。後ほど説明します。

仮想メモリの使用例を見るために、実際に動作しているところを見てみましょう。

ここで何が起こっているかに注目してください。仮想アドレス内の各メモリブロックはリニアです。各メモリブロックは、実際の物理RAM内の位置か、ハードディスクなどの別のデバイスにマッピングされます。 ブロックは、これらのデバイス間で必要に応じてスワップされます。これは遅く見えるかもしれませんが、MMUのおかげで非常に高速です。

覚えておいてください。各プログラムは、上に示したような独自の仮想アドレス空間を持っています。各アドレス空間はリニアで、0x0000:00000から始まるので、メモリの断片化やプログラムの再配置に関する問題の多くを解決することができます。

また、仮想メモリはメモリブロックを使用する際に異なるデバイスを使用するため、システム内のメモリ量以上を容易に管理することができます。もしメモリが足りなくなったら、このページファイルを必要に応じて増やすか、警告やエラーメッセージを表示することができます。

各メモリ「ブロック」は「ページ」と呼ばれ、通常4096バイトの大きさになっています。

繰り返しになりますが、詳細は後ほど説明します。

メモリ管理ユニット(MMU)。概要

この用語はどこかで聞いたことがあるような気がしますが、どうでしょう?)

MMUは、また、ページメモリ管理ユニット(PMMU)として知られているマイクロプロセッサ内部のコンポーネントは、CPUが要求するメモリの管理に責任がある。仮想アドレスから物理アドレスへの変換、メモリ保護、キャッシュ制御など、さまざまな役割を担っている。

セグメンテーション。概要

セグメンテーションは、メモリ保護の手法の一つです。セグメンテーションでは、現在実行中のプログラムから特定のアドレス空間だけを確保します。これは、ハードウェアレジスタを通じて行われます。

セグメンテーションは、最も広く使用されているメモリ保護方式の1つである。x86では、通常、セグメントレジスタによって処理される。CS、SS、DS、ESです。

Real Modeでは、このレジスタが使用されています。

ページング概要

これは私たちにとって重要なことです。ページングとは、RAMにない仮想メモリページへのプログラムのアクセスを管理するプロセスです。これは後で詳しく説明します。

プログラム管理

ここで、リングレベルが重要になります。

ご存知のように、KernelはRing 0で、アプリケーションはRing 3にあります。これは良いことで、アプリケーションが特定のシステムリソースに直接アクセスするのを防ぐことができるからです。しかし、アプリケーションはこれらのリソースを必要とするため、これは悪いことでもあります。

プロセッサがどのように現在のリングレベルを知り、どのようにリングレベルを切り替えるのか、気になるところです。 プロセッサは単純に内部フラグを使って現在のリングレベルを保存しています。では、プロセッサはどのリングでコードを実行すべきかをどのようにして知るのでしょうか。

ここで、GDTとLDTが重要になります。

ご存知のように、Real Modeではプロテクションレベルがありません。プロテクトモードに入る前に、GDTを設定しなければならないことを思い出してください。また、32ビットモードに入るために、ファージャンプを実行する必要があったことを思い出してください。ここでは、これらが非常に重要な役割を果たすので、より詳細に説明しましょう。

スーパーバイザーモード

リング0はスーパーバイザーモードとして知られています。このモードでは、すべての命令、レジスタ、テーブル、その他、より高いリングレベルを持つ他のアプリケーションがアクセスできない特権的なリソースにアクセスできます。

リング0はカーネルレベルとも呼ばれ、絶対に失敗しないことが期待されています。もしRing 0のプログラムがクラッシュすると、システムも一緒にダウンしてしまいます。覚えておいてください。「大きな力には大きな責任が伴う」これがプロテクトモードの主な理由です。

Supervisor Modeは、システムレベルのソフトウェアによって変更可能なハードウェアフラグを利用します。システムレベルのソフトウェア(リング0)にはこのフラグが設定され、アプリケーションレベルのソフトウェア(リング3)には設定されません。

Ring 0のコードにしかできないこと、Ring 3のコードにはできないことがたくさんあります。チュートリアル7で出てきたフラグレジスタを覚えていますか?RFLAGSレジスタのIOPLフラグはIN命令やOUT命令など、特定の命令を実行するのに必要なレベルを決定します。IOPLは通常0なので、Ring0プログラムのみがソフトウェアポート経由でハードウェアに直接アクセスできることを意味します。このため、頻繁にRing 0に戻す必要があります。

カーネルスペース

カーネルスペースとは、カーネルとRng 0デバイスドライバのために予約された特別なメモリ領域を指します。 ほとんどの場合、カーネルスペースは仮想メモリのようにディスクにスワップアウトしてはいけません

OSがユーザー空間で動作する場合、「ユーザーランド」と呼ばれることが多い。

ユーザースペース

これは通常、Ring 3のアプリケーションプログラムです。各アプリケーションは通常、独自の仮想アドレス空間(VAS)で実行され、異なるディスクデバイスからスワップすることができます。各アプリケーションは独自の仮想メモリ内にあるため、他のプログラムのメモリに直接アクセスすることはできません。そのため、Ring 0のプログラムを経由してアクセスする必要があります。これはデバッガに必要なことです。

アプリケーションは通常、最も特権のないものです。このため、通常、システムリソースにアクセスするには、リング0のカーネルレベルのソフトウェアにサポートを要求する必要があります。

保護レベルの切り替え

必要なのは、これらのアプリケーションがシステムに対してこれらのリソースを問い合わせることができるようにする方法です。しかし、これを行うには、リング3ではなく、リング0である必要があります。このため、プロセッサの状態をRing 3からRing 0に切り替えて、アプリケーションがシステムを照会できるようにする方法が必要です。

チュートリアル5で、アセンブリ言語のリングについて説明したのを思い出してください。プロセッサは次のような条件で現在のリングレベルを変更することを思い出してください。

  • far jump、far call、fat retなどの指示命令。
  • INT、SYSCALL、SYSEXIT、SYSENTER、SYSRETURNなどのトラップ命令。
  • 例外
つまり、アプリケーションがシステムルーチンを実行するには(Ring 0に切り替わりながら)、ファージャンプするか、割り込みを実行するか、SYSENTERなどの特別な命令を使用しなければならないのです。

これは素晴らしいことですが、プロセッサはどのリングレベルに切り替わるかをどのように知るのでしょうか?ここでGDTの出番です。

GDTの各ディスクリプタで、各ディスクリプタのリングレベルを設定する必要があったのを覚えていますか?今回のGDTでは、2つのディスクリプタを用意しました。それぞれ、Kernel Mode Ring 0用です。

このGDTに、Ring3アクセス用のモードディスクリプタを2つ追加するだけです。これがユーザ空間です。

もう少し詳しく見てみましょう。

チュートリアル8で、重要なバイトはアクセスバイトであることを思い出してください。このため、バイトパターンをもう一度説明します。

  • ビット0(GDTではビット40)。アクセスビット(仮想メモリで使用)。仮想メモリは使わないので(まだ、とにかく)、無視することにします。従って、0になります。
  • ビット1(GDTのビット41):読み取り/書き込み可能なビットです。このビットは(コードセレクタのために)設定されているので、セグメント(0x0から0xFFFFまで)のデータをコードとして読み、実行することができる。
  • ビット2(GDTのビット42):「拡張方向」ビットです。これについては、後で詳しく見ていきます。今のところ、無視してください。
  • ビット3(GDTのビット43):プロセッサに、これがコードまたはデータ記述子であることを伝えます。(セットされているので、コード・ディスクリプタです。)
  • ビット4 (GDTのビット44)。システム "または "コード/データ "ディスクリプタとして表現します。これはコードセレクタなので、ビットに1がセットされています。
  • ビット5〜6(GDTのビット45〜46):特権レベル(すなわち、リング0またはリング3)である。リング0にいるので、両ビットは0です。
  • ビット7(GDTのビット47)。セグメントがメモリ内にあることを示すために使用されます(仮想メモリで使用されます)。まだ仮想メモリを使用していないため、今は0に設定する
;******************************************* ; Global Descriptor Table (GDT) ;******************************************* gdt_data: ; Null descriptor (Offset: 0x0)--Remember each descriptor is 8 bytes! dd 0 ; null descriptor dd 0 ; Kernel Space code (Offset: 0x8 bytes) dw 0FFFFh ; limit low dw 0 ; base low db 0 ; base middle db 10011010b ; access - Notice that bits 5 and 6 (privilege level) are 0 for Ring 0 db 11001111b ; granularity db 0 ; base high ; Kernel Space data (Offset: 16 (0x10) bytes dw 0FFFFh ; limit low (Same as code)10:56 AM 7/8/2007 dw 0 ; base low db 0 ; base middle db 10010010b ; access - Notice that bits 5 and 6 (privilege level) are 0 for Ring 0 db 11001111b ; granularity db 0 ; base high ; User Space code (Offset: 24 (0x18) bytes) dw 0FFFFh ; limit low dw 0 ; base low db 0 ; base middle db 11111010b ; access - Notice that bits 5 and 6 (privilege level) are 11b for Ring 3 db 11001111b ; granularity db 0 ; base high ; User Space data (Offset: 32 (0x20) bytes dw 0FFFFh ; limit low (Same as code)10:56 AM 7/8/2007 dw 0 ; base low db 0 ; base middle db 11110010b ; access - Notice that bits 5 and 6 (privilege level) are 11b for Ring 3 db 11001111b ; granularity db 0 ; base high
ここで何が起こっているかに注目してください。すべてのコードとデータは同じ範囲の値を持っており、唯一の違いはリングレベルの違いです。

ご存知のように、プロテクトモードは現在の特権レベル(CPL)を保存するためにCSを使用します。初めてプロテクトモードに入るとき、Ring0に切り替える必要がありましたが、CSの値が無効だったため(リアルモードより)、GDTからCSに正しいディスクリプタを選択する必要があります。詳しくは、チュートリアル8をご覧ください。

このため、CSに新しい値をアップロードする必要があり、ファージャンプが必要でした。リング3ディスクリプタに遥かにジャンプすることで、実質的にリング3状態に入ることができます。

ご存知のように、INT、SYSCALL/SYSEXIT/SYSENTER/SYSRET、far call、または例外を使用して、プロセッサをRing 0に戻すことが可能です。

これらのメソッドについて詳しく見ていきましょう。

システムAPI概要

このプログラムは、システムリソースにアクセスするためにシステムAPIに依存しています。ほとんどのアプリケーションはシステム API を直接、またはC ランタイムライブラリのような言語 API を通して参照します。

システムAPIは、システム・コールを通じて、アプリケーションとシステム・リソースとの間のインタフェースを提供します。

割り込み

ソフトウェア割り込みは、ソフトウェアで実装された特別なタイプの割り込みです。割り込みは非常に頻繁に使用され、割り込み記述子テーブル(IDT)という特別なテーブルを使用することになります。割り込みはカーネルに実装される最初のものなので、後でもっと詳しく見ます。

Linuxでは、すべてのシステムコールにINT 0x80を使用しています。

割り込みは、システムコールを実装するための最もポータブルな方法です。このため、システムルーチンを呼び出す最初の方法として割り込みを使用することにします。

コールゲート

コールゲートは、Ring 3 アプリケーションがより私的な(Ring 0、1、2)コードを実行するための 方法を提供します。コールゲートは、Ring 0 ルーチンと Ring 3 アプリケーションの間のインターフェイスで、通常カーネルによって設定されます。

コールゲートは、FAR CALLへの単一のゲート(エントリポイント)を提供します。このエントリポイントはGDTまたはLDTの中で定義されます。

コールゲートを理解するには、例を挙げるとわかりやすいでしょう。

;******************************************* ; Global Descriptor Table (GDT) ;******************************************* gdt_data: ; Null descriptor (Offset: 0x0)--Remember each descriptor is 8 bytes! dd 0 ; null descriptor dd 0 ; Kernel Space code (Offset: 0x8 bytes) dw 0FFFFh ; limit low dw 0 ; base low db 0 ; base middle db 10011010b ; access - Notice that bits 5 and 6 (privilege level) are 0 for Ring 0 db 11001111b ; granularity db 0 ; base high ; Kernel Space data (Offset: 16 (0x10) bytes dw 0FFFFh ; limit low (Same as code)10:56 AM 7/8/2007 dw 0 ; base low db 0 ; base middle db 10010010b ; access - Notice that bits 5 and 6 (privilege level) are 0 for Ring 0 db 11001111b ; granularity db 0 ; base high ; Call gate (Offset: 24 (0x18) bytes CallGate1: dw (Gate1 & 0xFFFF) ; limit low address of gate routine dw 0x8 ; code segment selector db 0 ; base middle db 11101100b ; access - Notice that bits 5 and 6 (privilege level) are 11 for Ring 3 db 0 ; granularity db (Gate1 >> 16) ; base high of gate routine ; End of the GDT. Define the routine wherever ; The call gate routine Gate1: ; do something special here at Ring 3 retf ; far return back to calling routine
上記はコールゲートの一例です。

コールゲートを実行するには、GDT 内の記述子コードからオフセットします。これは、jmp 0x8:Stage2命令と似ていることに注意してください。

; execute the call gate call far 0x18:0 ; far call--calls our Gate1 routine
コールゲートは、最近のオペレーティングシステムではあまり使われません。その理由の1つは、ほとんどのアーキテクチャがコールゲートをサポートしていないためです。また、FAR CALL命令とFAR RET命令を必要とするため、かなり遅いです。

GDTが保護されたメモリにないシステムでは、他のプログラムが独自のコールゲートを作成し、その保護レベルを上げる(そしてリング0アクセスを得る)ことも可能です。 セキュリティ上の問題があることも知られています。例えば、Windowsオペレーティングシステムに独自のコールゲートをインストールするGurongは注目すべきワームの1つです。

SYSENTER/SYSEXITの命令

これらの命令は Pentium II 以降の CPU に導入されました。最近のAMDプロセッサの一部もこれらの命令をサポートしています。

SYSENTERは、どのアプリケーションでも実行できます。SYSRETはRing 0のプログラムだけが実行できる。

これらの命令は、ユーザーモード(リング3)からプリビレッジモード(リング0)へ制御を移し、素早く戻すための高速な方法として使用されます。これにより、ユーザモードからシステムルーチンを高速かつ安全に実行することができます。

これらの命令は、モデルスペシフィックレジスタ(MSR)に直接依存しています。MSRとRDMSRおよびWRMSR命令については、チュートリアル7を参照してください。

SYSENTER

SYSENTER命令は、以下のレジスタをMSR内で定義された位置に自動的に設定します。

  • CS = IA32_SYSENTER_CS MSR + the value 8
  • ESP = IA32_SYSENTER_ESP MSR
  • EIP = IA32_SYSENTER_IP MSR
  • SS = IA32_SYSENTER_SS MSR

この命令は、Ring 3コードからRing 0に制御を移すためにのみ使用されます。 起動時には、これらのMSRを、すべてのシステムコールのためのシスコールエントリーポイントとなる開始位置を指すように設定する必要があります。

ここでは、SYSEXITについて説明します。

SYSEXIT

SYSEXIT命令は、次のレジスタを MSR 内で定義された場所に自動的に設定します。

  • CS = IA32_SYSENTER_CS MSR + the value 16
  • ESP = ECX Register
  • EIP = EDX Register
  • SS = IA32_SYSENTER_CS MSR MSR + 24

SYSENTER/SYSEXITの使い方

さて、これらの命令の使い方は複雑に見えるかもしれませんが、それほど難しいものではありません;)

SYSENTER と SYSEXIT は呼び出す前にMSR をセットアップする必要があるため、まずこれらの MSR を初期化する必要があります。

IA32_SYSENTER_CSはMSR内のインデックス0x174、IA32_SYSENTER_ESPは0x175、IA32_SYSENTER_IPは0x176であることを思い出してください。チュートリアル7を覚えていますか?

これを知った上で、SYSENTERのためにそれらを設定しましょう。

%define IA32_SYSENTER_CS 0x174 %define IA32_SYSENTER_ESP 0x175 %define IA32_SYSENTER_EIP 0x176 mov eax, 0x8 ; kernel code descriptor mov edx, 0 mov ecx, IA32_SYSENTER_CS wrmsr mov eax, esp mov edx, 0 mov ecx, IA32_SYSENTER_ESP wrmsr mov eax, Sysenter_Entry mov edx, 0 mov ecx, IA32_SYSENTER_EIP wrmsr ; Now, we can use sysenter to execute Sysenter_Entry at ring 0 from either a Ring 0 program or Ring 3: sysenter Sysenter_Entry: ; sysenter jumps here, is is executing this code at prividege level 0. Simular to Call Gates, normally we will ; provide a single entry point for all system calls.
sysenterを実行するコードがリング3であり、Sysenter_Entryが保護レベル0である場合、プロセッサはSYSENTER命令内でモードを切り替えます。

上記のコードでは、両方ともプロテクションレベル0であるため、プロセッサはモードを変更せずにルーチンを呼び出すだけです。

このように、SYSENTER.indexとSYSEXITを呼び出す前に行わなければならない作業が少しあります。

SYSENTERとSYSEEXITは移植性がありません。このため、SYSENTER/SYSEXITと並行して、より移植性の高い別の方法を導入することが賢明です。

SYSCALL / SYSRET命令

[近々、ここにSYSCALLとSYSRETのセクションを追加する予定です]。

エラー処理

プログラムが問題を起こした場合、どうすればいいのでしょうか?その問題が何であるか、どのように対処すればよいかを知るにはどうしたらよいでしょうか。

通常、これは例外処理によって行われます。無効な命令や0による除算などによってプロセッサが無効な状態になると、プロセッサは割り込みサービスルーチン(ISR)をトリガします。もし、私たち自身のISRをマッピングしていれば、私たちのルーチンが呼び出されます。

呼び出されるISRは、問題が何であったかに依存します。これは素晴らしいことで、問題が何であるかが分かっているので、元々問題を起こしたプログラムを探してみることができます。

この方法の1つは、プロセッサ時間を与えた最後のプログラムを取得することです。これは、ISRを生成したプログラムであることが保証されています。

プログラムの情報を得たら、エラーを出力するか、プログラムをシャットダウンさせるか、どちらかの方法があります。

IRQは、プロセッサ内部のPIC(Programmable Interrupt Controller)によってマッピングされる。IRQはプロセッサ内部のPICによってマッピングされ、IDT(Interrupt Descriptor Table)内の割り込みエントリにマップされます。これはカーネル内部で最初に取り組むことなので、後ですべてをカバーすることになります。

まとめ

このチュートリアルでは、カーネル理論、メモリ管理の概念、仮想メモリアドレス指定(VMA)、プログラム管理、リング0とリング3の分離、アプリケーションとシステムソフトウェア間のインターフェースなど、様々な概念について見てきました。ふーっ。という感じでしょうか?

このチュートリアルのコンセプトの多くは、あなたにとって新しいものかもしれません。このチュートリアルは、カーネルに関連するすべての基本的な概念をカバーする、「Get your feet wet」チュートリアルのようなものです。

このチュートリアルはカーネルがしなければならないことの表面をかすったに過ぎません。しかし、これはスタート地点です。)

次のチュートリアルでは、カーネルを別の角度から見ていきます。また、いくつかの新しい概念をカバーし、カーネルの設計と実装について話します。その後、CとC++で動作するコンパイラとツールチェインの構築を開始する予定です。面白そうでしょう?

私は現在、私のカーネルに MSVC++ 2005 を使っています。

また、マルチタスク、TSS、ファイルシステムなど、ここで見ていない他の概念も仕上げる予定です。楽しくなりそうです ;)