ブートローダ編② bootmain.c (Xv6を読む~OSコードリーディング~)

前回
jupiteroak.hatenablog.com
トップページ
jupiteroak.hatenablog.com




bootmain.c
https://github.com/mit-pdos/xv6-public/blob/master/bootmain.c

// Boot loader.
//
// Part of the boot block, along with bootasm.S, which calls bootmain().
// bootasm.S has put the processor into protected 32-bit mode.
// bootmain() loads an ELF kernel image from the disk starting at
// sector 1 and then jumps to the kernel entry routine.

#include "types.h"
#include "elf.h"
#include "x86.h"
#include "memlayout.h"

#define SECTSIZE  512

void readseg(uchar*, uint, uint);

void
bootmain(void)
{
  struct elfhdr *elf;
  struct proghdr *ph, *eph;
  void (*entry)(void);
  uchar* pa;

  elf = (struct elfhdr*)0x10000;  // scratch space

  // Read 1st page off disk
  readseg((uchar*)elf, 4096, 0);

  // Is this an ELF executable?
  if(elf->magic != ELF_MAGIC)
    return;  // let bootasm.S handle error

  // Load each program segment (ignores ph flags).
  ph = (struct proghdr*)((uchar*)elf + elf->phoff);
  eph = ph + elf->phnum;
  for(; ph < eph; ph++){
    pa = (uchar*)ph->paddr;
    readseg(pa, ph->filesz, ph->off);
    if(ph->memsz > ph->filesz)
      stosb(pa + ph->filesz, 0, ph->memsz - ph->filesz);
  }

  // Call the entry point from the ELF header.
  // Does not return!
  entry = (void(*)(void))(elf->entry);
  entry();
}


処理の内容

物理メモリアドレス0x1 0000以降にカーネルプログラムの一部(ELFファイルのヘッダー)をロードする

elf = (struct elfhdr*)0x10000;  // scratch space

  // Read 1st page off disk
  readseg((uchar*)elf, 4096, 0);

  // Is this an ELF executable?
  if(elf->magic != ELF_MAGIC)
    return;  // let bootasm.S handle error

ハードディスク上にあるカーネルプログラム(kernel)のうち、先頭から4096バイトまでの領域を、物理アドレス0x1 0000にロードします。


elf = (struct elfhdr*)0x10000;
ELFファイルのヘッダーを表現したelf構造体へのポインタに、物理アドレス0x1 0000を格納しています。
次の処理で、物理アドレス0x1 0000にカーネルプログラムの一部(ELFファイルのヘッダー)がロードされるので、elf構造体で定義された各メンバとカーネルプログラムのELFヘッダーにある各フィールドが対応するようになります。


readseg( (uchar*)elf, 4096, 0 );
readseg関数を呼び出して、ハードディスク上にあるカーネルプログラム(ELFファイル)の先頭(バイトオフセット0)から、elf(0x1 0000)を先頭アドレスとするメモリ領域へ、4096バイトのデータをロードします。
第1引数のelf(0x1 0000)は、ロード先となるメモリ領域の先頭アドレスです。
第2引数の4096は、ロードされるデータのサイズ(バイト単位)です。
第3引数の0は、カーネルプログラム(ELFファイル)先頭からの位置をバイトオフセットとして示した値です。


if(elf->magic != ELF_MAGIC) 
return;
前の処理で部分的にロードされたカーネルプログラム(kernel)の先頭4バイト(elf->magic)が、ELFファイル固有のマジック・ナンバであるかどうかを確認しています(これからロードされるカーネルプログラムがELFファイルであるかを確認しています)。
ELFファイルの先頭4バイト(elf->magic)には、ELFファイル固有のマジック・ナンバが格納されています。
マジック・ナンバは、メモリの下位アドレスから、0x7F、E、L、F と並んでいます。これを16進数のASCIIコードになおすと、0x7F、0x45(E)、0x4C(L)、0x46(F)となります。さらにこれをソースコード上の表記になおすと0x 46 4C 45 7F Uとなります。(符号なし整数として扱うために末尾にUをつけています。elf->magicのデータ型はuintです。)
elf->magic != ELF_MAGIC が真となる場合→部分的にロードされたカーネルプログラムの先頭4バイト(elf->magic)がELF_MAGIC(0x464C457FU)ではない場合は、bootasm.Sに処理を戻し、エラー処理を行わせます。

残り全てのカーネルプログラムを読み込む

// Load each program segment (ignores ph flags).
  ph = (struct proghdr*)((uchar*)elf + elf->phoff);
  eph = ph + elf->phnum;
  for(; ph < eph; ph++){
    pa = (uchar*)ph->paddr;
    readseg(pa, ph->filesz, ph->off);
    if(ph->memsz > ph->filesz)
      stosb(pa + ph->filesz, 0, ph->memsz - ph->filesz);
  }

