レガシーガジェット研究所

気になったことのあれこれ。

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
    • 0x10000カーネル(セクタ"1"から始まる4KB)を読み込む
    • ELFフォーマットであるか確認
    • ELFヘッダの情報を元に各プログラムヘッダをハードディスクから読み込む
    • ELF(カーネル)のエントリポイントを呼び出す。

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()関数では命令の送受信が可能であるかどうかをステータスレジスタの値を参照することで確認している。

参考