xv6のブートローダーをrustで書き直す

はじめに

大遅刻してすみません。 この記事は自作OSアドベントカレンダー9日目の記事です。 ここ半年ぐらいxv6をrustで書き直すことに挑戦しているので、その話を書きます。この記事ではxv6のブートローダーの書き換えについて詳しく書きます。

まだ全然完成には程遠いですが成果物(以下xv6-rs)のリポジトリです。 https://github.com/youta1119/xv6-rs

現在までの進捗はこんな感じになってます

  • ブートローダー側はrustで書き換え完了
  • カーネル側はuartによる標準出力まで実装

なぜvx6?

今年の3月ごろにOSの勉強したいと思って、30日自作OS本を買いました。 しかし、ツールチェインが著者の自作したものだったり、僕の環境(mac os)で動かすのが大変だったりといろいろ思うところがあって3日目くらいでやめました。

それからしばらくしたある日、Turing Complete FMを聞いていたらxv6の話でてきたので、xv6とはなんぞやと調べてみると、MITが開発した教育用のOSであること、講義資料が公開されていることを知りました。 xv6は、はりぼてOSと比較して、現代の汎用OSに必要な機能が最低限そろっていること、unix環境で開発できるのことが良いと思いました。 そこでxv6の講義資料を使ってOSの勉強を始めることにしました。

なぜrust?

おそらく講義時間の都合だと思いますが、xv6の講義では、ほとんど完成してあるOSに不足した機能を、穴埋め形式でを足していく形になってます。 しかし、これは、30日自作OS本のように最初から手を動かしてOSを作ってみたかった僕には不満がありました。 なので、別の言語で最初から書き直してしまえばいいのではないかrustで書き直すことにしました。 rustを選んだのは、並行性、安全性を言語仕様として保証してくれる点がOS開発の際に役立ちそうだと思ったからです。

ブートローダーの実装

開発環境

  • ubuntu 18.04.1 LTS
  • rustc 1.32.0-nightly
  • xargo 0.3.12

開発環境はこのようになっています。 nightly版のrustを使用し、xargoというツールを使ってビルドします。 xargoはカスタムターゲット向けのsysrootのビルドして、rustのプロジェクトをビルドする際にsysrootをいい感じに切り替えてくれるツールです。 またrustの標準ライブラリは使わず、OSに依存しないポータブルなコアライブラリを使用しています。

カスタムターゲットの設定

自作OSに適したコンパイルのターゲットはrustcでサポートされていないため、カスタムターゲットを作成します。 rustではターケット仕様ファイルを定義することでカスタムターゲットを作成できます。 ターケット仕様ファイルはjson形式で作成します。

この辺を参考にコピペして、以下のようなターケット仕様ファイルは作成しました。 多分いらない項目もあると思いますが、動いているので気にしないことにします。

{
    "llvm-target": "i386-unknown-none",
    "target-endian": "little",
    "target-c-int-width": "32",
    "target-pointer-width": "32",
    "max-atomic-width": "64",
    "data-layout": "e-m:e-i32:32-f32:32-n8:16:32-S128-p:32:32:32",
    "arch": "x86",
    "cpu": "pentium4",
    "os": "none", // os
    "linker-flavor": "ld.lld",
    "linker": "rust-lld", //rustlld
    "pre-link-args": {
        "ld.lld": [
            "--script=linker.ld"
        ]
    },
    "features": "-mmx,-sse,+soft-float", //smid(退)
    "dynamic-linking": false,
    "has-rpath": false,
    "no-default-libraries": true,
    "has-elf-tls": true,
    "eliminate-frame-pointer": false,
    "no-compiler-rt": true,
    "disable-redzone": true, //()
    "panic-strategy": "abort", //abort
    "executables": true
}

実装について

rustでブートローダーを作るには私の知る限り2つの方法があります。

  • 1つ目は、ブートローダーのアセンブリとコードを別々にコンパイル方法です。この方法ではアセンブリはgccで、コードはrustで静的ライブラリとしてコンパイルします。そして生成されたオブジェクトファイルをリンカで結合することでブートローダを作成します。
  • 2つ目は、rustの自由形式のアセンブリ(global_asm)をつかって rustのコンパイラで一緒にコンパイルする方法です。

rustでOSを作る以上できるだけrustの機能を使いたい気持ちがあったので2つ目の方法を選びました。

ブートローダー(アセンブリ側)

自由形式のアセンブリはLLVMのモジュールレベルのインラインアセンブリに変換されるようで、おそらくllvmの組み込みアセンブラで処理されるのだと思います。 試してみた感じgasと構文で、gasのマクロやディレクティブが使えました