ELFファイル内にあるプログラムヘッダの情報に基づいて残り全てのカーネルプログラムを読み込んでいきます。


プログラムヘッダとプログラムヘッダ・テーブルについて
ELFファイル内にある命令やデータはいくつかの領域に分割して管理され、この分割された領域はセグメントと呼ばれています。
ELFファイル内では、セグメントについての情報はプログラムヘッダと呼ばれるデータ構造に記述され、1つのセグメントにつき1つのプログラムヘッダが用意されています。
ELFファイルのヘッダーの直後には、複数のプログラムヘッダが連続して並んだ配列があり、このプログラムヘッダの配列をプログラムヘッダ・テーブルと言います。


ph = (struct proghdr*)( (uchar*)elf + elf->phoff );
プログラムヘッダ・テーブルの先頭アドレス→プログラムヘッダ・テーブルのエントリ0を指定するアドレス を取得しています。
proghdr構造体はプログラムヘッダを表現したデータ構造です。
elf->phoff(ELFファイルのヘッダーにあるphoffフィールド)には、プログラムヘッダ・テーブルの位置をELFファイル先頭からのバイトオフセットで示した値が格納されています。そのため、phの値( (uchar*)elf + elf->phoffの値)は、プログラムヘッダ・テーブルの先頭アドレス→プログラムヘッダ・テーブルのエントリ0を指定するアドレスとなります。


eph = ph + elf->phnum;
プログラムヘッダ・テーブルの最終エントリを指定するアドレス+32(0x20)の値を取得しています。
elf->phnum(ELFファイルのヘッダーにあるphnumフィールド)は、ELFファイル内にあるプログラムヘッダの個数を示す値です。
proghdr構造体へのポインタph(プログラムヘッダ・テーブルの先頭アドレス→プログラムヘッダ・テーブルのエントリ0を指定するアドレス)にelf->phnumを加算すると、phに格納されているアドレスに、32(proghdr構造体のサイズ) × phnumバイトが加算されます。そのアドレスはプログラムヘッダ・テーブルの最終エントリを指定するアドレス+32(0x20)の値です(eph = ph + (elf->phnum - 1) であれば、ephに格納されるアドレスは、プログラムヘッダ・テーブルの最終エントリを指定するアドレスになります)。

全てのプログラムヘッダを使用してカーネルプログラム内にある全てのセグメントをロードする

for(; ph < eph; ph++){
    pa = (uchar*)ph->paddr;
    readseg(pa, ph->filesz, ph->off);
    if(ph->memsz > ph->filesz)
      stosb(pa + ph->filesz, 0, ph->memsz - ph->filesz);
}

全てのプログラムヘッダ(プログラムヘッダ・テーブルにある全てのエントリ)を使用して、カーネルプログラム内にある全てのセグメントをロードし、カーネルプログラムのロードを完了させます。


readseg(pa, ph->filesz, ph->off);
readseg関数を呼び出して、ハードディスク上にあるカーネルプログラム(ELFファイル)内のph->offに位置するセグメントを、pa(ph->paddr)を先頭アドレスとするメモリ領域へロードします。
第1引数のpa(ph->paddr プログラムヘッダのpadrrフィールド)は、セグメントのロード先となるメモリ領域の先頭アドレスを示しています。
第2引数のph->filesz(プログラムヘッダのfileszフィールド)は、ロードされるセグメントのデータサイズ(バイト単位)を示しています。
第3引数のph->off(プログラムヘッダのoffフィールド)は、ELFファイルにおけるセグメントの位置を示しています。
これをプログラムヘッダ・テーブルにある全てのエントリに対して行うことで、カーネルプログラム内の全てのセグメントをロードし、カーネルプログラムのロードを完了させます。(エントリ0のプログラムヘッダphから、最終エントリのプログラムヘッダeph-1まで、順番に繰り返し処理を行います。)


if(ph->memsz  >  ph->filesz)
stosb(pa + ph->filesz, 0, ph->memsz - ph->filesz);
ph->memsz(プログラムヘッダのmemszフィールド)はセグメントがメモリ上に展開される時のデータサイズ、ph->filesz(プログラムヘッダのfileszフィールド)はセグメントがELFファイル中にある時のデータサイズ、を示しています。
本来は、ph->memsz と ph->filesz は同じ値になるはずですが、BSS領域(ファイル中に実体がないセクション)がセグメント中に含まれる場合には、ph->memsz  > ph->fileszとなります。
その場合、stosb関数を使ってBSS領域を0の値で初期化する処理を行います。
stosb関数は、先頭アドレスがpa + ph->filesz・サイズがph->memsz - ph->fileszとなるメモリ領域を、0を使って初期化します(特定のメモリ領域を指定された値で埋めます)。
第1引数のpa + ph->fileszは、BSS領域の先頭アドレスを指定する値です。
第2引数の0は、初期化で使用する値です。
第3引数のph->memsz - ph->fileszは、BSS領域のサイズを指定する値です。

