OS起動編⓪ entry.S (Xv6を読む~OSコードリーディング~)

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




entry.s
https://github.com/mit-pdos/xv6-public/blob/master/entry.S

# The xv6 kernel starts executing in this file. This file is linked with
# the kernel C code, so it can refer to kernel symbols such as main().
# The boot block (bootasm.S and bootmain.c) jumps to entry below.
        
# Multiboot header, for multiboot boot loaders like GNU Grub.
# http://www.gnu.org/software/grub/manual/multiboot/multiboot.html
#
# Using GRUB 2, you can boot xv6 from a file stored in a
# Linux file system by copying kernel or kernelmemfs to /boot
# and then adding this menu entry:
#
# menuentry "xv6" {
# 	insmod ext2
# 	set root='(hd0,msdos1)'
# 	set kernel='/boot/kernel'
# 	echo "Loading ${kernel}..."
# 	multiboot ${kernel} ${kernel}
# 	boot
# }

#include "asm.h"
#include "memlayout.h"
#include "mmu.h"
#include "param.h"

# Multiboot header.  Data to direct multiboot loader.
.p2align 2
.text
.globl multiboot_header
multiboot_header:
  #define magic 0x1badb002
  #define flags 0
  .long magic
  .long flags
  .long (-magic-flags)

# By convention, the _start symbol specifies the ELF entry point.
# Since we haven't set up virtual memory yet, our entry point is
# the physical address of 'entry'.
.globl _start
_start = V2P_WO(entry)

# Entering xv6 on boot processor, with paging off.
.globl entry
entry:
  # Turn on page size extension for 4Mbyte pages
  movl    %cr4, %eax
  orl     $(CR4_PSE), %eax
  movl    %eax, %cr4
  # Set page directory
  movl    $(V2P_WO(entrypgdir)), %eax
  movl    %eax, %cr3
  # Turn on paging.
  movl    %cr0, %eax
  orl     $(CR0_PG|CR0_WP), %eax
  movl    %eax, %cr0

  # Set up the stack pointer.
  movl $(stack + KSTACKSIZE), %esp

  # Jump to main(), and switch to executing at
  # high addresses. The indirect call is needed because
  # the assembler produces a PC-relative instruction
  # for a direct jump.
  mov $main, %eax
  jmp *%eax

.comm stack, KSTACKSIZE


処理の内容

multiboot_headerを定義する

.p2align 2
.text
.globl multiboot_header
multiboot_header:
  #define magic 0x1badb002
  #define flags 0
  .long magic
  .long flags
  .long (-magic-flags)

Multiboot Specificationという仕様で定められているmultiboot_headerというデータ構造を定義しています。


.p2align 2
.p2alignn(.p2alignディレクティブ)は、このディレクトリの直後にある命令やデータを、2^nバイト境界のアドレス(2^nバイトの倍数値となるアドレス)に配置させます。
ここでは、4バイト境界のアドレス(4バイトの倍数値となるアドレス)からデータが整列するようにp2alignディレクティブを指定しています。


.text
テキストセクションの開始位置を指定するディレクティブです。
このディレクティブ以降に配置されている命令は、entry.Sがアセンブルされて作成されたオブジェクトファイルのテキストセクションに配置されるようになります。


.globl multiboot_header
.globl(globlディレクティブ)を使って、multiboot_headerラベルが外部のファイルから参照できるようにしています。


multiboot_header:
.long magicが配置されているアドレスにラベルを付けます。
multiboot_headerラベルから、multiboot headerの定義が始まります。


.long magic
.long(longディレクティブ)は32bitの整数値を定義するために使用されるディレクティブです。
この32bitのデータ領域をmultiboot headerのmagicフィールドとして使用しています。
magicフィールドには、multiboot headerを識別するためのマジックナンバー(0x1badb002)を格納します。


.long flags
.long(longディレクティブ)は32bitの整数値を定義するために使用されるディレクティブです。
この32bitのデータ領域をmultiboot headerのflagsフィールドとして使用しています。
flagsフィールドには、OSイメージが必須としている、または、要求しているブートローダの特徴を指定する値を設定します。特に指定がないようなので、0の値を設定しているようです。


.long (-magic-flags)
.long(longディレクティブ)は32bitの整数値を定義するために使用されるディレクティブです。
この32bitのデータ領域をmultiboot headerのchecksumフィールドとして使用しています。
checksumフィールドには、magicフィールドとflagsフィールドを足した時の合計が0になる32bit符号なし整数値を設定するので、その値は -magic-flags となります。

4MBのページフレームを使用できるようにする

.globl _start
_start = V2P_WO(entry)

# Entering xv6 on boot processor, with paging off.
.globl entry
entry:
  # Turn on page size extension for 4Mbyte pages
  movl    %cr4, %eax
  orl     $(CR4_PSE), %eax
  movl    %eax, %cr4

CR4(コントロールレジスタ4)のbit4(PSEフラグ Page Size Extensionフラグ)を1にセットして、4MBのページフレームを使用できるようにします。


.globl _start
.globl(globlディレクティブ)を使って、_startラベルが外部のファイルから参照できるようにしています。