ほとんどvx6のブートローダーのアセンブリ(boot.S)をコピペしたものですが、アセンブラがgccからllvmの組み込みアセンブラに変わったため、#defineマクロが使えなくなってしまったので、その部分はアセンブリのマクロで書き直しました。

# Start the CPU: switch to 32-bit protected mode, jump into C.
# The BIOS loads this code from the first sector of the hard disk into
# memory at physical address 0x7c00 and starts executing in real mode
# with %cs=0 %ip=7c00.

.set PROT_MODE_CSEG, 0x8         # kernel code segment selector
.set PROT_MODE_DSEG, 0x10        # kernel data segment selector
.set CR0_PE_ON,      0x1         # protected mode enable flag
.set STA_X, 0x8 // Executable segment
.set STA_R, 0x2 // Readable (executable segments)
.set STA_W, 0x2 // Writeable (non-executable segments)

# defineマクロが使えなくなってしまったので書き換えた
.macro SEG_NULL
  .word 0, 0
	.byte 0, 0, 0, 0
.endm  

.macro SEG type, base, lim
	.word ((\lim >> 12) & 0xffff), (\base & 0xffff)
	.byte ((\base >> 16) & 0xff), (0x90 | \type),	(0xC0 | ((\lim >> 28) & 0xf)), ((\base >> 24) & 0xff)
.endm

.code16                     # Assemble for 16-bit mode
.globl start

start:
  cli                         # Disable interrupts
  cld                         # String operations increment
  # Set up the important data segment registers (DS, ES, SS).
  xorw    %ax,%ax             # Segment number zero
  movw    %ax,%ds             # -> Data Segment
  movw    %ax,%es             # -> Extra Segment
  movw    %ax,%ss             # -> Stack Segment

# A20ゲートの有効化
# ref:http://caspar.hazymoon.jp/OpenBSD/annex/gate_a20.html
  # Enable A20:
  #   For backwards compatibility with the earliest PCs, physical
  #   address line 20 is tied low, so that addresses higher than
  #   1MB wrap around to zero by default.  This code undoes this.
seta20.1:
  inb     $0x64,%al               # Wait for not busy
  testb   $0x2,%al
  jnz     seta20.1

  movb    $0xd1,%al               # 0xd1 -> port 0x64
  outb    %al,$0x64

seta20.2:
  inb     $0x64,%al               # Wait for not busy
  testb   $0x2,%al
  jnz     seta20.2

  movb    $0xdf,%al               # 0xdf -> port 0x60
  outb    %al,$0x60

  # Switch from real to protected mode, using a bootstrap GDT
  # and segment translation that makes virtual addresses 
  # identical to their physical addresses, so that the 
  # effective memory map does not change during the switch.
  # GDTの設定
  lgdt    gdtdesc
  movl    %cr0, %eax
  orl     $CR0_PE_ON, %eax
  movl    %eax, %cr0
  
  # Jump to next instruction, but in 32-bit code segment.
  # Switches processor into 32-bit mode.
  # 32bitモードに移行
  ljmp    $PROT_MODE_CSEG, $protcseg

.code32                     # Assemble for 32-bit mode
protcseg:
  # Set up the protected-mode data segment registers
  # 各レジスタ初期化
  movw    $PROT_MODE_DSEG, %ax    # Our data segment selector
  movw    %ax, %ds                # -> DS: Data Segment
  movw    %ax, %es                # -> ES: Extra Segment
  movw    %ax, %ss                # -> SS: Stack Segment
  movw    %ax, %fs                # -> FS
  movw    %ax, %gs                # -> GS
  
  
  # Set up the stack pointer and call into C.
  # スタックポインタを設定してるっぽい(よくわかってない)
  movl $start, %esp
  call bootmain #rustの関数呼び出し

  # If bootmain returns (it shouldn't), loop.
spin:
  hlt
  jmp spin

# Bootstrap GDT
#.p2align 2                                # force 4 byte alignment
gdt:
  SEG_NULL				# null seg
  SEG STA_X|STA_R, 0x0, 0xffffffff	# code seg
  SEG STA_W, 0x0, 0xffffffff	        # data seg

gdtdesc:
  .word   0x17                            # sizeof(gdt) - 1
  .long   gdt                             # address gdt

ブートローダー(rust側)

xv6のmain.cをほぼそのままrustに移植しただけです。 僕はポインタが苦手ですが、変なことしようとするとrustコンパイラがしっかり怒ってくれたので、特にハマったりしませんでした。