ELFファイルのヘッダー情報を使ってカーネルプログラムのエントリポイントへ制御を移す

entry = (void(*)(void))(elf->entry);
entry();


entry = (void(*)(void))(elf->entry);
elf->entry(ELFファイルのヘッダーにあるentryフィールド)は、ELFファイル内にある_startラベルのアドレス値(エントリポイント、実行開始アドレス)を示しています。この値を 戻り値void型・引数なしの関数 へのポインタentryに格納します。


entry();
entryを実行し、entry.Sにあるentryラベルのアドレスへジャンプします。

elf->entryのアドレス値について

elf->entry(ELFファイルのヘッダーにあるentryフィールド)には、ELFファイル内にある_startラベルのアドレス値が格納されていますが、そのアドレスは仮想アドレス(VMA:リンク時に使用されるアドレス、または、ページングが機能してる時に使用されるアドレス)として扱われることを想定しています。
リンカスクリプトkernel.ldを読むと、ロケーションカウンタの値が0x8010 0000に設定されているため、本来であればstartラベルの仮想アドレス値(elf->entryの値)は、0x8010 0000以降の値(正確には、0x8010 000c)になります。

kernel.ld

/* Simple linker script for the JOS kernel.
   See the GNU ld 'info' manual ("info ld") to learn the syntax. */

OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
OUTPUT_ARCH(i386)
ENTRY(_start)

SECTIONS
{
	/* Link the kernel at this address: "." means the current address */
        /* Must be equal to KERNLINK */
	. = 0x80100000;

	.text : AT(0x100000) {
		*(.text .stub .text.* .gnu.linkonce.t.*)
	}

(以下省略)

しかし、リンカスクリプトのAT(0x10 0000)によってtext領域が0x10 0000にロードされるので、_startラベルはメインメモリ上にある時は0x10 0000以降のアドレス(正確には、0x10 000c)に位置しています。つまり、_startラベルのアドレス値(elf->entryの値)は0x8010 000cですが、メインメモリ上にあるときの_startラベルは0x10 000cのアドレスに位置していることになります。
このままentry()を実行すると、elf->entryが指定する0x8010 000cのアドレスへジャンプすることになってしまい、メインメモリ上にあるときの_startラベルが位置するアドレス0x10 000cから処理を開始することができません(この時点では、まだ、ページング設定をオンにしていないので、_startラベルが位置するアドレス0x10 000cは物理アドレス値となっています)。
この問題を解決するために、_startラベルがあるentry.Sでは次のような記述を行なっています。

entry.s

.globl _start
_start = V2P_WO(entry)

# Entering xv6 on boot processor, with paging off.
.globl entry
entry:

(以下省略)

startラベルに、V2P_WOマクロを使って値を設定することにより、本来(0x8010 000c)とは異なる仮想アドレス(0x10 000c)が割り当てられています。
entryラベルの値(entryラベルの参照値は仮想アドレス0x8010 000c)とV2P_WOマクロを使って、_startラベルに割り当てる仮想アドレスを無理やり0x10 000cにしています。

memorylayout.h

#define KERNBASE 0x80000000
#define V2P_WO(x) ( (x) - KERNBASE )

これにより、elf->entryに格納されている値(_startラベルのアドレス値)を0x10 000cに変更することができます。
そのため、entry関数を実行しても、問題なく_startラベルに割り当てられてたアドレス(0x10 000c)から処理を開始することができるようになります。この時点では、ページング機能がオンになっていないので、_startラベルに割り当てられている仮想アドレスは、物理アドレスとして使用されます。

readelf -s kernel
85: 8010000c 0 NOTYPE GLOBAL DEFAULT 1 entry
303: 0010000c 0 NOTYPE GLOBAL DEFAULT 1 _start

https://qiita.com/knknkn1162/items/cb06f19e1f999bf098a1 より引用
https://gist.github.com/knknkn1162/09e9a8c12e0a4ea0db07deb1d7bb4c19#file-kernel_readelf_s-log-L90
https://gist.github.com/knknkn1162/09e9a8c12e0a4ea0db07deb1d7bb4c19#file-kernel_readelf_s-log-L308




次回
jupiteroak.hatenablog.com