_start = V2P_WO(entry)
movl命令が配置されているアドレスにラベルを付けます。
V2P_WOマクロを使って、_startラベルを参照したときの値を書き換えています(詳細)。


.globl entry
.globl(globlディレクティブ)を使って、entryラベルが外部のファイルから参照できるようにしています。


entry:
movl命令が配置されているアドレスにラベルを付けます。


movl %cr4, %eax
CR4の値をeaxレジスタにコピーします。


orl $(CR4_PSE), %eax
第1オペランドの値CR4_PSEと第2オペランドのeaxの値をOR演算した結果を第2オペランドのeaxにセットします。
or命令を使うのはCR4のbit4(PSEフラグ)以外のbitの値が変更されないようにするためです。
また、or命令のオペランドにCR4を指定することはできないので、前の命令でCR4の値をeaxレジスタにコピーしています。


movl %eax, %cr4
eaxレジスタの値($CR4_PSEの値とCR4の値の論理和)をCR4にセットします。
この命令により、CR4のbit4(PSEフラグ)に1の値がセットされ、4MBのページフレームを使用できるようになります。

CR3にページディレクトリの物理アドレスを設定する

movl    $(V2P_WO(entrypgdir)), %eax
movl    %eax, %cr3

CR3にページディレクトリの物理アドレスを設定します。


movl $(V2P_WO(entrypgdir)), %eax
ページディレクトリの物理アドレスV2P_WO(entrypgdir)をeaxレジスタにコピーします。
ページディレクトリentrypgdirは、main.cに定義されています。
ソースコードコンパイルして作成されたELFファイルのシンボル(ラベル名、変数名、関数名など)には仮想アドレスが割り当てられるので、entrypgdirを参照した時に得られるアドレス値は仮想アドレスの値です。そのため、V2P_WOマクロを使って、ページディレクトリテーブルの仮想アドレスentrypgdirを物理アドレスに変換します。


movl %eax, %cr3
eaxレジスタの値(ページディレクトリテーブルentrypgdirの物理アドレス値)をCR3にセットします。

ページディレクトリentrypgdirについて

main.c

__attribute__((__aligned__(PGSIZE)))
pde_t entrypgdir[NPDENTRIES] = {
  // Map VA's [0, 4MB) to PA's [0, 4MB)
  [0] = (0) | PTE_P | PTE_W | PTE_PS,
  // Map VA's [KERNBASE, KERNBASE+4MB) to PA's [0, 4MB)
  [KERNBASE>>PDXSHIFT] = (0) | PTE_P | PTE_W | PTE_PS,
};

entrypgdir[NPDENTRIES]を使って、仮想アドレス0x0000 0000〜0x003F FFFFと物理アドレス0x0000 0000〜0x003F FFFFのマッピング、仮想アドレス0x8000 0000〜0x803F FFFFと物理アドレス0x0000 0000〜0x003F FFFFのマッピングを設定しています。


__attribute__((__aligned__(PGSIZE)))
attributeは、コンパイラの挙動を指示するキーワード(gcc拡張機能)の1つです。
__attribute__((__aligned__(n)))は、このキーワードの直後にあるデータを、nバイト境界のアドレス(nバイトの倍数値となるアドレス)に配置させます。
ここでは、PGSIZEバイト境界のアドレス(4096バイト→4KB→0x1000の倍数値となるアドレス)からデータが整列するようにattributeを指定しています。