#![no_std] //標準ライブラリをつかわない
#![no_main]
#![feature(lang_items)]
#![feature(global_asm)]
#![feature(asm)]

extern crate x86;

mod elf;
use elf::{ElfHeader, ProgramHeader, ELF_MAGIC};
use x86::instruction::{inb, insl, outb};
use core::panic::PanicInfo;

// boot.Sの内容を文字列として読み込んで, 自由形式のアセンブリとしてコンパイラに解釈させる
global_asm!(include_str!("boot.S"));

const SECTOR_SIZE: u32 = 512;
#[no_mangle]
pub unsafe extern "C" fn bootmain() {
    let elf_header = 0x10000 as *const ElfHeader;// scratch space
    read_segment(elf_header as u32, SECTOR_SIZE * 8, 0);
    if (*elf_header).magic != ELF_MAGIC {
        //outw(0x8A00, 0x8A00);
        //outw(0x8A00, 0x8E00);
        return;
    }
    // load each program segment (ignores ph flags)
    let mut ph = ((elf_header as *const u8).offset((*elf_header).phoff as isize)) as *const ProgramHeader;
    let eph = ph.offset((*elf_header).phnum as isize);
    while ph < eph {
        read_segment((*ph).paddr, (*ph).memsz, (*ph).off);
        ph = ((ph as u32) + 32) as *const ProgramHeader; //ph.offset(1);
    }
    // call the entry point from the ELF header
    // note: does not return!
    // カーネルを呼び出す。
    let entry: extern "C" fn() -> ! = core::mem::transmute((*elf_header).entry);
    entry();
}

// Read 'count' bytes at 'offset' from kernel into physical address 'pa'.
// Might copy more than asked
// コンパイラにインライン展開されると510バイト制限を超えてしまうので、インライン展開させないようにする。
#[inline(never)]
unsafe fn read_segment(pa: u32, count: u32, offset: u32) {
    let epa = pa + count;
    // round down to sector boundary
    let mut bpa = pa & !(SECTOR_SIZE - 1);

    // translate from bytes to sectors, and kernel starts at sector 1
    let mut offset = (offset / SECTOR_SIZE) + 1;
    // If this is too slow, we could read lots of sectors at a time.
    // We'd write more to memory than asked, but it doesn't matter --
    // we load in increasing order.
    while bpa < epa {
        // Since we haven't enabled paging yet and we're using
        // an identity segment mapping (see boot.S), we can
        // use physical addresses directly.  This won't be the
        // case once JOS enables the MMU.
        read_sector(bpa , offset);
        offset += 1;
        bpa += SECTOR_SIZE;
    }
}

unsafe fn wait_disk() {
    // wait for disk reaady
    while (inb(0x1F7) & 0xC0) != 0x40 {};
}

unsafe fn read_sector(dst: u32, offset: u32) {
    // wait for disk to be ready
    wait_disk();
    outb(0x1F2, 1); // count = 1
    outb(0x1F3, offset as u8);
    outb(0x1F4, (offset >> 8) as u8);
    outb(0x1F5, (offset >> 16) as u8);
    outb(0x1F6, ((offset >> 24) | 0xE0) as u8);
    outb(0x1F7, 0x20); // cmd 0x20 - read sectors
    // wait for disk to be ready
    wait_disk();
    // read a sector
    insl(0x1F0, dst, SECTOR_SIZE / 4);
}

#[panic_handler]
#[no_mangle]
// パニックが起きたときに呼ばれる
// ref: https://doc.rust-lang.org/nomicon/panic-handler.html
pub extern "C" fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

#[lang = "eh_personality"]
#[no_mangle]
// おまじない(よくわかってない)
// https://doc.rust-lang.org/1.6.0/book/no-stdlib.html
pub extern "C" fn eh_personality() {
    loop {}
}

リンカスクリプト

リンカスクリプトでブートローダーのブートシグネチャの付与もやっています。 開発当初ブートシグネチャをつけ忘れたため、ブートできなくてデバックに何時間も時間を溶かしました😇

OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
OUTPUT_ARCH(i386)
ENTRY(start)
IPLBASE = 0x7c00;

SECTIONS {
  /* ブートローダは0x7c00に読み込まれるため、開始アドレスを0x7c00にする*/
    . = IPLBASE;
    .text : AT(IPLBASE) {
      *(.text)
      *(.data)
      *(.bbs)
    }
    . = IPLBASE + 510;
    /* 510バイト目に0xaa55を書き込む、これがないとブートローダーとして認識されない*/
    .sign : {
      SHORT(0xaa55)
    }
}

今後の目標

とりあえず半年くらいかけてカーネルの部分を実装したいなと思っています。