カーネル(その8)
前回は、GNU/Linux (以下、Linux) における ELF 実行処理について見てきた。今回は、共有ライブラリをロードしない ELF 実行形式ファイルを実行する環境を onix に構築する話をする。そして、実際に実行形式ファイルを Linux で作成して、onix で実行する。
onix で ELF を実行する処理は elf.c に記述されている。elf_load_file() ルーチンでは、実行するプログラムのパスを受け取り、パス名検索を行って、ディレクトリツリーからディスク inode を取得。そして、ファイルの実態を取得している。
そして、ファイルの ELF ヘッダ及びプログラムヘッダを解析し、実行ファイルによって共有された仮想領域に、各セグメントをマッピングさせる。
ここでの完全に独立した(共有ライブラリのロード及び、実行時ロードを行わない) ELF ファイルの実行処理は、linux における load_elf_binary() 処理とやっている事はほとんど同じである。ただし、各種ファイルチェック及びセキュリティチェック、そして共有ライブラリのロード処理に関する処理については、ここでは一切行っていない。
このように、ロード処理は極めて単純な仕組み及び処理になっている。
ここで話が逸れるが。プログラムのページマッピング処理を作っている最中に onix におけるページ管理の仕組みを大幅に変更した。現在の拡張ページテーブルにおける設計が限界を迎えた為である。
そもそも、何故この仕組みを使おうとしたかというと、半年前に遡る。
当時、スワップアウト時にページを圧縮して、ディスクアクセスを減らし、スワップ時のパフォーマンス劣化を防ぐ機能を持たせる事で、onix の存在意義を訴えようとしていた。ページフォルトによるスワップイン処理はハードウェアによってページ単位で処理が行われるので、スワップ時のパフォーマンスを上げる際には、圧縮するページのサイズが大きい程パフォーマンスが向上する(と思われる)。
それによって、IA-32 に存在する拡張ページテーブルの機能によって、仮想領域へのアクセスによるアドレス解決を 1 段階のページテーブルによって実現した。これによって、ページサイズは (2^22 = 4MB) になる。
だが、これを実現した場合、ユーザーに配れるページ数が極端に制限される。なぜならば、複数のプロセスによって同一の物理メモリを参照した場合には、メモリ保護を行う事が (恐らく) 不可能である為である。
例えば、1024 MB のメモリを搭載したマシンで onix を動かしたときを考える。搭載されたメモリの 3/8 はカーネルによって使われ、5/8 がユーザーが使える領域となるので、一つのプロセスが一つのページを利用した場合でも、最大で 160 個のプロセスしか起動できない事になる。
数 TB のメモリを積んだ 64 bitマシンで、頻繁にページテーブルがストレートマッピングされるような環境でも無い限り、この設計ではとても運用できない。
そこで、ユーザーランドのメモリ領域として予約してあるシステムの全体の 5/8 のメモリ領域のみ通常のページング方式によるマッピングを行い、それ以外のカーネル領域では拡張ページング方式によるアドレス解決を行う事にした。
カーネル領域において、従来通りの方法を採った理由は二つ。一つは、カーネル領域をアクセスするのはカーネルだけであるので、カーネルのメモリ管理機構によって拡張ページをスラブに分割してカーネルに供給する分には、ユーザー側で発生するような問題は発生せず、物理メモリの先頭からストレートマッピングしているだけの構造において、ページテーブルを参照するのは無駄である。という理由と、書き直すのが面倒という理由によって、拡張ページと通常ページが混在するシステムとなった。
次に、ELF 実行形式ファイルを Linux 側で作成して onix で実行する。
onix には、現在シェルが存在せず、カーネルモードによって実行するデバッグ用のシェルしかない。よって次のようにしてプログラムを実行する。
まず、プログラムカーネル起動時にプロセスを一つ生成して、デバッグシェルを起動。そして、"start timer" コマンドによってキューに存在する他のプロセス (プログラムファイルを実行するプロセス) に切替える。そして、プログラムを実行するプロセスは、プログラムの処理を終えたならば戻り値を受け取り、その値を表示する。
Linux による外部プログラムの実行時に処理が行われる load_elf_binary() では、システムコールのサービスルーチンから直接呼び出し、プロセスの制御を外部プログラムに移しているが。ここでは、ユーザープロセスから直接ロードを行って、エントリポイントを関数のポインタとしてユーザープログラムに渡し、関数呼び出しでプログラムの処理を実行する。
上述したように、ここで実行させるプログラムは、共有ライブラリをリンクせず、自前のコードのみを実行するプログラムである。とはいっても、出力に何も表れないのはさみしいので、write システムコールを呼び、標準出力に文字列を表示させる。コードは以下のようになる。
int main()
{
write(1,"FOO BAR baz\n",12);
return 0xaabbccdd;
}
当然だが、write のラッパールーチンも自前で書かなければならない。なので、次のコードを静的にリンクする。
.code32
.text
.global write, _start
_start:
jmp main
write:
movl $0x4, %eax # syscall number
movl 0x04(%esp), %ebx # first argument
movl 0x08(%esp), %ecx # second argument
movl 0x0c(%esp), %edx # third argument
int $0x80
ret
そして、-nostdlib オプションをつけてコンパイル・リンクして得た実行ファイルを onix で実行すると、次の用になる。
このようにカーネル側で、ユーザープロセスに関するメモリ管理機構が機能していれば、この手のプログラムを実行するのは難しい話では無い。しかし、動的リンクを行う場合にはちょっと話が難しくなる。Linux では、そのあたりをカーネル側ではサポートせずに、ライブラリに依存した形で実現している。これについては、次回以降で話をする。
- Category(s)
- 学習経過
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/ohyama/30ab30fc30cd30eb-305d306e8/tbping