OS起動編⑮-2 entryother.S (Xv6を読む~OSコードリーディング~)
前回
jupiteroak.hatenablog.com
トップページ
jupiteroak.hatenablog.com
entryother.S
https://github.com/mit-pdos/xv6-public/blob/master/entryother.S
#include "asm.h" #include "memlayout.h" #include "mmu.h" # Each non-boot CPU ("AP") is started up in response to a STARTUP # IPI from the boot CPU. Section B.4.2 of the Multi-Processor # Specification says that the AP will start in real mode with CS:IP # set to XY00:0000, where XY is an 8-bit value sent with the # STARTUP. Thus this code must start at a 4096-byte boundary. # # Because this code sets DS to zero, it must sit # at an address in the low 2^16 bytes. # # Startothers (in main.c) sends the STARTUPs one at a time. # It copies this code (start) at 0x7000. It puts the address of # a newly allocated per-core stack in start-4,the address of the # place to jump to (mpenter) in start-8, and the physical address # of entrypgdir in start-12. # # This code combines elements of bootasm.S and entry.S. .code16 .globl start start: cli # Zero data segment registers DS, ES, and SS. xorw %ax,%ax movw %ax,%ds movw %ax,%es movw %ax,%ss # Switch from real to protected mode. Use a bootstrap GDT that makes # virtual addresses map directly to physical addresses so that the # effective memory map doesn't change during the transition. lgdt gdtdesc movl %cr0, %eax orl $CR0_PE, %eax movl %eax, %cr0 # Complete the transition to 32-bit protected mode by using a long jmp # to reload %cs and %eip. The segment descriptors are set up with no # translation, so that the mapping is still the identity mapping. ljmpl $(SEG_KCODE<<3), $(start32) //PAGEBREAK! .code32 # Tell assembler to generate 32-bit code now. start32: # Set up the protected-mode data segment registers movw $(SEG_KDATA<<3), %ax # Our data segment selector movw %ax, %ds # -> DS: Data Segment movw %ax, %es # -> ES: Extra Segment movw %ax, %ss # -> SS: Stack Segment movw $0, %ax # Zero segments not ready for use movw %ax, %fs # -> FS movw %ax, %gs # -> GS # Turn on page size extension for 4Mbyte pages movl %cr4, %eax orl $(CR4_PSE), %eax movl %eax, %cr4 # Use entrypgdir as our initial page table movl (start-12), %eax movl %eax, %cr3 # Turn on paging. movl %cr0, %eax orl $(CR0_PE|CR0_PG|CR0_WP), %eax movl %eax, %cr0 # Switch to the stack allocated by startothers() movl (start-4), %esp # Call mpenter() call *(start-8) movw $0x8a00, %ax movw %ax, %dx outw %ax, %dx movw $0x8ae0, %ax outw %ax, %dx spin: jmp spin .p2align 2 gdt: SEG_NULLASM SEG_ASM(STA_X|STA_R, 0, 0xffffffff) SEG_ASM(STA_W, 0, 0xffffffff) gdtdesc: .word (gdtdesc - gdt - 1) .long gdt
entryother.Sでは、AP(Application Processor)のブート処理、プロテクトモード移行、ページング設定を行っています。
startothers関数内で実行されたuniversal startup algorithmによって、AP(Application Processor)が起動します。
起動したAP(Application Processor)の処理は、entryotherファイル内の命令から始まります。
entryotherファイル内の命令を実行するのは、AP(Application Processor)です。
処理の内容
- ハードウェア割り込みを禁止する
- CSレジスタ以外のセグメントレジスタ(DS, ES, SSレジスタ)を初期化する
- GDT・GDTRを設定する(プロテクトモード移行準備)
- CPUをプロテクトモードへ移行する
- プロテクトモードにおいてCSレジスタ以外の各種セグメントレジスタを初期化する
- 4MBのページフレームを使用できるようにする
- CR3にページディレクトリの物理アドレスを設定する
- ページング機能をONにする
- main.cへ処理を移す
ハードウェア割り込みを禁止する
.code16 .globl start start: cli
.code16
このソースファイルをアセンブルするGNU Assembler(gas)が16bitセグメント用の機械語コードを出力するように、.code16(code16ディレクティブ)を指定します。.code16(code16ディレクティブ)を指定することにより、指定箇所以降のアセンブリ言語命令は16bitセグメント用の機械語命令にアセンブルされるようになります。
GNU Assembler(gas)はデフォルトで32bitセグメント用の機械語コードを出力しますが、x86CPUは16bitモード(リアルモードモード)で起動を開始します(レガシーBIOSの場合)。そのため、GNU Assembler(gas)が16bitセグメント用の機械語コードを出力するように、.code16(code16ディレクティブ)を指定する必要があります。
.globl start
.globl(globlディレクティブ)を使って、startラベルを外部のファイルから参照できるようにします。
start:
cli命令が配置されているアドレスにstartラベル(シンボル名、名前)を付けます。
CSレジスタ以外のセグメントレジスタ(DS, ES, SSレジスタ)を初期化する
xorw %ax,%ax movw %ax,%ds movw %ax,%es movw %ax,%ss
xorw %ax,%ax
第1オペランドのaxと第2オペランドのaxをXOR演算(排他的論理和)し、その結果を第2オペランドのaxレジスタにセットします。
第1オペランドのaxレジスタと第2オペランドのaxレジスタの各bit同士でXOR演算(排他的論理和)を行うので、第2オペランドのaxレジスタには0がセットされます。このaxレジスタにセットされた0の値を使って、以降のレジスタを初期化していきます。
movw %ax,%ds
axレジスタからDSレジスタへ値をコピーします(axレジスタにセットされた0の値でDSレジスタを初期化します)。
セグメント方式は利用していません。(メモリ領域をコードセグメント、データセグメント、スタックセグメントに3分割するような使い方はしていません。)
movw %ax,%es
axレジスタからESレジスタへ値をコピーします(axレジスタにセットされた0の値でESレジスタを初期化します)。
movw %ax,%ss
axレジスタからSSレジスタへ値をコピーします(axレジスタにセットされた0の値でSSレジスタを初期化します)。
セグメント方式は利用していません(メモリ領域をコードセグメント、データセグメント、スタックセグメントに3分割するような使い方はしていません)。
GDT・GDTRを設定する(プロテクトモード移行準備)
プロテクトモードへ移行する準備として、GDT(Global Descriptor Table グローバルディスクリプタテーブル)とGDTR(グローバルディスクリプタテーブルレジスタ)の設定を行います。まず先に、このソースファイルの終わりに記述されているGDTの設定から説明します。
GDT(グローバルディスクリプタテーブル)の設定
.p2align 2 gdt: SEG_NULLASM SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) SEG_ASM(STA_W, 0x0, 0xffffffff)
メインメモリ上にGDT(グローバルディスクリプタテーブル)を定義します。
.p2align 2
.p2alignn(.p2alignディレクティブ)は、このディレクトリの直後にある命令やデータを、2^nバイト境界に配置させます。
今回の場合、.p2align 2の直後にあるデータはGDT(グローバルディスクリプタテーブル、SEG_NULLASMから始まる)なので、GDTの先頭アドレスは、2^2=4バイト境界(4バイトの倍数値)になります。
gdt:
SEG_NULLASMが配置されているアドレスにラベルを付けます。
gdtラベルから、GDTの定義が始まります。
SEG_NULLASM
GDTの先頭にあるディスクリプタを設定するために、SEG_NULLASMマクロを使用しています。
GDTの先頭にあるディスクリプタ(8バイトサイズ)は、セグメントレジスタ(CSレジスタ、DSレジスタなど)を無効にするために使用されます。
SEG_ASM(STA_X | STA_R, 0x0, 0xffffffff)
GDTの2番目にあるディスクリプタを、コードセグメントに関するディスクリプタとして設定するために、SEG_ASMマクロを使用しています。
1つ目の引き数(type)は、セグメントの種類を指定する4bitの値です。
設定値は STA_X | STA_R (0x0a→1010)なので、GDTの2番目にあるディスクリプタは、実行と読み出しが可能なコードセグメントに関するディスクリプタになります。
2つ目の引数(base)は、セグメントベース(コードセグメントの先頭アドレス)となる32bitの値です。
設定値は 0→0x0000 0000です。
3つ目の引数(lim)は、セグメントのサイズを指定するリミット値(セグメントのサイズ-1)を上位20bitに含んだ値です。
設定値は0xffffffffなので、リミット値は0xfffffとなります。
また、SEG_ASMマクロ内でディスクリプタのbit55(Gフラグ)を1に設定しているので、コードセグメントのサイズは4GBとなります。
SEG_ASM(STA_W, 0x0, 0xffffffff)
GDTの3番目にあるディスクリプタを、データセグメントに関するディスクリプタとして設定するために、SEG_ASMマクロを使用しています。
1つ目の引き数(type)は、セグメントの種類を指定する4bitの値です。
設定値はSTA_W(0x2→0010)なので、GDTの3番目にあるディスクリプタは、読み書き可能なデータセグメントに関するディスクリプタになります。
2つ目の引数(base)では、セグメントベース(データセグメントの先頭アドレス)となる32bitの値です。
設定値は 0x0です。(LinuxなどのOSでは、セグメント方式を使ってメモリを区分けするようなことはしていません。CPUがプロテクトモードで動くためにはセグメントディスクリプタを必要としますが、セグメント回路を無効化することができないので便宜的にこのような設定を行なっています。詳しくは)
3つ目の引数(lim)は、セグメントのサイズを指定するリミット値(セグメントのサイズ-1)を上位20bitに含んだ値です。
設定値は0xffffffffなので、リミット値は0xfffffとなります。
また、SEG_ASMマクロ内でディスクリプタのbit55(Gフラグ)を1に設定しているので、データセグメントのサイズは4GBとなります。
GDTR(グローバルディスクリプタテーブルレジスタ)の設定
lgdt gdtdesc
gdtdesc: .word (gdtdesc - gdt - 1) # sizeof(gdt) - 1 .long gdt # address gdt
メインメモリ上にGDTR(グローバルディスクリプタテーブルレジスタ)の構造と同じデータ構造を定義し、lgdt命令によりそのデータ構造の値をGDTRにロードします。
lgdt gdtdesc
lgdt命令により、gdtdescラベルが付けられたアドレスに配置されている48bitのデータ構造の値(GDTRの構造と同じデータ構造の値)をGDTRにロードします。
gdtdesc:
.word (gdtdesc - gdt - 1)が配置されているアドレスにラベルを付けます。
gdtdescラベルから、GDTRの構造と同じデータ構造(48bit)の定義が続きます。
.word (gdtdesc - gdt - 1)
.word(wordディレクティブ)は16bitの整数値を定義するディレクティブです。
この16bitのデータ領域にGDTのサイズ-1の値(バイト単位)を格納します。
下位アドレス側のgdt(グローバルディスクリプタテーブルの先頭の先頭アドレス)から、上位アドレス側のgdtdesc(GDTR様のデータ構造の先頭アドレス)まで、8バイトのディスクリプタが3つ連続で並んでいます。
よって、設定される値は gdtdesc - gdt - 1 = 24(0x18) -1 = 23(0x17) となります。
gdtdescやgdtのアドレス値からも確認することができます。
## Global Destriptor table(8byte*3entries) 00007c60 : ## put the LGDT register(2+4byte) 00007c78 :
xv6実装の詳解(boot処理編: segmentationとpagingを中心に) - Qiita
→https://gist.github.com/knknkn1162/9ba537b49b10e77f39462a30b274689e#file-bootblock-asm-L144
.long gdt
.long(longディレクティブ)は32bitの整数値を定義するディレクティブです。
この32bitのデータ領域にGDTの先頭アドレス値(gdtラベルが付けられたアドレス)を格納します。
CPUをプロテクトモードへ移行する
movl %cr0, %eax orl $CR0_PE, %eax movl %eax, %cr0 # Complete the transition to 32-bit protected mode by using a long jmp # to reload %cs and %eip. The segment descriptors are set up with no # translation, so that the mapping is still the identity mapping. ljmpl $(SEG_KCODE<<3), $(start32)
GDT・GDTRの設定が終わったら、CR0(コントロールレジスタ0)のbit0(PE:Protection Enable フラグ)を1にセットしてプロテクトモード(32bitモード)へ移行します。
movl %cr0, %eax
CR0の値をeaxレジスタにコピーします。
orl $CR0_PE, %eax
第1オペランドの即値CR0_PEと第2オペランドのeaxの値をOR演算(論理和演算)した結果を第2オペランドのeaxにセットします。
or命令を使うのはCR0のbit0(PEフラグ)以外のbitの値が変更されないようにするためです。
また、or命令のオペランドにCR0を指定することはできないので、前の命令でCR0の値をeaxレジスタにコピーしています。
movl %eax, %cr0
eaxレジスタの値($CR0_PEの値 と 元のCR0の値 の論理和)をCR0にセットします。
この命令により、CR0のbit0(PEフラグ)に1がセットされ、次の命令からはプロテクトモードで実行されます。
ljmp $(SEG_KCODE<<3), $start32
第1オペランドにセレクタ値であるSEG_KCODE<<3、第2オペランドにオフセットアドレスであるstart32(ラベル名)を指定することで、そのセレクタ値に対応する(セグメントディスクリプタ内にある)セグメントベース と オフセットアドレス を足し合わせたアドレス(セグメントベース+オフセットアドレス)へ無条件ジャンプします。
SEG_KCODE(1)を3bit左シフト演算することで、セレクタ値8(0b1000)を得ることができます。
このセレクタ値8に対応するセグメントディスクリプタは、GDTの2番目にあるディスクリプタ SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) です。このディスクリプタからはコードセグメントのセグメントベース0x0000 0000を取得することができます。
また、プロテクトモードに移行した直後は、CPUのパイプライン内にリアルモードで解釈された命令が残存しているので、jmp命令かcall命令を実行してCPUのパイプライン内をフラッシュする必要があります。
プロテクトモードにおいてCSレジスタ以外の各種セグメントレジスタを初期化する
.code32 # Tell assembler to generate 32-bit code now. start32: # Set up the protected-mode data segment registers movw $(SEG_KDATA<<3), %ax # Our data segment selector movw %ax, %ds # -> DS: Data Segment movw %ax, %es # -> ES: Extra Segment movw %ax, %ss # -> SS: Stack Segment movw $0, %ax # Zero segments not ready for use movw %ax, %fs # -> FS movw %ax, %gs # -> GS
プロテクトモードに移行したら、各種のセグメントレジスタを初期化します。
.code32
このソースファイルをアセンブルするGNU Assembler(gas)が32bit用の機械語コードを出力するように、.code32(code32ディレクティブ)を指定します。.code32(code32ディレクティブ)を指定することにより、指定箇所以降のアセンブリ言語命令は32bit用の機械語命令にアセンブルされるようになります。
start32:
movw命令が配置されているアドレスにstartラベル(シンボル名、名前)を付けます。
movw $(SEG_KDATA<<3), %ax
SEG_KDATA(2)を3bit左シフト演算した値16(0x10)をaxレジスタにコピーしています。このaxレジスタの値16はセレクタ値として、次のDSレジスタ、ESレジスタ、SSレジスタにセットされます。
movw %ax, %ds
axレジスタにセットされているセレクタ値16をDSレジスタにコピーしています。
このセレクタ値16に対応するセグメントディスクリプタは、GDTの3番目にあるセグメントディスクリプタSEG_ASM(STA_W, 0x0, 0xffffffff) です。
セグメント方式は利用していません。
movw %ax, %es
axレジスタにセットされているセレクタ値16をESレジスタにコピーしています。
movw %ax, %ss
axレジスタにセットされているセレクタ値16をSSレジスタにコピーしています。
このセレクタ値16に対応するセグメントディスクリプタは、GDTの3番目にあるセグメントディスクリプタSEG_ASM(STA_W, 0x0, 0xffffffff) です。
セグメント方式は利用していません。
movw %ax, %es
axレジスタにセットされているセレクタ値16をESレジスタにコピーしています。
movw $0, %ax
即値0をaxレジスタにコピーしています。
このaxレジスタにセットされた0をFSレジスタ、GSレジスタの初期化に使用します。
movw %ax, %fs
axレジスタにセットされた0でFSレジスタを初期化しています。
4MBのページフレームを使用できるようにする
# 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のページフレームを使用できるようにします。
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にページディレクトリの物理アドレスを設定する
# Use entrypgdir as our initial page table movl (start-12), %eax movl %eax, %cr3
movl (start-12), %eax
ページディレクトリの物理アドレスをeaxレジスタにコピーします。
startothers関数内の処理で、アドレス(start-12)が指定するメモリ領域には、ページディレクトリの物理アドレスV2P(entrypgdir)が格納されています。
ページディレクトリentrypgdirは、main.cに定義されているものです。
movl %eax, %cr3
eaxレジスタの値(ページディレクトリテーブルentrypgdirの物理アドレス値)をCR3にセットします。
ページディレクトリentrypgdirについて
__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バイト境界に配置させます。
ここでは、PGSIZE(4092)バイト境界(4KBの倍数値)のアドレスからデータが整列するように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_PE|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へ処理を移す
# Switch to the stack allocated by startothers() movl (start-4), %esp # Call mpenter() call *(start-8)
main.cファイルにあるmpenter関数へ処理を移します。
movl (start-4), %esp
スタック領域のボトムアドレスを求をespレジスタ(スタックポインタ)にセットしています。
startothers関数内の処理で、アドレス(start-4)が指定するメモリ領域には、スタック領域のボトムアドレスが格納されています。
call *(start-8)
call命令を実行して、mpenter関数をサブルーチンとして呼び出します。
startothers関数内の処理で、アドレス(start-8)が指定するメモリ領域には、mpenter関数のアドレスが格納されています。