elf_exec関数

elf_exec関数は、引数として渡されるKernelのELF Headerを解析し、Kernelのセグメントやセッションを読込みメモリー上に展開します。

  1. プログラム ヘッダ テーブルを参照して、TEXTまたはDATAセグメントをメモリ上に展開する。
  2. セクション ヘッダ テーブルを参照して、SYMBOLまたはSTRINGテーブルをメモリ上に展開する。
  3. ELFヘッダをメモリ上に展開する。

elf_exec関数

引数で渡されるKernelのELF Header

ELF Headerはboot関数から渡され、OpenBSD 3.6 GENERIC Kernel(i386)のELF Headerの場合、次の値になります。

OpenBSD 3.6 GENERIC Kernel(i386)のELF Header
構造体名メンバー名詳細
elfe_ident[EI_NIDENT]index内容
EI_MAG0'\177'マジックNO.0byte目
EI_MAG1'E'マジックNO.1byte目
EI_MAG2'L'マジックNO.2byte目
EI_MAG3'F'マジックNO.3byte目
EI_CLASSELFCLASS32バイナリファイルは32bit アーキテクチャ
ファイル空間と仮想アドレス空間が 4 ギガバイトまでのマシン
EI_DATAELFDATA2LSB|プロセッサのバイトオーダーはリトルエンディアン
EI_VERSIONEV_CURRENTELFのHeader Versionはカレントバージョン
EI_OSABI0 (未使用?)オペレーティングシステムとABIを識別
EI_ABIVERSION0 (未使用?)ABIのバージョンを識別
EI_PAD0パディングの始め
10-150未定義(0 パディング)
e_typeET_EXECオブジェクトファイルタイプが実行可能ファイル
e_machineEM_386アーキテクチャはIntel 80386
e_versionEV_CURRENTELFのFile Versionはカレントバージョン
e_entry0xD0100120プロセスを開始する仮想アドレス
e_phoff0x34プログラムヘッダテーブルが存在する場所のファイルオフセット値(byte)
e_shoff0x4B0D14セクションヘッダテーブルが存在する場所のファイルオフセット値(byte)
e_flags0x00ファイルに関連するプロセッサに固有なフラグ(未定義)
e_ehsize0x34ELF ヘッダのサイズ(byte)
e_phentsize0x20プログラムヘッダテーブルにあるエントリ 1個のサイズ
e_phnum0x01プログラムヘッダテーブル中のエントリの個数
e_shentsize0x28セクションヘッダテーブルにあるエントリ 1個のサイズ
e_shnum0x08セクションヘッダテーブル中のエントリの個数
e_shstrndx0x05セクションヘッダテーブルの、
セクション名文字列テーブルに結びつけられたエントリへのインデックス

このデータを見ると、Kernel Fileの構造が次のように構成されているのがわかります。

  1. ELF Headerのサイズは0x34 byte (e_ehsize)
  2. Program Headerが1つ(e_phnum)存在する。
    Program Headerのサイズは0x20 byte(e_phentsize)であり、ファイルの先頭から0x34 byte目(e_phoff)に位置する。
  3. Section Headerが8つ(e_shnum)存在する。
    各Header サイズは0x28 byte(e_shentsize)であり、ファイルの先頭から0x4B0D14 byte目(e_shoff)に位置する。
ELF Headerから見たkernelのファイルマップ
ELF Headerから見たkernelのファイルマップ

不明なエリアが存在しますが、このエリアの構成情報はProgram HeaderSection Headerに記述されています。Kernelをロードするためにはこれらのヘッダを読込み解析する必要があります。

Program Headerの読込み

Program Headerは、プログラム実行にシステムが必要とするセグメントなどの情報が定義されています。この関数ではProgram Headerの情報をもとに実行コードの読込みを行ないます。

Program Header Lengthを求める

Program Header Lengthを求めるには、ELF Headerにあるe_phentsize(Program Header Size)e_phnum(Program Headerの個数)の積を求めればよい。求めたサイズ分読込むためのメモリを確保する。

Program Header Read

読出し位置をProgram Headerが存在するe_phoffに移動し、確保したメモリにProgram Headerを読込む。読込んだ値は次のようになる

読込んだProgram Headerの内容
変数詳細
p_typePT_LOADこの配列要素は、ロード可能なセグメントであること示す。
p_offset0x120このセグメントが存在するファイルのオフセット。
p_vaddr0xD0100120このセグメントの仮想アドレス。
p_paddr0xD0100120物理アドレッシング上の物理アドレス。
p_filesz0x4B0BBCセグメントのファイルイメージのバイト数をを示す。
p_memsz0x5829B0セグメントのメモリイメージのバイト数を示す。
p_flags0x07
(PF_X | PF_W | PF_R)
セグメントの属性を表すフラグ。(実行・書込・読取可)
p_align0x20alignment値

