Linux カーネルハックを始める前に知っておきたいこと
カーネルハックの入門的な内容について、あまりまとまった情報がないように思ったので、記事にしてみようと思いました。
これから勉強を始めてみようと思われる方の参考になれば幸いです。
ユーザー空間とカーネル空間
Linux のような UNIX 系 OS では、ユーザー空間とカーネル空間とよばれる2種類の空間でプログラムを実行します。
ユーザー空間で実行されるプログラムは、ユーザー空間アプリケーションとも呼ばれ、日頃利用している多くのアプリケーション、例えば、ブラウザ、Web サーバー、Office ソフトウェアはユーザー空間で動作するものです。
カーネル空間で実行されるプログラムは、まさにカーネルで、OS と呼ばれる部分にあたります。
ユーザー空間とカーネル空間の大きな違いは、ユーザー空間で実行されるプログラムについては、CPU 機能によって、できることが制限されている一方、カーネル空間で実行されるプログラムには、その制限がない点にあります。
| User-space Application | <= Unprivileged -------------------------- | Kernel | <= Privileged
保護機能
このように、プログラムを二つの空間に分けて実行する大きな理由の一つは、システム全体を保護することです。
想像し難いかもしれませんが、コンピューターでは、プログラムによって不適切な操作を行うと、システム全体を機能停止状態にすることができます。これは、意図的ではなくても、プログラムのバグ等によってシステム全体を停止できることを意味します。
Linux のような UNIX 系 OS では、なるべくシステム全体が機能停止しないで動作し続けられるようにするためのアプローチとして、本当に必要な場合を除いて、不適切な動作をそもそも実行できないようにする、という戦略をとっています。
具体的には、プログラムの大部分をユーザー空間で実行し、バグがない信頼に足るプログラムだけをカーネル空間で実行します。
この戦略において、ユーザー空間では、システム全体に影響を与えられる操作ができないように制限されているため、ユーザー空間プログラムはバグだけではなく悪意があっても、システムを破壊できないようになっています。
ユーザー空間において制限される操作
ユーザー空間では、主に2点、CPU の特権命令が発行できない、また、限られたメモリ領域以外にアクセスできない、という制限があります。
具体的には、例えば、四則演算は特権命令ではありませんが、CPU をシャットダウンする命令は特権命令です。
また、デバイスの入出力に関わる箇所も特権命令、もしくは特定のメモリ領域へのアクセスが必要となるため、通常、ユーザー空間からは直接デバイスの操作を行うことはできません。
システムコール
CPU の特権命令と、メモリアクセスが制限された場合、そのままではユーザー空間プログラムは簡単な電卓プログラムさえ実行できません。キーボードもデバイスの一種なので入力は特権がなければ受け取れない上、結果をディスプレイへ出力するためにも特権が必要だからです。
カーネルは、ユーザー空間プログラムが、機能が制限された状態でも特権が必要な機能にアクセスできるように、CPU、デバイスとユーザー空間プログラムを仲介します。
ユーザー空間プログラムが、特権が必要な機能へアクセスする場合には、通常、システムコールと呼ばれる特殊な関数をコールします。
ユーザー空間プログラムからシステムコールを呼び出すと、対応するカーネル空間内部の関数に処理が移行し、特権のある状態でカーネル空間のプログラムが実行できます。
ユーザー空間から任意のシステムコールをカーネル空間で実行できるので、保護機能に関して言うと本末転倒に見えるかもしれませんが、システムコールに対応する関数の実装は、カーネルの一部なので、ユーザー空間は、システムコール自体の処理を改変することはできず、システムコールの実装自体に問題がなければ、ユーザー空間からカーネルおよび、システム全体に悪影響を与えることはできません。
システムコールという仕組みが用いられる理由は、ユーザー空間が、決められた方法(つまり、システムコール)以外でカーネル空間の機能にアクセスできないようにするためです。
ユーザー空間プログラムは、システムコール以外でカーネル空間の機能にアクセスできないので、カーネルは、数に限りがあるシステムコールだけ適切に実装すればシステム全体の機能の安全性を担保できます。
| User-space Application | <= Unprivileged | ||| | -----< system call >------ | ||| | | Kernel | <= Privileged
カーネル内部に実装される機能
カーネルは、ユーザー空間で実行できない CPU の特権命令とメモリアクセス、また、それらを使ったデバイスへのアクセスを組み合わせて、ユーザー空間で動作するプログラムが効率よくアプリケーション機能を実装できるような仕組みを提供します。
どのような機能をカーネル内に実装すべきかという議論は古くからありますが、UNIX 系の OS では以下のような機能がカーネルの一部として実装されています。(他にもたくさんあります。)
| User-space Application | <= Unprivileged | ||| | -----< system call >------ | ||| | | Kernel | <= Privileged - Scheduler - Memory Management - Network Stack - File System ...
上記の機能は、必要に応じて、システムコールを通じてユーザー空間プログラムからアクセスできるように実装されています。
例えば、プロセススケジューラに関して、プロセスの生成をリクエストできるように、fork システムコールが用意されています。
メモリ管理に関しては、ユーザー空間のプログラムは、新しいメモリ領域を brk システムコール、もしくは mmap システムコールを通してリクエストできます。
ネットワークスタックやファイルシステムは、read や write システムコールを通じて読み書きをリクエストできます。
デバイスドライバに関しては、多くの場合、直接処理をリクエストするシステムコールは実装されていませんが、ネットワークスタックや、ファイルシステムが利用しています。
カーネルハックで何ができるのか
カーネルハックをすると、先述のような、カーネルの一部として実装されている機能を追加、改変することができます。
カーネルを改変することの利点は主に、特権が必要なプログラムを実装できることと、(実装によりますが)ユーザー空間プログラムを変更することなく、システムの挙動を変更できることです。
例えば、ファイルシステムの性能を改善すると、あるデータベースアプリケーションについて全く変更を加えなくても、性能を向上できることがあります。
カーネルを改変することの欠点は、可搬性が低いことです。カーネルにバグがあるとシステム全体が破損する、もしくはセキュリティ上の問題が発生する可能性があるので、安全性の保証されていないカーネルのコードを利用するのは危険が伴います。なので、新しいカーネルの機能を取り込むことには多くのユーザーにとって抵抗があります。
なので、プロダクション環境で利用するプログラムを想定し、かつ、OS のブランチにマージされることを意図しないのであれば、カーネル内部のプログラミングは本質的に避けるべきであると思われます。
ですが、OS の知識や技術獲得のためには良い教材であり、継続すれば OS にマージされるプログラムが書けるようになるかもしれないので、取り組む価値があると思います。
また、カーネルのハックの方法によっては上記の欠点をある程度補うことができ、それらについて以下で述べたいと思います。
カーネルハックの2通りの方法
カーネルをハックする場合、主に、カーネルのソースコード自体を改変する、もしくは、カーネルモジュールを作成する、という2通りの方法があります。
カーネルのソースコード自体を改変する場合、文字通りなんでもできます。ですが、改変の適用にはカーネルをコンパイルしたのち再起動して、新しいカーネルをロードし直す必要があります。
一方で、カーネルモジュールは、できることが制限されますが、OS を再起動することなくモジュールをロードすることができます。また、カーネルのソースコードの改変と再コンパイルも不要です。
このことから、カーネルモジュールで完結するシステムであれば、比較的可搬性が担保できます。
カーネルモジュールで実装されており、Linux にマージされていない有名なソフトウェアとして、ZFS があります。ZFS はライセンスの理由で Linux にマージされていませんが、新しいバージョンの Linux カーネルで利用可能なように更新が継続されており、カーネルモジュールとしてビルド可能なソースコードが配布されています。
カーネルのソースコードを改変しなければならない場合
可搬性の観点から、なるべくカーネルモジュールとして実装が出来ると良いのですが、それができない場合もあります。
カーネルは、カーネルモジュールのために特定のインターフェースを提供しており、その枠組みの上でカーネルモジュールが実装されることを想定しています。なので、カーネルが想定していない機能について、カーネルモジュールで実装することがとても難しい場合があります。
例えば、プロセススケジューラは、OS の中心部にあたるため、カーネル自体はスケジューラに関するアクセスをカーネルモジュールに対して明示的に提供しません。なので、スケジューリングアルゴリズムを追加する場合には、カーネルのソースコードを改変する必要があると思われます。
一方で、ファイルシステムについてはモジュールとして実装可能なように、カーネルがインターフェースを提供しています。結果として、ZFS のようなファイルシステムは独立したカーネルモジュールとして実装されています。
このように、ある機能がカーネルモジュールとして実装可能かどうかについては、カーネル自体が想定しているかどうかによるところが大きいです。
カーネルハックを始める際には、モジュールとして実装可能な箇所から始めていくとお手軽で良いかと思われます。