Operating Systems Development Series
User land
by Mike, 2010

はじめに

ようこそ!

前章では、VFSについて調べ、テキストファイルをロードして表示しました。このVFSを使って、実行可能なプログラムファイルをロードすることもできます。これには、ドライバ、プログラムソフト、共有、ランタイムライブラリなどが含まれます。

この章では、ユーザーランドソフトウェアのサポートに踏み切ります。また、システムAPIとその動作についても見ていきます。

さっそく始めましょう。

保護レベル

アセンブリ言語の輪

カーネルランド

第5章では、アセンブリ言語で使用されるリングの概念について簡単に見てきました。このリングは、異なる保護レベルを表しています。これらの保護レベルはハードウェアの詳細であり、ハードウェアによって実装されます。

リング0で動作するソフトウエアは、最も制御性が高いです。ハードウェアPIO、MMIO、プロセッサのハードウェア制御とテーブル(CPUキャッシュ制御やMMRなど)など、ソフトウェアがより多くのアクションを実行できるようにする特権命令を実行することができる。

特権命令の一覧は第7章に示したが、念のためここにも示しておく。

保護レベル0より大きいソフトウェアが上記の命令を実行しようとすると、プロセッサは保護フォールト(#PF)例外を生成する。

特権レベル命令
命令名 説明
LGDT GDTのアドレスをGDTRにロードする。
LLDT LDTRにLDTのアドレスをロードします。
LTR TRへのタスクレジスタのロード
MOVコントロールレジスタ データをコピーし、コントロールレジスタに格納
LMSW 新しいマシンステータスWORDをロードする
CLTS コントロールレジスタのタスクスイッチフラグのクリア CR0
MOVデバッグレジスタ データをコピーしてデバッグレジスタに格納
INVD キャッシュの無効化(ライトバックなし
INVLPG TLBエントリの無効化
WBINVD キャッシュの無効化(ライトバックあり
HLT プロセッサの停止
RDMSR モデル固有レジスタ(MSR)の読み出し
WRMSR ライト・モデル・スペシフィック・レジスタ(MSR)
RDPMC 性能監視カウンターの読み出し
RDTSC タイムスタンプカウンターの読み出し

カーネルランドや カーネルモードはリング0で動作するソフトウェアのことを指します。

これまでこの連載で書いてきたソフトウェアはすべてカーネルモードのソフトウェアで、カーネルとミニドライバでした。マイクロカーネルとハイブリッドは一般的に、このシリーズで使用しているものより高度なドライバインタフェーススキームを採用しており、ドライバを適切にインストールし、カーネルから完全に分離したユーザモードでドライバを実行できるようになっています。カーネルの一部をユーザーモードにすることも可能で、全ては設計次第です。

システムを最初に起動するときは、BIOSとOSが起動できるようにスーパーバイザーモードで動作しています。

ユーザーランド

リング1〜リング3で動作するソフトウェアは、リング0で動作するソフトウェアよりもマシンを制御しにくい。これはマシンを保護するためで、リング1〜3で動作するソフトウェアが原因でエラーが発生した場合、プロセッサは一般保護(#GP)例外を使ってシステムエグゼクティブまたはカーネルに問題を通知する。

多くのOSでは、カーネルモードとユーザーモードの2モード制を採用している。x86ファミリーは4つの保護モードをサポートしていますが、これらのオペレーティングシステムでは、アーキテクチャ間での移植性を高めるために2つの保護モードのみを使用しています。

これらのOSでは、カーネルモードのソフトウェアはリング0、ユーザーランドのソフトウェアはリング3で実行されるように設計されています。リング1とリング2は使用されません。ドライバソフトウェアは、リング0で動作してハードウェアデバイスにアクセスするか、リング3で提供されるドライバAPIやシステムAPIを使ってハードウェアデバイスと通信することができる。

ユーザーモードソフトウェアはハードウェアデバイスに直接アクセスできないため、システムタスクを完了するためにオペレーティングシステムに通知する必要があります。これには、テキストの表示、ユーザーからの入力の取得、ドキュメントの印刷などが含まれる。これらの機能は、ライブラリやAPIという形でユーザーモードソフトウェアに提供される。これらのライブラリやAPIは、システムAPIと通信する。

システムAPI......この言葉は以前にも目にしたことがありますね。システムAPIについてはもう少し詳しく見ていきますが、ここではユーザーモードについて詳しく見ていきましょう。

リング-1

最近のプロセッサには、ハイパーバイザーのリング0アクセスを可能にする特別な保護レベルを持つものがある。 これは「リング-1」と呼ばれることもある。

ユーザーランドへようこそ

ユーザーモードに入るには、いくつかのステップが必要です。(さあ、簡単だとは思わなかったでしょう:) しかし、それほど悪いことではありません。

ステップ1:グローバルディスクリプターテーブル

まず、Global Descriptor Table (GDT)に戻る必要があります。GDTは、初めてプロテクトモードを設定する際に必要となった、あの大きな醜い構造体です。GDTは、プロセッサの情報を含む8バイトのエントリのリストを含んでいることを思い出してください。GDTのエントリーのビットフォーマットをもう一度見てみましょう(重要な部分は太字にしてあります)。

  • Bits 56-63: Bits 24-32 of the base address
  • Bit 55: Granularity
    • 0: None
    • 1: Limit gets multiplied by 4K
  • Bit 54: Segment type
    • 0: 16 bit
    • 1: 32 bit
  • Bit 53: Reserved-Should be zero
  • Bits 52: Reserved for OS use
  • Bits 48-51: Bits 16-19 of the segment limit
  • Bit 47: Segment is in memory (Used with Virtual Memory)
  • Bits 45-46: Descriptor Privilege Level
    • 0: (Ring 0) Highest
    • 1: (Ring 1)
    • 2: (Ring 2)
    • 3: (Ring 3) Lowest
  • Bit 44: Descriptor Bit
    • 0: System Descriptor
    • 1: Code or Data Descriptor
  • Bits 41-43: Descriptor Type
    • Bit 43: Executable segment
      • 0: Data Segment
      • 1: Code Segment
    • Bit 42: Expansion direction (Data segments), conforming (Code Segments)
    • Bit 41: Readable and Writable
      • 0: Read only (Data Segments); Execute only (Code Segments)
      • 1: Read and write (Data Segments); Read and Execute (Code Segments)
  • Bit 40: Access bit (Used with Virtual Memory)
  • Bits 16-39: Bits 0-23 of the Base Address
  • Bits 0-15: Bits 0-15 of the Segment Limit

やばい、大丈夫.上のDPL(Descriptor Privilege Level)ビットは、そのディスクリプタに使用される特権レベルを表しています。つまり、これらのビットを3に設定することで、実質的にそのディスクリプタをユーザーモードのディスクリプタにすることができるのです。

そこで、最初のステップとして、GDT内に2つの新しいディスクリプタを作成します。1つはユーザーモードデータ用、もう1つはユーザーモードコード用です。これは、i86_gdt_initializeを修正して、ユーザーモード・コードとデータ用に2つの新しいGDTエントリーを追加することで行われます。では、それをやってみましょう。

//! initialize gdt int i86_gdt_initialize () { //! etc... //! set default user mode code descriptor gdt_set_descriptor (3,0,0xffffffff, I86_GDT_DESC_READWRITE|I86_GDT_DESC_EXEC_CODE|I86_GDT_DESC_CODEDATA| I86_GDT_DESC_MEMORY|I86_GDT_DESC_DPL, I86_GDT_GRAND_4K | I86_GDT_GRAND_32BIT | I86_GDT_GRAND_LIMITHI_MASK); //! set default user mode data descriptor gdt_set_descriptor (4,0,0xffffffff, I86_GDT_DESC_READWRITE|I86_GDT_DESC_CODEDATA|I86_GDT_DESC_MEMORY| I86_GDT_DESC_DPL, I86_GDT_GRAND_4K | I86_GDT_GRAND_32BIT | I86_GDT_GRAND_LIMITHI_MASK); // etc... return 0; }

上記のコードは、他のGDTエントリーを作成するときに行ったものと同じですが、1つだけ変更があります。I86_GDT_DESC_DPL フラグに注目してください。 これにより、両方の DPL ビットが 2 に設定され、ユーザー モード (リング 3) 用のビットが作成されます。上記のフラグはすべて、以前の章でプロテクトモードについて説明したときに書いたものです。

必要なのはこれだけです。ユーザモードコードディスクリプタはGDTのインデックス3に、ユーザモードデータディスクリプタはインデックス4にインストールされていることに注意してください。セグメントレジスタは、使用するセレクタのオフセットを含んでいることを忘れないでください。GDTの各エントリは8バイトのサイズなので、コードセレクタ0x18(8*3)、データセレクタ0x20(8*4)ということになる。

したがって、これらのセレクタを使用するには、上記のセグメントセレクタのいずれかを、使用するセグメントレジスタにコピーすればよいことになります。

DPL

DPL(Descriptor Protection Level)は、セグメント記述子の保護レベルです。 例えば、私たちのカーネルのコードとデータセグメントのDPLは、リング0アクセスで0です。

RPL

要求保護レベル(RPL)は、ソフトウェアがCPLをオーバーライドして新しい保護レベルを選択することを可能にします。これは、ソフトウェアがリング0からリング3など他の保護レベルへの変更を要求できるようにするものである。RPLは、記述子セレクタのビット0と1に格納される。

待って、?セグメントセレクタは、GDTへのオフセットに過ぎないことを思い出してください。たとえば、0x8バイトはリング0のコードディスクリプタのオフセットでした。0x8 と 0x10 はセグメントセレクタです。GDTのエントリーはすべて8バイトなので、セグメントセレクタの値は常に8の倍数、つまり8、16、24、32などです。8 は 2 進法では 1000 です。つまり、セグメントセレクタの値がどのようなものであっても、下位3ビットは0であることを意味します。

RPLはセグメントセレクタの下位2ビットに格納されます。したがって、セグメントセレクタが0x8の場合、RPLは0です。 0xbの場合(0x8ですが、最初の2ビットが設定されており、1000ではなく1011)、RPLは3です。 これは、ソフトウェアがユーザーモードに切り替わるために必要なことです。

CPL

CPL(Current Protection Level)は、現在実行中のプログラムの保護レベルである。 CPLは、SSとCSのビット0と1に格納されている。

GDTエントリーは8バイトのサイズであることを忘れないでください。プロテクトモードのセグメントレジスタにはセグメントセレクタ(GDTエントリオフセット)が含まれるため、下位3ビットはゼロであることが保証されます。 CSとSSの下位2ビットは、ソフトウェアのCPLを格納するために使用されます。

保護レベル

ソフトウェアが新しいセグメントをセグメントレジスタにロードしようとすると、プロセッサはソフトウェアのCPLとロードしようとするセグメントのRPLに対してチェックを実行します。RPLがCPLよりも高い場合、そのソフトウェアはセグメントをロードできます。そうでない場合、プロセッサはGeneral Protection Fault(#GP)を発生させます。

RPLの仕組みを理解することは重要で、ユーザーモードに切り替える際に使用する必要な情報です。

ステップ2:切り替え

これで、ユーザーモードに切り替えることができます。

ジャンプを実行する方法は2つあります。どちらもメリットとデメリットがあるので、詳しく見ていきましょう。なお、本連載では移植性の観点からIRETを使用します。

SYSEXIT命令

このセクションは、今後拡張していく予定です。

IRET / IRETD命令

多くのOSでは、SYSEXIT命令よりも移植性が高いため、この方法を採用している場合があります。大きなOSでは、SYSEXITが使用できない場合のバックアップ方法としてサポートされている場合もあります。

さて、それではIRETはどのようにスイッチの実行に役立つのでしょうか。第3章で、モード切り替えの際に使用されるさまざまな方法を思い出してください。IRETはトラップリターン命令です。IRETを実行すると、ユーザーモードのコードに戻るようにスタックフレームを調整することができます。

IRETDが実行されると、スタックに次のようなものがあることを期待します。

  • SS
  • ESP
  • EFLAGS
  • CS
  • EIP

IRETD により、プロセッサはスタックから取得した CS:EIP にジャンプします。また、EFLAGS レジスタにスタックから取得した値を設定します。SS:ESPには、スタックから取得したSSとESPの値を指すように設定されます。

これらは、INT命令実行時に自動的にスタックにプッシュされます。このため、通常の場合、これらの値は変更されないままです。しかし、これらの値を変更することで、IRETにモード切替を行わせることができます。

さて、まずはセグメントセレクタの設定です。下位2ビットが必要なRPLを表していることを思い出してください。この場合、ユーザーモードには3が必要です。では、それを実行しましょう。

void enter_usermode () { _asm { cli mov ax, 0x23 ; user mode data selector is 0x20 (GDT entry 3). Also sets RPL to 3 mov ds, ax mov es, ax mov fs, ax mov gs, ax
これで、ユーザーモードへの切り替えを実行することができます。これは、IRET用のスタックフレームを構築し、IRETを発行することで行われます。
push 0x23 ; SS, notice it uses same selector as above push esp ; ESP pushfd ; EFLAGS push 0x1b ; CS, user mode code selector is 0x18. With RPL 3 this is 0x1b lea eax, [a] ; EIP first push eax iretd a: add esp, 4 // fix stack } }

スタックフレームが上記のリストにあったものと一致していることに注意してください。IRETD命令により、上記のコードではリング3の内部で0x1B:aが呼び出されることになります。

しかし、少し問題があります。上記のルーチンを使ったり、カーネルで別の方法でユーザーモードに切り替えたりしようとすると、ページフォルト(PF)例外が発生するのです。これは、カーネル用のページがカーネルモードアクセス専用にマッピングされているためです。これを解決するには、別の方法でユーザーモードに入るか、ユーザーモードのソフトウェアがアクセスできるようにカーネルをマップする必要があります。

今のところ、ユーザーモードソフトウェアがアクセスできるように、カーネルをマップすることだけを考えています。これには、vmmngr_initialize()ルーチンを更新し、PTEとPDEにUSERビットを設定することが含まれます。

より複雑なオペレーティングシステムでは、この方法は使用されないでしょう。この方法は、ユーザーモードソフトウェアからアクセスできるようにカーネルページをマッピングした場合にのみ機能しますが、これは悪いことです。より推奨されるアプローチは、カーネルページをカーネルのみのアクセスのためにマッピングしておき、ユーザプログラムをロードするときに、カーネルのローダコンポーネントにユーザモードページをマッピングさせることです。そして、スタックとヒープのアロケータが、プログラムのスタックとヒープの領域をユーザモードにマップすることになります。この現在の方法は、カーネルスタックをユーザーランドと共有するもので、大規模なシステムではこれを行うべきではありません。

v8086モードへの移行

v8086モードに入るには、ユーザーモードのタスクが必要です。したがって、上記のようにすれば、v86モードにも入ることができます。ただし、1点だけ少し工夫が必要です。

EFLAGSレジスタの形式を思い出してください。ビット17(VM)は、v8086モード制御フラグです。IRETを行う際にEFLAGSの値をスタックにプッシュするので、v86モードにするためには、EFLAGSのビット17をセットしてからスタックにプッシュすればよいのです。これにより、IRETは戻り時にEFLAGSレジスタのVMビットをセットするようになります。

以上で、v8086モードに移行することができます。

設計上の注意

上記の方法は、ユーザモードに入る簡単な方法ですが、コストがかかります:上記の方法が動作するためには、カーネル領域は、リング3ソフトウェアがカーネルメモリにアクセスできるようにマッピングされなければなりません。 このため、リング3で実行している間、ソフトウェアは、保護モードによるいくつかの制限がありますが、カーネルルーチンを直接呼び出すことができ、カーネル空間を破壊することさえできます。

上記の問題を解決する可能性のある方法は、カーネルメモリをリング0ソフトウェアのために予約しておくことです。カーネルのローダーコンポーネントは、プログラムをロードしている、プロセスのために必要なメモリのリング3領域をマップすることができます。

これについては、次章でOSのローダを開発する際に、さらに検討する予定です。

カーネルの世界へ戻る

ステップ1:TSSのセットアップ

x86アーキテクチャは、ハードウェア支援によるタスクスイッチングをサポートしています。つまり、このアーキテクチャには、プロセッサが異なるタスクを選択できるようにするためのハードウェア定義構造が含まれています。

最近のほとんどのオペレーティングシステムは、移植性の観点からハードウェアタスク切り替えサポートを利用していません。 これらのオペレーティングシステムは、通常、ソフトウェアタスク切り替え方法を採用しています。

タスクステートセグメント(TSS)

TSSの構造は非常に大きいです。

#ifdef _MSC_VER #pragma pack (push, 1) #endif struct tss_entry { uint32_t prevTss; uint32_t esp0; uint32_t ss0; uint32_t esp1; uint32_t ss1; uint32_t esp2; uint32_t ss2; uint32_t cr3; uint32_t eip; uint32_t eflags; uint32_t eax; uint32_t ecx; uint32_t edx; uint32_t ebx; uint32_t esp; uint32_t ebp; uint32_t esi; uint32_t edi; uint32_t es; uint32_t cs; uint32_t ss; uint32_t ds; uint32_t fs; uint32_t gs; uint32_t ldt; uint16_t trap; uint16_t iomap; }; #ifdef _MSC_VER #pragma pack (pop, 1) #endif

TSSは、ハードウェアタスクスイッチの前のマシンの状態に関する情報を格納するために使用されます。多くのメンバーを持っているので、見てみましょう。

  • 一般的なフィールド。
    • タスクスイッチ前のLDT、EIP、EFLAGS、CS、DS、ES、FS、GS、SS、EAX、EBX、ECX、EDX、ESP、EBP、ESI、EDI の状態
  • prevTSS- タスクリスト内の前のTSSのセグメントセレクタ
  • cr3- PDBR、現在のタスクのページディレクトリのアドレス
  • トラップ
    • ビット0:0: 無効、1: タスク切り替え時にデバッグ例外を発生させる
  • iomap- TSSベースからI/O許可および割り込みリダイレクションビットマップへの16ビットオフセット
  • esp0,esp1,esp2- リング0、1、2用のESPスタックポインタ。
  • ss0,ss1,ss2- リング0,1,2のSSスタックセグメント

これらのフィールドのほとんどは、非常に単純です。このため、この構造体のいくつかのフィールド、特にリング 0 スタックとセレクタのフィールドを設定する必要があります。

ステップ2:TSSのインストール

ディスクリプタ・セグメント

TSSは、その名前からも分かるように、セグメントです。すべてのセグメントと同様に、TSSはGDTのエントリを必要とします。これにより、タスクがビジー状態か非アクティブ状態かを設定したり、どのソフトウェアがアクセスできるか(DPL)、ディスクリプタで設定できる他のフラグなど、TSSを制御することができます。Base Addressフィールドは、設定したTSS構造体のベースアドレスでなければなりません。

LTR命令

LTR(LoadTask Register)命令は、TSSをTSRレジスタにロードするために使用します。 例えば、以下のようなものです。

void flush_tss (uint16_t sel) { _asm ltr [sel] }

axはTSSのセグメントセレクタです。このアーキテクチャはハードウェアタスクの切り替えをサポートしているため、TSRには現在のタスクを定義するTSSのアドレスが格納されます。

タスクステートレジスタ(TSR)は、TSSセレクタTSSベースアドレスTSSリミットを格納するレジスタです。ただし、ソフトウェアで変更できるのは、TSSセレクタのみです。

TSSのインストール

TSS構造をインストールするには、まずTSSのGDTエントリーをインストールします。次に、上記のflush_tssを呼び出して、TSS をカレントタスクとして選択します。

void install_tss (uint32_t idx, uint16_t kernelSS, uint16_t kernelESP) { //! install TSS descriptor uint32_t base = (uint32_t) &TSS; gdt_set_descriptor (idx, base, base + sizeof (tss_entry), I86_GDT_DESC_ACCESS|I86_GDT_DESC_EXEC_CODE|I86_GDT_DESC_DPL|I86_GDT_DESC_MEMORY, 0); //! initialize TSS memset ((void*) &TSS, 0, sizeof (tss_entry)); TSS.ss0 = kernelSS; TSS.esp0 = kernelESP; TSS.cs=0x0b; TSS.ss = 0x13; TSS.es = 0x13; TSS.ds = 0x13; TSS.fs = 0x13; TSS.gs = 0x13; //! flush tss flush_tss (idx * sizeof (gdt_descriptor)); }

上記のコードでは、TSSは、我々のtss_entry構造体のグローバル構造体の定義です。 前のタスク(ユーザーモードセレクタ)とリング0スタック(カーネルスタック、kernelSS:kernelESPに位置)に合わせてTSSのセレクタエントリを設定しています。flush_tssはTSSをTSRにインストールします。

追加命令

他にも便利な命令がいくつかあります。これらの命令はすべて、ユーザーモードのソフトウェアで実行することができます。

VERR命令

VERR(Verify Segment is Readable) は、セグメントが読み取り可能かどうかをチェックするために使用できます。読み出し可能であれば、プロセッサはゼロフラグ(ZF)を1にセットします。この命令は、どのプロビレッジレベルでも実行することができます。

verr [ebx] jz .readable

VERW命令

VERW(Verify Segment is Writable) は、セグメントが書き込み可能かどうかをチェックするために使用できます。書き込み可能であれば、プロセッサはゼロフラグ(ZF)を1にセットします。この命令は、どのプロビレッジレベルでも実行することができます。

verw [ebx] jz .readable

LSL命令

セレクタのセグメントリミットをレジスタにロードする命令です。

lsl ebx, esp jz .success

ARPL 命令

セレクタのRPLを調整する命令です。arpl dest,srcの形式で、destはメモリ位置またはレジスタ、srcはレジスタです。destのRPLがsrcより小さい場合、destのRPLビットはsrcのRPLビットに設定されます。たとえば、次のようになります。

arpl ebx, esp

システムAPI

概要

システムAPI は、ソフトウェアがオペレーティング システムと相互作用できるようにするツール、文書、およびインターフェイスを提供します。オペレーティングシステムによって使用される用語は異なりますが、基本的な考え方は同じです。例えば、WindowsはこのAPIを「Native API」と呼んでいます。

システムAPIは、ソフトウェアがオペレーティングシステムやデバイスドライバと対話するのを容易にする。 システムAPIは、ユーザーモードソフトウェアとカーネルモードソフトウェアの間のインターフェースである。ソフトウェアがシステム情報を必要とするとき、またはファイルの作成などのシステムタスクを実行するときはいつでも、ソフトウェアはシステムコールを呼び出します。

システムコールは、システムサービスとも呼ばれ、オペレーティングシステムによって提供されるサービスです。 このサービスは、通常、関数またはルーチンです。ソフトウェアは、システムタスクを実行するためにシステムコールを呼び出すことができます。

設計

sysenter / sysexit

このセクションは、今後拡張していく予定です。

ソフトウェア割り込み

ほとんどのシステムAPIは、ソフトウェア割り込みを使用して実装されています。ソフトウェアは、int 0x21のような命令を使用して、オペレーティングシステムのサービスを呼び出すことができます。例えば、DOSのTerminate関数を呼び出すには、次のようにします。

mov ax, 0x4c00 ; function 0x4c (terminate) return code 0 int 0x21 ; call DOS service

上記のコードでは、AHに関数番号が含まれています。int 0x21は0x21割り込みベクタを呼び出してDOSを呼び出しています。

上記を動作させるためには、OSが割り込みベクタ0x21のISRをインストールする必要があります。ISRはAHを比較し、正しいカーネルモード関数に制御を渡すFSM(Finity State Machine)になるでしょう。そして、親愛なる読者の皆さん、これが設計です。

ソフトウェア割り込みは、SYSENTERやSYSEXITよりも移植性が高いです。このため、ほとんどのOSはこの方法を(おそらく他の方法と一緒に)サポートしています。このシリーズでは、この方法を使用します。

システムAPIは通常、数百のシステムコールで構成されています。

ここでは、いくつかのオペレーティングシステムと、それらがサポートしているメソッドの一覧を示します。INT番号は、上記の方法によるソフトウェア割り込みベクタ番号です。

  • DOS: INT 0x21
  • Win9x (95,98):INT 0x2F
  • WinNT (2k,XP,Vista,7)。INT 0x2E、SYSENTER/SYSEXIT、SYSCALL/SYSRET
    • 211以上の関数
  • Linuxの場合INT 0x80、SYSENTER/SYSEXIT。
    • 190以上の関数

基本システムAPI

ステップ1:システムコールテーブル

ほとんどのシステムAPIは、すべてのサービスを含むシステムコール・テーブルを実装しています。このテーブルは、静的、動的、自動生成、またはその3つの組み合わせのいずれかにすることができます。大規模なオペレーティングシステムでは、通常、システムコールの自動生成された動的サイズのテーブルが採用されています。これは、このテーブルに含まれる可能性のあるシステムサービスの数が多いためで、手作業で作成すると非常に面倒な作業となります。

この目的のためには、カーネルにシステムサービステーブルを定義すればよいのです。このテーブルには、カーネルにある呼び出し可能なさまざまな関数のアドレスが含まれています。

#define MAX_SYSCALL 3 void* _syscalls[] = { DebugPrintf };

うーん、この表はかなり小さいですね。次の章でこのリストにさらに追加していきますが、あまり複雑にはならないでしょう。

DebugPrintf はユーザモードからアクセス可能であり(カーネルページがマッピングされているため)、DebugPrintf は特権命令を使用していないため、ユーザモードのソフトウェアは技術的に何の問題もなくこのルーチンを直接呼び出すことができます。オペレーティングシステムやエグゼクティブソフトウェアの設計によっては、セキュリティや安定性の問題を引き起こす可能性があります。

このため、カーネルページはカーネルモードからしかアクセスできないようにしておくことが一般的に推奨されています。 ソフトウェアは複雑になりますが、最終的には努力に見合った結果を得ることができるかもしれません。

ステップ2:サービスディスパッチャ

次のステップは、サービスディスパッチャのISRを作成することです。その前に、どのISRを使うか決めなければなりません。ここではLinuxに倣って0x80を使うことにします。しかし、多くのOSは異なるベクタを使用しています。では、ISRをインストールしましょう。

ISRはHAL層が管理するIDTに格納されていることを思い出してください。また、IDTディスクリプタはそれぞれDPLの設定を持っていることを15章で思い出してください。IDTエントリのDPLがCPLより小さいと、GPFが発生します。つまり、ユーザモードに入ると、DPL 3のIDTディスクリプタを持つISRしか呼び出せなくなるのです。リング3のソフトウェアからシステム割り込みを呼び出したいので、このISRを正しいフラグでインストールしなければなりません。

しかし、現在のHALサブシステムの設計では、setvect()を呼び出すだけでは、特定のフラグを設定することができないため、これを実現することはできません。この問題を回避するために、setvect() は、オプションのフラグを設定できるように、 2番目のパラメータを持つように修正されました。これは、C++のデフォルトのパラメータ機能を使用して実現されているため、他のコードを更新する必要はありません。

void syscall_init () { //! install interrupt handler! setvect (0x80, syscall_dispatcher, I86_IDT_DESC_RING3); }

これで全部です :)

syscall_dispatcherは、システムコール用の ISR です。このISRは、_syscallsで関数を検索することによって、どのシステムサービスを呼び出すかを決定する必要があります。通常、システムAPIは関数番号を識別するためにEAXを使用します。ここでも同じことをするつもりです。上で定義したシステム・サービス・テーブルのおかげで、EAXをインデックスとして使用することができます。したがって、呼び出す関数は_syscalls [eax]となります。

_declspec(naked) void syscall_dispatcher () { static int idx=0; _asm mov [idx], eax //! bounds check if (idx>=MAX_SYSCALL) _asm iretd //! get service static void* fnct = _syscalls[idx];

さて、これで呼び出す関数へのポインタができました。しかし、ここでちょっとした問題が発生します。上記は、EAXで与えられた値に基づいて、必要なサービス関数へのポインタを効果的に取得します。しかし、その関数が何であるかはわかりません。また、その関数に何を渡せばいいのか、パラメータはどれくらいあるのかもわかりません。

解決策の1つは、関数呼び出しのためにすべてのレジスタをスタックにプッシュすることである。サービスはすべてCルーチンなので、C関数が期待するような方法でパラメータを渡さなければなりません。

//! and call service _asm { push edi push esi push edx push ecx push ebx call fnct add esp, 20 iretd } }

これで終わりです :)add esp, 20は、プッシュした 20 バイトをスタックからポップし、IRETD命令で ISR からリターンしていることに注意してください。

システムソフトウェアやエグゼクティブがそれぞれの割り込みベクタにISRをインストー ルした後、ソフトウェアはソフトウェア割り込みを発行してそれを呼び出すことができま す。例えば、syscall_initを呼んでISRをインストールした場合、次のようにシステムサービスを呼び出すことができます。

_asm { xor eax, eax ; function 0, DebugPrintf lea ebx, [stringToPrint] int 0x80 ; call OS }

設計上の注意点

多くのOSは割り込みベクタ番号やレジスタの詳細をC言語のインタフェースで抽象化しています。 大きなOSではシステムサービスを直接呼び出すことも可能ですが、システムがユーザランドソフトウェアに提供するシステムサービスに関する標準Cインタフェースを開発することが推奨されます。

大型のOSでは、ディスプレイにメッセージを表示するようなシステムサービスは一般にありません。むしろ、ユーザがAPIを使ってカーネルモードのサービスやサーバ、デバイスドライバと対話できるような、ユーザランドのソフトウェアから呼び出せるようなサービスが含まれるでしょう。このため、大規模なOSでは、数百の関数呼び出しからなるシステムAPIを含むのが一般的です。

まとめ

ユーザランドへようこそ!

これで、ユーザランドとカーネルランドの切り替えに必要なものは全て揃いました。これで、ユーザモードのページをマッピングし、ロードし、ユーザモードでプログラムを実行することができるようになりました。しかし、システムがタスクを管理しないため、OSのカーネルにうまく戻ることはできません。これについては、次の章で見ていきます。