Program Headerを読込んだ事によりKernel Fileの不明エリアの構造がわかる。

  1. このセグメントは実行・書込・読取可である。(p_flags)
  2. セグメントの始まりはfileの先頭から 0x120 Byte (p_offset)に位置しており、セグメントの大きさは0X4B0BBC Byte (p_filesz)ある。
Program Headerから見たkernelのファイルマップ
Program Headerから見たkernelのファイルマップ

セグメントの読込み

セグメントタイプのチェック

読込み対象のセグメントが、ロード可能(p_type == PT_LOAD)で実行・書込・読取可(p_flags & (PF_W|PF_R|PF_X))であるかチェック該当しないセグメントはスキップする。

Load Flagのチェック

現在のセグメントが、引数で渡されたLoad Flagの要求とマッチするか調べ、マッチするならセグメントの読込みを行なう。

現在のセグメントを読込

最初に、ロードの進捗状況(p_fileszの値)を画面に表示する。

Using drive 0 , partition 4 .
Loading........
probing:pc0 com1 com2 apm pci mem[639K 253M a20=on]
disk:fd0 hd0+
>>OpenBSD/i386 BOOT 2.06
boot>
booting /dev/rhd0a:/bsd: 4918204
	  

次に、読出し位置をp_offsetに移動し、p_filesz byteをMemory上のp_vaddr番地に読込む。

読込みに使われる、READはマクロになっていて(ヘッダを読込む関数はread関数であることに注意)

次のように定義されている

 #define READ(f, b, c)           read((f), (void *)LOADADDR(b), (c))
	    

このマクロでは、読出し位置がLOADADDRマクロにより変更される。

LOADADDRマクロは、下記のように定義されていて仮想アドレスからリニアアドレスへの変換を行なう。

#define LOADADDR(a)             ((((u_long)(a)) + offset)&0xfffffff)
	    

phdr[i].p_vaddr=0xd0100120でoffset=0(DEFAULT_KERNEL_ADDRESS)の場合、読出し先アドレスは次のように変換される。

((((u_long)(0xd0100120)) + 0)&0xfffffff) = 0x100120
	    

何故このような変換を行なっているかと言うと、現段階では仮想メモリを制御する機能が働いていない。このため、データを一度リニアアドレス上に置く必要がある。この後、仮想メモリを設定する所で、読出した先のリニアアドレスを仮想アドレスの番地(この場合0xd0100120)に再マッピングを行なう。


リニアアドレス上の読込んだデータは次のようになる。

メモリマップ
メモリマップ
セグメントの最上位・最下位アドレスを計算

読込んだセグメントの仮想メモリ上の最上位(maxp)・最下位アドレス(minp)のアドレスを求める。

maxpとminp
maxpとminp
bssをzeroクリア

メモリ上に読込んだセグメントデータの中にはbssのエリアは含まれていない。何故ならば、bssは初期値を持たない変数の領域なのでファイル中に値として持っていても意味がない(というかdiskの無駄)だからだ。ではどのように作成するかというと、Program Headerは次の変数を持っている。

イメージのバイト数
p_filesz0x4B0BBCセグメントのファイルイメージのバイト数をを示す。
p_memsz0x5829B0セグメントのメモリイメージのバイト数を示す。

これらの、メモリ上のサイズ(p_memsz)とファイルのサイズ(p_filesz)の差分がbss領域の大きさとなる。

bss領域の大きさ
bss領域の大きさ

最初に、クリアする前に進捗としてbssのサイズを画面に表示

Using drive 0 , partition 4 .
Loading........
probing:pc0 com1 com2 apm pci mem[639K 253M a20=on]
disk:fd0 hd0+
>>OpenBSD/i386 BOOT 2.06
boot>
booting /dev/rhd0a:/bsd: 4918204+859636
	  

その後、bss領域をzeroクリアする。

zeroクリアする際に使われる、BZEROもマクロになっており、次のように定義されています。

#define BZERO(d, c)             memset((void *)LOADADDR(d), 0, (c))
	    

READマクロと同じように、仮想アドレスからリニアアドレスへの変換を行なっている。

セグメントの最上位アドレスの更新

最上位アドレス(maxp)にbssのサイズを加算

maxpの更新
maxpの更新

最上位アドレス更新

maxpの丸め

最上位アドレス(maxp)をsizeof(long)の倍数アドレスに切り上げる。

maxpにELF Headerサイズ分加算

次の条件を満たしていれば、ELF Headerの領域を確保してmaxpを更新する。

if((flag=="ELF Headerを読込")  && (flag=="ELF Headerをカウント"))
  maxp += sizeof(ELF Header)	    
最上位アドレス更新
最上位アドレス更新

Section Headerの読込み

