linuxの話 (ELF ローダ)
長らく手つかずだった ELF ローダの製作に着手する。
その前に linux において ELF 実行形式ファイルを実行した場合に、どのような処理がカーネルによって行われるのかを調べる。概念的な疑問やカーネルにおける実装方法について不明な点はあまり無いが、ソースを読んで仕組みを確認するという行為そのものに意味がある。
なので、以下では execve システムコールの処理について見てゆく。
まずは、動作の流れの予想を考える。
execve システムコールは、引数に現在のプロセスで実行させる実行ファイルプログラムのパスを取り、システムコールを発行する。
ここでの処理内容としては、恐らくパス名から実行ファイルを探し、そして実行ファイルを読み込んで、実行ファイルのエントリポイントを取得し、ユーザーのレジスタ情報 (実行アドレス等) を書き換えて、ユーザープロセスに処理を返すものと思われる。
次に、実際の処理について見てゆく。
execve() システムコールによって、linux の sys_execve() サービスルーチンが呼び出される。システムコールが発行されると、eax レジスタにシステムコール番号 (この場合 0xb) が入り、ebx レジスタにファイル名のパス。ecx レジスタには 引数のアドレスの配列のポインタが入る。そして、edx レジスタには、ecx と同じように環境変数のそれが入る。
sys_execve() では、レジスタの ebx に格納されているファイル名の妥当性をチェックして、do_execve() を呼び出す。
以下では、do_execve() の処理について見てゆく。
まず unshare_files () 関数を呼び出し、オープンしているファイル情報を独立させる。通常、プロセス生成時にプロセス毎のオブジェクトが生成され、プロセスディスクリプタと関連づけられるのだが、全てのプロセスに対してこれを行った場合、fork 実行時のオーバーヘッドが大きくなる為、fork 実行時にプロセス情報の生成・初期化を初期化せずに、これらが更新されるまで親プロセスの各種オブジェクトを共有する(軽量プロセス)。一般的に fork を実行した後、直ちに exec を実行するケースが多い (らしい) ので、この手法によってプロセス生成時のパフォーマンス向上が見込める。ここではオープンしたファイル情報に関して、共有の解除を行っている。
次に、linux_binprm 型という、ちょっと見慣れない構造体のオブジェクトを取得している。これは、ユーザー空間にある引数をカーネル側で保持・加工する為のデータ構造らしい (include/linux/binfmts.h より)。
次に、open_exec() 関数を呼び出し、引数で指定したファイル名のファイルオブジェクトを取得する。ここでは、パス名検索に do_path_lookup_open() 関数を使っている。コメントには、open する意志をもってパス名検索を行うとあるが、実際には nameidata オブジェクトの intent メンバのいくうつかを予め設定して、結局 do_path_lookup を呼び出している。これについては、これ以上調べない事にする。
さて次に、init_new_context() 関数を実行している。これはアーキテクチャ依存関数であり、IA32 では、現在実行中のプロセスの LDT を引き継ぐ処理を行っている。
次に、引数と環境変数の数を取得して、linux_binprm オブジェクトを更新し、prepare_binprm() 関数を呼び出す。ここでは、指定したファイルのソフトウェア inode から、linux_binprm オブジェクトを設定する。そして、kernel_read() を呼び出し、ファイルの先頭から BINPRM_BUF_SIZE だけディスクファイルシステムから linux_binprm オブジェクトの buf メンバに読み込む。
include/linux/binfmts.h で BINRPM_BUF_SIZE は 128 と定義されている。この数字には意味があり、ファイルの先頭 128 バイトにファイルのマジック番号が存在する為、これを見て後にロードするファイルの種類を特定する。ここでは、それを行う為に、BINPRM_BUF_SIZE のデータをファイルから取得している。
次に、copy_strings() 関数や copy_strings_kernea() 関数を呼び出し、ファイル名、引数、そして環境変数を linux_binprm オブジェクトに設定する。copy_strings_kernel() で、カーネル空間からコピーしているのは、sys_execve() で getname によってユーザー空間から既にファイル名を取ってきた為。
そして、search_binary_handler() 関数を呼び出す。
ここでは、指定した実行形式ファイルに適合した実行形式でバイナリファイルを実行処理させる。具体的には、カーネル初期化時に登録された ELF ローダの load_binary メソッドを呼び出す。elf の linux_binfmt オブジェクトの load_binary メソッドが実行する実態は、load_elf_binary [fs/binfmt_elf.c] で定義されている。
linux_binprm に似た名前の見慣れない linux_binfmt だが、当然ながらその存在異義はまるで違う。このデータ構造はロードするバイナリファイルで利用される機能を定義したもの。全ての linux_binfmt オブジェクトは、リストに繋げられていて、format 変数がその先頭をさす。
又、このデータ構造は (恐らく) 各実行形式毎に存在しており、その実行形式で実行する 3 つの方法 (インターフェイス) を用意している。ここでは、通常の実行方法について見るため load_binary という実行メソッドについて注目する。
以下では、elf 実行形式における linux_binfmt オブジェクトの load_binary メソッドから実行される load_elf_binary() 関数の処理について見てゆく。
この関数内で使われている ELF ヘッダのデータ構造 struct elfhdr は 32 bit アーキテクチャと 64 bit アーキテクチャでデータ構造が異なる。32 bit の場合、[include/linux/elf.h] で定義されている elf32_hdr 構造体というデータ構造を持つ。
まず、実行形式ファイルの ELF ヘッダ部分に相当する領域を読み込む為のオブジェクトのメモリ領域の確保を行い、prepare_binprm() 関数で取得した大きさ BINPRM_BUF_SIZE の実行形式ファイルの先頭バッファを ELF ヘッダオブジェクトに読ませる。そして、ファイルのヘッダ部分のマジックナンバーや実行タイプや実行環境 (アーキテクチャ) 等を調べる。
そして、load_elf_interp() 関数を呼び出し、対象の実行形式ファイルのエントリポイントを取得し、start_thread() マクロで、ユーザープロセスのレジスタセットの eip を実行ファイルのエントリポイントに設定する。これによってシステムコールから帰ると、ユーザープロセスは、execve で指定したファイルの実行を開始する。
全体的な処理の流れはこれでよさそうだが、ファイルが動的リンクファイルの場合、load_elf_interp() 関数で、ファイルのエントリポイントが修正されている。ここにはローダが指定したリニアアドレスが書かれている為、ローダの要求通りのメモリマッピングを行えば、エントリエントリポイントのアドレスを修正する必要は無さそうだが、内部で mmap 処理を行い、戻り値のアドレスで修正している。
動的リンクの場合の ELF ローダの処理については把握しきってないので、これについての調査は、未来の自分に任せる事にする。
- Category(s)
- 学習経過
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/ohyama/linux306e8a71-elf-30ed30fc/tbping