[0] = (0) | PTE_P | PTE_W | PTE_PS,]
仮想アドレスの上位10bitの値が、その仮想アドレスのマッピングに関わっているページディレクトリエントリを指定するインデックス値になります。
そのため、仮想アドレス0x0000 0000 〜 0x003F FFFFのマッピングに関わっているページディレクトリエントリを指定するインデックス値は0(0b 00 0000 0000)となります。
よって、仮想アドレス0x0000 0000〜0x003F FFFFと物理アドレス0x0000 0000〜0x003F FFFFのマッピングを設定する場合は、ページディレクトリentrypgdir[NPDENTRIES]の先頭にあるページディレクトリエントリ([0])を使用します。
設定値は、マッピングさせたいページフレーム(物理アドレスの0x0000 0000〜0x003F FFFF)の先頭アドレス0、PTE_P(#define PTE_P 0x001)、PTE_W(#define PTE_W 0x002)、PTE_PS(#define PTE_PS 0x080)の論理和です
ページディレクトリエントリ)のbit31-22にページフレームの先頭アドレスbit31-12が、Pフラグ(bit0)に1(#define PTE_P 0x001→0b 0001)が、R/Wフラグ(bit1)に1(#define PTE_W 0x002→0b 0010)が、U/Sフラグ(bit7)に1(#define PTE_PS 0x080→0b 0000 1000 0000)が、それぞれ格納されます。
ページディレクトリエントリのPフラグ(bit0)が1の時は、ページディレクトリエントリが参照しているページフレームが存在することを示しています。
ページディレクトリエントリのR/Wフラグ(bit1)が1の時は、ページフレームが読み書き可能であることを示しています。
ページディレクトリエントリのPageSizeフラグ(bit7)が1の時は、ページディレクトリエントリが参照しているページフレームのサイズが4MBであることを示しています。


[KERNBASE>>PDXSHIFT] = (0) | PTE_P | PTE_W | PTE_PS,
仮想アドレスの上位10bitの値が、その仮想アドレスのマッピングに関わっているページディレクトリエントリを指定するインデックス値になります。
そのインデックス値を得るためにKERNBASE(0x8000 0000)をPDXSHIFT(22)bit右シフト演算しています。演算結果より、仮想アドレス0x8000 0000 〜 0x803F FFFFのマッピングに関わっているページディレクトリエントリを指定するインデックス値は512(0x200→0b 10 0000 0000)となります。
よって、仮想アドレス0x8000 0000 〜 0x803F FFFFと物理アドレス0x0000 0000〜0x003F FFFFのマッピングを設定する場合は、ページディレクトリentrypgdir[NPDENTRIES]の513個目にあるページディレクトリエントリ([KERNBASE>>PDXSHIFT])を使用します。
設定値は、マッピングさせたいページフレーム(物理アドレスの0x0000 0000〜0x003F FFFF)の先頭アドレス0、PTE_P(#define PTE_P 0x001)、PTE_W(#define PTE_W 0x002)、PTE_PS(#define PTE_PS 0x080)の論理和です
ページディレクトリエントリ)のbit31-22にページフレームの先頭アドレスbit31-12が、Pフラグ(bit0)に1(#define PTE_P 0x001→0b 0001)が、R/Wフラグ(bit1)に1(#define PTE_W 0x002→0b 0010)が、U/Sフラグ(bit7)に1(#define PTE_PS 0x080→0b 0000 1000 0000)が、それぞれ格納されます。
ページディレクトリエントリのPフラグ(bit0)が1の時は、ページディレクトリエントリが参照しているページフレームが存在することを示しています。
ページディレクトリエントリのR/Wフラグ(bit1)が1の時は、ページフレームが読み書き可能であることを示しています。
ページディレクトリエントリのPageSizeフラグ(bit7)が1の時は、ページディレクトリエントリが参照しているページフレームのサイズが4MBであることを示しています。

ページング機能をONにする

movl    %cr0, %eax
orl     $(CR0_PG|CR0_WP), %eax
movl    %eax, %cr0

ページディレクトリを設定、CR3へページディレクトリの物理アドレスの設定が終わったら、CR0(コントロールレジスタ0)の最上位bit(PGフラグ Pagingフラグ)を1にセットしてページング機能をONにします。


movl %cr0, %eax
CR0の値をeaxレジスタにコピーします


orl $(CR0_PG | CR0_WP), %eax
第1オペランドの値CR0_PG | CR0_WPと第2オペランドのeaxの値をOR演算(論理和演算)した結果を第2オペランドのeaxにセットします。
or命令を使うのはCR0の最上位bit(PGフラグ)とbit16(WPフラグ)以外のbitの値が変更されないようにするためです。
また、or命令のオペランドにCR0を指定することはできないので、前の命令でCR0の値をeaxレジスタにコピーしています。
(CR0のbit16(WP:Write Protectフラグ)が1の値にセットされている時、スーパーバイザーレベルのプロシージャが、read- onlyのページに書き込みを行うのを防ぎます。)


movl %eax, %cr0
eaxレジスタの値(CR0_PG | CR0_WPの値とCR0の値の論理和)をCR0にセットします。
この命令により、CR0の最上位bit(PGフラグ)とbit4(PSEフラグ)に1の値がセットされ、ページング機能がオンになります。

main.cへ処理を移す

 # Set up the stack pointer.
  movl $(stack + KSTACKSIZE), %esp

  # Jump to main(), and switch to executing at
  # high addresses. The indirect call is needed because
  # the assembler produces a PC-relative instruction
  # for a direct jump.
  mov $main, %eax
  jmp *%eax

.comm stack, KSTACKSIZE

main.cファイルにあるmain関数へ処理を移します。


movl $(stack + KSTACKSIZE), %esp
スタック領域のトップアドレスの限界値となるstackに、KSTACKSIZE(4096)を足してスタック領域のボトムアドレスを求め、この値をespレジスタ(スタックポインタ)にセットしています。
stackはグローバルな初期値のないデータです。


mov $main, %eax
main関数のアドレス値をeaxレジスタにコピーしています。


jmp *%eax
eaxレジスタにセットされたオフセットアドレス値(main関数のアドレス)へジャンプします。
(セグメントディスクリプタでセグメントベースの値が0x0000 0000に設定されているので、オフセットアドレスの値がそのままリニアアドレスの値(仮想アドレスの値)になります。)


.comm stack, KSTACKSIZE
.comm(commディレクティブ)は、グローバルな初期値のないデータを定義するために使用するディレクティブです。
グローバルな初期値のないデータstackを4KB境界のアドレス(4KBの倍数値となるアドレス)に配置していると思われます。




次回
jupiteroak.hatenablog.com