Section Headerは、プログラムコードや読取り専用データ、読書き可能データ、再配置エントリー、シンボル等の情報が種類ごとに格納されている。ここではSection Headerをファイルから読込み、さらにSection Headerが示すSymbol Tableの内容も読込む

if( (flagがシンボルを読込) || (flagがシンボル sizeをカウント)){
  Disk 読取り位置をelf->e_shoffに移動
  Section Header Lengthを計算 ( elf->e_shnum * sizeof(Elf_Shdr) )
  Section Header Read
  
  for(Section Hederの数だけループ){
    if(現在のSection Headerがシンボルテーブルか文字列テーブル ){
      Disk 読取り位置をshp[i].sh_offsetに移動
      Table Read

    }
  }
}	
Section Headerの読込み

読取り位置をe_shoffに移動させ、Section Headerサイズの領域をアロケートしてから、アロケート領域に読込みを行う。読込んだ値は下記のようになる。

Section Headerの内容
構造体名変数shdr[0]shdr[1]shdr[2]shdr[3]shdr[4]shdr[5]shdr[6]shdr[7]
shdrsh_name0x00x1B0x210x290x2F0x110x10x9
sh_typeSHT_NULLSHT_PROGBITSSHT_PROGBITSSHT_PROGBITSSHT_NOBITSSHT_STRTABSHT_SYMTABSHT_STRTAB
sh_flags0x00x70x20x30x30x00x00x0
sh_addr0x00xD01001200xD048FCE00xD05924200xD05B0CE00x00x00x0
sh_offset0x00x1200x38FCE00x4924200x4B0CE00x4B0CE00x4B0E540x4E92D4
sh_size0x00x38FBA80x10273D0x1E8BC0xD1DF00x340x384800x33123
sh_link0x00x00x00x00x00x00x70x0
sh_info0x00x00x00x00x00x00x80x0
sh_addralign0x00x100x200x200x200x10x40x1
sh_entsize0x00x00x00x00x00x00x100x0

Section Headerを読込んだ事によりKernel FileのSection構造がわかる。

Kernel File 上のセクション位置
Kernel File 上のセクション位置
maxpにSection Headerサイズ分加算

maxpにSection Headerの値(丸めたもの)を加算し、off(ELF HeaderとSection Headerの合計サイズを丸めたもの)を求める

Section Headerサイズ加算
Section Headerサイズ加算
Symbol Sectionのチェック

Section Headerを検索してSymbol table(sh_type = SHT_SYMTAB)が存在するかチェック。見つかった場合は、変数havesymsに1をセット。

Symbol Sectionの読込み

Section Headerを検索して、Symbol table(sh_type = SHT_SYMTAB)とString table(sh_type = SHT_STRTAB)が見つかったならば、メモリ上に展開する。読込んだ後に変数maxpと変数offを更新する。またSection Headerのsh_offsetをメモリ上のアドレスに更新する。

Symbol Sectionの読込み
Symbol Sectionの読込み

読込んだtableのサイズを画面に表示

Using drive 0 , partition 4 .
Loading........
probing:pc0 com1 com2 apm pci mem[639K 253M a20=on]
disk:fd0 hd0+
>>OpenBSD/i386 BOOT 2.06
boot>
booting /dev/rhd0a:/bsd: 4918204+859636 [52+230528+209187]
	  
Section Headerのコピー

アロケートしたSection Headerをshppの示すエリアにコピーしメモリを解放する。

Section Headerのコピー
Section Headerのコピー

後処理

ELF Headerのコピー

ELF Headerのポインタelfpが示すエリアにコピーする。

ELF Headerのコピー
ELF Headerのコピー

ELF Headerをコピーする前に、Headerを変更する。Program Headerは存在しないのでこれらの情報は削除し、Section Headerへのポインタを変更する。

e_phoff = 0;
e_shoff = sizeof(Elf_Ehdr);
e_phentsize = 0;
e_phnum = 0;	  

変更後のメモリ上のヘッダ情報は下記のような関係になる。

変更後ヘッダ情報
変更後ヘッダ情報
marks配列に値をセット

marks配列に以下の値をセットする。この配列は上位の関数で使用される。

marks[MARK_START] = LOADADDR(minp);         /* リニアアドレス上のKernel先頭アドレス */
marks[MARK_ENTRY] = LOADADDR(elf->e_entry); /* リニアアドレス上のプロセスを開始するアドレス */
marks[MARK_NSYM] = 1;                       /* XXX: Kernel needs >= 0 */
marks[MARK_SYM] = LOADADDR(elfp);           /* リニアアドレス上ELF Headerのアドレス */
marks[MARK_END] = LOADADDR(maxp);           /* リニアアドレス上のKernelの最後尾アドレス */
	  
return

loadfile関数に戻る。

Last modified: Sat Jan 26 16:10:13 2008 JST