Unix xv6 ~ 起動 ~
概要
xv6のコードリーティングを通してUnixの動作を追う。今回はCPUの起動からカーネルの起動直前までを見ていく。
xv6
xv6は、ANSI Cによる、6th Edition Unixのマルチプロセッサx86システムへの再実装である。 xv6はMITにおけるオペレーティングシステムエンジニアリング(6.828)コースにて、教育を目的として使われている。 引用: https://ja.wikipedia.org/wiki/Xv6
ソースコードは以下。コード内にも多くのコメントが記述されており非常に理解し易くなっている。
https://github.com/mit-pdos/xv6-public
全体の流れ
起動処理は以下のような流れで行われる。
- bootasm.S
- 割り込みの禁止
- A20の有効化
- GDT(Global Descriptor Table)のロード
- CPUのプロテクトモードへの移行
- bootmain()を呼び出す()
- bootmain.c
CPUののセットアップ
起動後はbootasm.S
から始まり、当該ファイルがブートセクタとして0x7000
に読み込まれる。
bootasm.S
では主に前述した以下の処理を行う。
- 割り込みの禁止
- A20の有効化
- GDT(Global Descriptor Table)のロード
- CPUのプロテクトモードへの移行
- bootmain()を呼び出す()
実際のコードは以下。
// bootasm.S #include "asm.h" #include "memlayout.h" #include "mmu.h" # CPUを起動後、32bitのプロテクトモードに遷移しC言語のコードに飛ぶ。 # BIOSはハードディスク上の先頭セクタにあるこのコードを物理アドレスの0x7c00に読み込み # %cs=0, %ip=7c00でCPUのリアルモードから実行を開始する。 .code16 # 16-bitのアセンブリ命令を出力する .globl start start: cli # 割り込みを禁する # DS、ES及びSSデータセグメントレジスタをゼロで初期化 xorw %ax,%ax # axにゼロを設定 movw %ax,%ds # データセグメント movw %ax,%es # エクストラセグメント movw %ax,%ss # スタックセグメント # 物理アドレスライン"A20"(アドレスバスの20本目以降をマスクするかどうかのフラグ) # はデフォルトでクリアされている。 # https://en.wikipedia.org/wiki/A20_line # https://en.wikipedia.org/wiki/A20_line seta20.1: inb $0x64,%al # Wait for not busy testb $0x2,%al jnz seta20.1 movb $0xd1,%al # "0xd1"ポート"0x64"に書き込むことでアウトプットポートへの書き込み操作を指定 outb %al,$0x64 seta20.2: inb $0x64,%al # Wait for not busy testb $0x2,%al jnz seta20.2 movb $0xdf,%al # "0xdf"をポート"0x60"に書き込むことで"A20アドレスライン"を有効化する outb %al,$0x60 # リアルモードから抜ける。起動用のグローバルディスクリプタテーブル(GDT)で # 仮想アドレスから物理アドレスへのマップを作成するメモリマップはこの操作中には変更されない # https://en.wikipedia.org/wiki/Control_register lgdt gdtdesc # GDTをロード movl %cr0, %eax # コントロールレジスタ0の値をロード orl $CR0_PE, %eax # bit0をセットしプロテクトモードを有効化 movl %eax, %cr0 # コントロールレジスタ0の値をセット # ロングジャンプ(long jmp)を用いて%cs及び%eipをリロードし、32bitのプロテクトモードへの移行が完了する。 # セグメントディスクリプタは置き換え無しにセットアップされるため、マッピングは変化しない # about "ljmp" opecode : https://docs.oracle.com/cd/E19455-01/806-3773/instructionset-73/index.html # ljmp $value_for_cs, $value_for_eip ljmp $(SEG_KCODE<<3), $start32 .code32 # 32-bitのアセンブリ命令を出力する start32: # プロテクトモード用のデータセグメントレジスタをセットアップ movw $(SEG_KDATA<<3), %ax # データセグメントレジスタ movw %ax, %ds # -> DS: データセグメント movw %ax, %es # -> ES: エクストラセグメント movw %ax, %ss # -> SS: スタックセグメント movw $0, %ax # Zero segments not ready for use movw %ax, %fs # -> FS: エクストラセグメント movw %ax, %gs # -> GS: エクストラセグメント # スタックポインタを設定しCのコードにとぶ。 movl $start, %esp call bootmain # bootmain()関数へ # もしbootmain()関数からリターンした場合は(すべきでないが), # Bochsで動作している場合にはブレイクポイントを起動後、ループに入る # Bochs = a highly portable open source IA-32 (x86) PC emulator written in C++.(http://bochs.sourceforge.net/) # http://bochs.sourceforge.net/doc/docbook/development/debugger-advanced.html movw $0x8a00, %ax # port 0x8a00: command register. movw %ax, %dx outw %ax, %dx # write "0x8a00" -> port:0x8a00 = Used to enable the device. movw $0x8ae0, %ax outw %ax, %dx # write "0x8ae0" -> port:0x8a00 = Return to Debugger Prompt spin: # 無限ループ jmp spin # 起動用のグローバルディスクリプタテーブル .p2align 2 # 4バイトアライメントを強制 gdt: SEG_NULLASM # NULLセグメント SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # コードセグメント(実行及び読み込み可能)。"0x0"から始まり、リミットは4GB(0xFFFFFFFF)(32bitモードで指定可能な最大のサイズ) SEG_ASM(STA_W, 0x0, 0xffffffff) # データセグメント(書き込み可能)。"0x0"から始まり、リミットは4GB(0xFFFFFFFF)(32bitモードで指定可能な最大のサイズ) # セグメントディスクリプタ gdtdesc: # .word (gdtdesc - gdt - 1) # グローバルディスクリプタテーブル(GDT)のサイズ - 1 (2バイト) .long gdt # グローバルディスクリプタテーブル(gdt)のアドレス (4バイト)
ここまでの処理でCPUはプロテクトモードへの移行やアドレスバスの制限解除を完了しており、セグメンテーションも使用可能な状態になっている。
次に行うカーネルイメージのロードはbootmain()
関数が行っており、C言語でbootmain.c
に記述されている。
カーネルイメージのロード
以下のような流れで処理を行う。
0x10000
にカーネル(セクタ"1"から始まる4KB)を読み込む- ELFフォーマットであるか確認
- ELFヘッダの情報を元に各プログラムヘッダをハードディスクから読み込む
- ELF(カーネル)のエントリポイントを呼び出す。
カーネルイメージをメモリに展開するための処理を行うのがbootmain()
関数で、以下のように定義されている。
// bootmain.c // ブートローダ // これはブートブロックの一部でbootmain()関数のを呼び出すbootasm.Sから続いている。 // bootasm.Sはプロセッサを32bitのプロテクトモードへの切り替える。 // bootmain()はディスクのセクタ"1"から始まるELFフォーマットのカーネルイメージを // 読み込み、カーネルのエントリポイントへジャンプする #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; // 0x10000をELFヘッダの先頭にする elf = (struct elfhdr*)0x10000; // 先頭セクタから4KB(カーネル)読み込む readseg((uchar*)elf, 4096, 0); // magicナンバーからELFフォーマットであるかを判定 if(elf->magic != ELF_MAGIC) return; // bootasm.Sのエラーハンドラを実行する // 各セグメントの読み込み (ignores ph flags). ph = (struct proghdr*)((uchar*)elf + elf->phoff); // プログラムヘッダの開始位置(ELFヘッダのアドレス + プログラムヘッダまでのオフセット) eph = ph + elf->phnum; // プログラムヘッダ数を取得 for(; ph < eph; ph++){ pa = (uchar*)ph->paddr; // プログラムヘッダの物理アドレス // プログラムヘッダの物理アドレスへファイルイメージのサイズ分、オフセットで指定したセクタからデータを読み込む readseg(pa, ph->filesz, ph->off); // メモリイメージサイズがファイルイメージサイズを上回る場合、はみ出した分を"0"埋めする if(ph->memsz > ph->filesz) stosb(pa + ph->filesz, 0, ph->memsz - ph->filesz); // 指定のアドレスからはみ出したサイズ分"0"埋めする } // ELFヘッダのエントリポイントから関数を読み込む entry = (void(*)(void))(elf->entry); entry(); // entry.Sへ }
bootmain()
関数ではカーネルをディスクから読み込み、ELFフォーマットであるかを確認した後、他のセグメントもロードし、最後にそのロードしたELFヘッダに記載のあるエントリポイントにジャンプする。
実際にディスクからデータを読み込むreadseg()
関数は以下のように定義されており、オフセットやカウンタから実際の終点アドレスなどを求める処理となっていることがわかる。
// bootmain.c // オフセット("offset")で指定したセクタから"count"バイト分のデータを読み込み、指定の物理アドレス"pa"に書き込む。 // おそらく指定したよりも大きなデータの読み込みが発生する。 // readseg((uchar*)elf, 4096, 0); in bootmain() void readseg(uchar* pa, uint count, uint offset) { // データの終端アドレス。この"アドレス-1"の位置までデータを読み込む uchar* epa; epa = pa + count; // 読み込み開始位置をセクタ境界で丸める pa -= offset % SECTSIZE; // オフセットをバイトからセクタサイズ単位へ変換(カーネルはセクタ"1"から始まる) offset = (offset / SECTSIZE) + 1; // "pa"アドレスを始点に"epa-1"まで、512(SECTSIZE)バイトずつ読み込む for(; pa < epa; pa += SECTSIZE, offset++) readsect(pa, offset); // offset(LBA)で指定したセクタから読み込んだデータを"pa"アドレスに展開する }
上記から実際にカーネルのコードをメモリ上に展開しているのはreadsect()
関数だとわかる。当該関数は以下のように定義されている。
// bootmain.c // offsetで指定したセクタを読み込みdstに書き込む // https://wiki.osdev.org/ATA_PIO_Mode#Primary.2FSecondary_Bus#x86_Directions void readsect(void *dst, uint offset) { // 28 bit PIO waitdisk(); // 命令の送受信可能になるまで待つ outb(0x1F2, 1); // 読み込みセクタ数 // 28bitを4回に分けて指定する outb(0x1F3, offset); // 最下位8bit outb(0x1F4, offset >> 8); // 次の8bit outb(0x1F5, offset >> 16); // 次の8bit // Send 0xE0 for the "master" or 0xF0 for the "slave", outb(0x1F6, (offset >> 24) | 0xE0); // 最上位4bit及び"master"に送信する値("0xE0") outb(0x1F7, 0x20); // cmd 0x20 - セクタの読み込みコマンド waitdisk(); // 命令の送受信可能になるまで待つ // long(32 bit = 4 Byte)単位で送信するためセクタサイズ(Byte)を4で割る insl(0x1F0, dst, SECTSIZE/4); }
readsect()
関数では実際にI/Oポートからコマンドを送信しデータを指定の位置に読み込むような処理を行う。
上記で命令の送受信が可能になるまで待機する関数であるwaitdisk()
は以下のように定義されている。
// 命令の送受信が可能になるまで待機する(これを待たないとハングアップする可能性がある) void waitdisk(void) { // https://wiki.osdev.org/ATA_PIO_Mode#Primary.2FSecondary_Bus // 0x1F7: Status Register // - 0 ERR Indicates an error occurred. Send a new command to clear it (or nuke it with a Software Reset). // - 1 IDX Index. Always set to zero. // - 2 CORR Corrected data. Always set to zero. // - 3 DRQ Set when the drive has PIO data to transfer, or is ready to accept PIO data. // - 4 SRV Overlapped Mode Service Request. // - 5 DF Drive Fault Error (does not set ERR). // - 6 RDY Bit is clear when drive is spun down, or after an error. Set otherwise. // - 7 BSY Indicates the drive is preparing to send/receive data (wait for it to clear). In case of 'hang' (it never clears), do a software reset. // // 0xC0: 命令の送受信が可能どうか。 while((inb(0x1F7) & 0xC0) != 0x40); }
waitdisk()
関数では命令の送受信が可能であるかどうかをステータスレジスタの値を参照することで確認している。