カーネルがユーザープログラム(システムコール)に代わって実行されているときに、カーネルスペースは使用されますか?それとも、すべてのカーネルスレッド(たとえば、スケジューラー)のアドレス空間ですか?
それが最初のものである場合、それは通常のユーザープログラムが3GBを超えるメモリを使用できないことを意味しますか(分割が3GB + 1GBの場合)?また、その場合、カーネルはどのようにしてハイメモリを使用できますか?1GBのカーネルスペースが論理的にマップされるので、ハイメモリからのページはどの仮想メモリアドレスにマップされますか?
カーネルがユーザープログラム(システムコール)に代わって実行されているときに、カーネルスペースは使用されますか?それとも、すべてのカーネルスレッド(たとえば、スケジューラー)のアドレス空間ですか?
はい、はい。
先に進む前に、これを記憶について述べておく必要があります。
メモリは2つの異なる領域に分けられます:
ユーザー空間で実行されているプロセスはメモリの限られた部分にしかアクセスできませんが、カーネルはすべてのメモリにアクセスできます。ユーザー空間で実行中のプロセスもしないでください-カーネル空間にアクセスできます。ユーザー空間プロセスは、カーネルの一部にのみアクセスできますカーネルによって公開されているインターフェイスを介してアクセスできます-システムコール。プロセスがシステムコールを実行すると、ソフトウェア割り込みがカーネルに送信され、カーネルは適切な割り込みハンドラーをディスパッチし、ハンドラーが終了した後も処理を続行します。
カーネルスペースコードには「カーネルモード」で実行するプロパティがあります。これは(通常のデスクトップでは-x86-コンピューターで)リング0で実行される呼び出しコードです。通常x86アーキテクチャでは、4つの保護リングがあります。リング0(カーネルモード)、リング1(仮想マシンハイパーバイザーまたはドライバーで使用される可能性があります)、リング2(ドライバーで使用される可能性があります。リング3は、典型的なアプリケーションが実行されるものです。これは最も権限の少ないリングであり、その上で実行されているアプリケーションは、プロセッサの命令のサブセットにアクセスできます。リング0(カーネルスペース)は最も特権的なリングであり、マシンのすべての命令にアクセスできます。たとえば、「ブラウザ」のような「プレーン」アプリケーションは、x86アセンブリ命令lgdt
を使用してグローバル記述子テーブルをロードしたり、hlt
を使用してプロセッサを停止したりすることはできません。
それが最初のものである場合、それは通常のユーザープログラムが3GBを超えるメモリを持つことができないことを意味しますか(分割が3GB + 1GBの場合)?また、その場合、カーネルはどのようにしてハイメモリを使用できますか?1GBのカーネルスペースが論理的にマップされるので、ハイメモリのページはどの仮想メモリアドレスにマップされるのですか?
CPUリングが最も明確な区別です
X86保護モードでは、CPUは常に4つのリングのいずれかにあります。 Linuxカーネルは0と3のみを使用します。
これは、カーネル対ユーザーランドの最もハードで高速な定義です。
Linuxがリング1および2を使用しない理由 https://stackoverflow.com/questions/6710040/cpu-privilege-rings-why-rings-1-and-2-arent-used
現在のリングはどのように決定されますか?
現在のリングは、次の組み合わせで選択されます。
グローバル記述子テーブル:GDTエントリのメモリ内テーブル。各エントリには、リングをエンコードするフィールドPrivl
があります。
LGDT命令は、アドレスを現在の記述子テーブルに設定します。
セグメントは、GDT内のエントリのインデックスを指すCS、DSなどを登録します。
たとえば、CS = 0
は、GDTの最初のエントリが実行中のコードに対して現在アクティブであることを意味します。
各リングは何ができますか?
CPUチップは、次のように物理的に構築されています。
リング0は何でもできます
ring 3は、いくつかの命令を実行して、いくつかのレジスタに書き込むことができません。
自分のリングを変更することはできません!それ以外の場合、それ自体をリング0に設定する可能性があり、リングは役に立ちません。
つまり、現在のリングを決定する現在の segment descriptor を変更することはできません。
ページテーブルを変更できません: https://stackoverflow.com/questions/18431261/how-does-x86-paging-work
つまり、CR3レジスタを変更できず、ページング自体がページテーブルの変更を防ぎます。
これにより、セキュリティ/プログラミングの容易さのために、1つのプロセスが他のプロセスのメモリを参照できなくなります。
割り込みハンドラを登録できません。それらはメモリ位置への書き込みによって構成されますが、これもページングによって防止されます。
ハンドラーはリング0で実行され、セキュリティモデルに違反します。
つまり、LGDTおよびLIDT命令を使用できません。
IO in
やout
のような命令は実行できないため、任意のハードウェアアクセスが可能です。
それ以外の場合、たとえば、プログラムがディスクから直接読み取ることができる場合、ファイルのアクセス許可は役に立ちません。
より正確には Michael Petch に感謝します。OSが実際にリング3でIO命令を許可することは可能です)、これは実際に タスク状態セグメント 。
リング3が最初にそれを持っていなかった場合、リング3がそれを許可することは不可能です。
Linuxは常にそれを拒否します。参照: https://stackoverflow.com/questions/2711044/why-doesnt-linux-use-the-hardware-context-switch-via-the-tss
プログラムとオペレーティングシステムはリング間でどのように移行しますか?
cPUがオンになると、リング0で初期プログラムの実行を開始します(まあ、それは良い概算です)。この初期プログラムはカーネルであると考えることができます(ただし、通常はリング0にあるカーネルを呼び出すブートローダーです)。
ユーザーランドプロセスがファイルへの書き込みのような何かをカーネルに実行させたい場合、 int 0x80
またはsyscall
などの割り込みを生成する命令を使用しますカーネルにシグナルを送る。 x86-64 Linux syscall hello worldの例:
.data
hello_world:
.ascii "hello world\n"
hello_world_len = . - hello_world
.text
.global _start
_start:
/* write */
mov $1, %rax
mov $1, %rdi
mov $hello_world, %rsi
mov $hello_world_len, %rdx
syscall
/* exit */
mov $60, %rax
mov $0, %rdi
syscall
コンパイルして実行:
as -o hello_world.o hello_world.S
ld -o hello_world.out hello_world.o
./hello_world.out
これが発生すると、CPUはカーネルがブート時に登録した割り込みコールバックハンドラーを呼び出します。ハンドラーを登録して使用する concrete baremetalの例を次に示します 。
このハンドラーはリング0で実行され、カーネルがこのアクションを許可するかどうかを決定し、アクションを実行し、リング3でユーザーランドプログラムを再起動します。x86_64
exec
システムコールが使用される場合(またはカーネル が/init
を開始する場合)、カーネル はレジスタを準備し、新しいユーザーランドプロセスのメモリ 、エントリポイントにジャンプし、CPUをリング3に切り替えます。
(ページングのため)禁止されたレジスタまたはメモリアドレスへの書き込みなど、プログラムがエッチなことを行おうとすると、CPUはリング0のカーネルコールバックハンドラも呼び出します。
しかし、ユーザーランドはいたずらだったので、今回はカーネルがプロセスを強制終了するか、シグナルで警告を出す可能性があります。
カーネルが起動すると、一定の周波数でハードウェアクロックがセットアップされ、定期的に割り込みが生成されます。
このハードウェアクロックは、リング0を実行する割り込みを生成し、ウェイクアップするユーザーランドプロセスをスケジュールできるようにします。
このようにして、プロセスがシステムコールを実行していない場合でも、スケジューリングを行うことができます。
複数のリングを持つことのポイントは何ですか?
カーネルとユーザーランドを分離することには、2つの大きな利点があります。
どうやってそれをいじるのですか?
リングを直接操作するのに適した方法であるベアメタルセットアップを作成しました: https://github.com/cirosantilli/x86-bare-metal-examples
残念ながら、ユーザーランドの例を作る忍耐力はありませんでしたが、ページングのセットアップまで行ったので、ユーザーランドは実現可能です。プルリクエストが欲しいです。
または、Linuxカーネルモジュールはリング0で実行されるため、それらを使用して特権操作を試すことができます。制御レジスタを読み取ります: https://stackoverflow.com/questions/7415515/how-to-access-the-control-registers-cr0-cr2-cr3-from-a-program- getting-segmenta/7419306#7419306
便利なQEMU + Buildrootセットアップ は、ホストを殺すことなく試してみることができます。
カーネルモジュールの欠点は、他のkthreadが実行中であり、実験に干渉する可能性があることです。しかし理論的には、カーネルモジュールですべての割り込みハンドラーを引き継ぎ、システムを所有することができます。これは実際には興味深いプロジェクトです。
マイナスのリング
負のリングは実際にはIntelのマニュアルでは参照されていませんが、実際にはリング0自体よりも多くの機能を備えたCPUモードがあるため、「負のリング」の名前に適しています。
1つの例は、仮想化で使用されるハイパーバイザーモードです。
詳細については、 https://security.stackexchange.com/questions/129098/what-is-protection-ring-1 をご覧ください。
[〜#〜]アーム[〜#〜]
ARMでは、リングは代わりに例外レベルと呼ばれますが、主な考え方は同じです。
ARMv8には4つの例外レベルがあり、一般に次のように使用されます。
EL0:ユーザーランド
EL1:カーネル(ARM用語)の「スーパーバイザー」)。
以前はsvc
として知られていたswi
命令(SuperVisor呼び出し)を使用して入力されました 統合前のアセンブリ 、これはLinuxシステムコールを作成するために使用される命令です。 Hello world ARMv8の例:
.text
.global _start
_start:
/* write */
mov x0, 1
ldr x1, =msg
ldr x2, =len
mov x8, 64
svc 0
/* exit */
mov x0, 0
mov x8, 93
svc 0
msg:
.ascii "hello syscall v8\n"
len = . - msg
Ubuntu 16.04のQEMUでテストします。
Sudo apt-get install qemu-user gcc-arm-linux-gnueabihf
arm-linux-gnueabihf-as -o hello.o hello.S
arm-linux-gnueabihf-ld -o hello hello.o
qemu-arm hello
EL2: hypervisors 、たとえば Xen 。
hvc
命令(HyperVisor Call)で入力されました。
ハイパーバイザーはOSに対するものであり、ユーザーランドにとってOSとは何か。
たとえば、Xenを使用すると、LinuxやWindowsなどの複数のOSを同じシステムで同時に実行でき、Linuxがユーザーランドプログラムで行うのと同じように、セキュリティとデバッグの容易さのためにOSを互いに分離します。
ハイパーバイザーは、今日のクラウドインフラストラクチャの重要な部分です。ハイパーバイザーは、単一のハードウェア上で複数のサーバーを実行できるようにし、ハードウェアの使用率を常に100%に保ち、コストを大幅に節約します。
たとえば、AWSは2017年まで its to KVM=がニュース に移行するまでXenを使用していました。
EL3:さらに別のレベル。 TODOの例。
smc
命令で入力(セキュアモードコール)
ARMv8アーキテクチャリファレンスモデルDDI 0487C.a -章D1-AArch64システムレベルプログラマのモデル-図D1-1はこれを美しく示しています。
後知恵の恩恵により、ARMの特権レベルの命名規則はx86よりも優れていることに注意してください。負のレベルは必要ありません。0は最低で3は最高です。高いレベルは、低いレベルよりも頻繁に作成される傾向があります。
現在のELは、MRS
命令でクエリできます: https://stackoverflow.com/questions/31787617/what-is-the-current-execution-mode-exception-level-etc
ARMでは、チップ領域を節約する機能を必要としない実装を可能にするために、すべての例外レベルが存在する必要はありません。 ARMv8の「例外レベル」は次のように述べています。
実装には、すべての例外レベルが含まれていない場合があります。すべての実装にはEL0とEL1を含める必要があります。 EL2とEL3はオプションです。
たとえばQEMUのデフォルトはEL1ですが、EL2およびEL3はコマンドラインオプションで有効にすることができます: https://stackoverflow.com/questions/42824706/qemu-system-aarch64-entering-el1- when-emulating-a53-power-up
Ubuntu 18.10でテストされたコードスニペット。
それが最初のものである場合、それは通常のユーザープログラムが3GBを超えるメモリを持つことができないことを意味しますか(分割が3GB + 1GBの場合)?
はい、これは通常のLinuxシステムの場合です。ユーザーとカーネルのアドレス空間を完全に独立させる一連の「4G/4G」パッチが一時的に浮かんでいました(カーネルがユーザーメモリにアクセスするのが難しくなるため、パフォーマンスコストがかかります)が、私は思いません彼らはかつて上流で合併され、x86-64の台頭により関心は衰えました
また、その場合、カーネルはどのようにしてハイメモリを使用できますか?1GBのカーネルスペースが論理的にマップされるので、ハイメモリのページはどの仮想メモリアドレスにマップされるのですか?
Linuxが機能する(かつ、アドレススペースと比較してメモリが小さいシステムでも機能する)方法は、物理メモリ全体がアドレススペースのカーネル部分に永続的にマッピングされることでした。これにより、カーネルはすべての物理メモリに再マッピングせずにアクセスできましたが、物理メモリが大量にある32ビットマシンにはスケーリングしません。
したがって、低メモリと高メモリの概念が生まれました。 「低」メモリは、カーネルのアドレス空間に永続的にマッピングされます。 「ハイ」メモリではありません。
プロセッサがシステムコールを実行しているときは、カーネルモードで実行されていますが、現在のプロセスのコンテキストで実行されています。したがって、現在のプロセスのカーネルアドレススペースとユーザーアドレススペースの両方に直接アクセスできます(前述の4G/4Gパッチを使用していない場合)。これは、「ハイ」メモリがユーザーランドプロセスに割り当てられても問題ないことを意味します。
カーネルの目的で「高」メモリを使用することは、さらに問題になります。現在のプロセスにマップされていないハイメモリにアクセスするには、一時的にカーネルのアドレス空間にマップする必要があります。つまり、追加のコードとパフォーマンスの低下を意味します。