ぱたへね

はてなダイアリーはrustの色分けができないのでこっちに来た

ゼロからのOS自作入門(その3) Rustで書いたカーネルをブートさせる

ゼロからのOS自作入門(その2) メモリマップの取得 の続きです。

natsutan.hatenablog.com

Cで書かれたフレームバッファを塗りつぶすカーネルを作っていたら、UEFIアプリは無理でもOSだけならRustで書けるんじゃと思ってやってみました。Rustで書かれたカーネルに制御が移りフレームバッファを書き換えて画面表示を変えるところまで動きました。 後から続く人に参考になると思い、試行錯誤の手順を書いておきます。Rustは初心者です。

参考Webサイト こちらの記事が非常に役に立ちました。ありがとうございます。

www.akiradeveloper.com

この記事のベースとなっているバージョンは day03bです。

github.com

UEFIアプリは本のコードを写経して、カーネルだけRustで書いています。 とりあずRustのカーネルがブートしたと自信持てるまでは、Cで書いた確実にブートするカーネルを用意して比較するのが良かったです。

UEFIアプリの修正

修正したのは二箇所です。

読み込むカーネルのファイル名を修正

  // #@@range_begin(read_kernel)
    EFI_FILE_PROTOCOL* kernel_file;
//    root_dir->Open(root_dir, &kernel_file, L"\\kernel.elf", EFI_FILE_MODE_READ, 0);
    root_dir->Open(root_dir, &kernel_file, L"\\rust_kernel", EFI_FILE_MODE_READ, 0);

Qemuの起動スクリプト内で、上手いことリネームしてくれていると勝手に思い込んでいました。ちゃんと自分で作ったカーネルのファイル名に変更しましょう。

EntryPointの表示

    UINT64 entry_addr = *(UINT64*)(kernel_base_addr + 24);

    typedef void EntryPointType(UINT64, UINT64);
    EntryPointType* entry_point = (EntryPointType *)entry_addr;
    Print(L"Entry point: 0x%0lx\n", entry_point);

    entry_point(gop->Mode->FrameBufferBase,  gop->Mode->FrameBufferSize);

読み込んだカーネルのエントリーポイントを表示するようにしました。ここが表示されなければ、カーネル(ファイル)を読み込めてません。 ブートするかどうかを確認するのにUEFIは止める必要が無いので、UEFIの無効化もいったんentry_point()の後に回しました。 カーネルのブートが確認出来たら元に戻しましょう。

Rust側

ここは動いたファイルだけあれば十分ですね。これを参考に動かしてください。(Rust初心者なので良く分からないです)

cargo.toml

[package]
name = "rust_kernel"
version = "0.1.0"
authors = ["natu"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rlibc = "1"
panic-halt = "0.2"

.cargo/config.toml

[build]
target = "x86_64-unknown-linux-gnu"

[profile.release]
panic = "abort"

[profile.dev]
panic = "abort"

[target.x86_64-unknown-linux-gnu]
linker = "ld.lld"

rustflags = [
    # Build Options
    "-C", "no-redzone=yes",
    "-C", "relocation-model=static",

    # Linker Options
    "-C", "link-arg=--entry=KernelMain",
    "-C", "link-arg=--image-base=0x100000",
    # "-C", "link-arg=-n",
    # "-C", "link-arg=-nmagic",
    #"-C", "link-arg=-znorelro",
]

main.rs

参考BlogのAkira Hayakawaさんは、Rustで書いたUEFIアプリからRustのカーネルを呼び出していましたが、僕はCのUEFIアプリからRustのカーネルを呼び出そうとしています。 UFEIアプリからはCのカーネルと同じように見える必要があります。ですので、関数宣言はpub extern "C"としています。

どうもiterationで落ちるようで、ループ無しでフレームバッファをいじってみました。abi_efiapiは要らない気がしますが、とりあえず動いた状態そのまま。

#![no_std]
#![no_main]

#![feature(asm)]
#![feature(abi_efiapi)]

extern crate rlibc;
extern crate panic_halt;

#[no_mangle]
pub extern "C" fn KernelMain(fb_addr:u64, _fb_size: u64) -> ! {
    let adr = fb_addr as *mut u8;
    unsafe {
        *adr.offset(3200 + 0) = 255  as u8;
        *adr.offset(3200 + 1) = 255  as u8;
        *adr.offset(3200 + 2) = 255  as u8;
        *adr.offset(3200 + 3) = 255  as u8;
        *adr.offset(3200 + 4) = 255  as u8;
        *adr.offset(3200 + 5) = 255  as u8;
        *adr.offset(3200 + 6) = 255  as u8;
        *adr.offset(3200 + 7) = 255  as u8;
        *adr.offset(3200 + 8) = 255  as u8;
        *adr.offset(3200 + 9) = 255  as u8;
        *adr.offset(3200 + 10) = 255  as u8;
        *adr.offset(3200 + 11) = 255  as u8;
        *adr.offset(3200 + 12) = 255  as u8;
        *adr.offset(3200 + 13) = 255  as u8;
        *adr.offset(3200 + 14) = 255  as u8;
        *adr.offset(3200 + 15) = 255  as u8;
    }

//    for i in 0..fb_size {
//        unsafe {
//            *adr.offset(i as isize) = (255 % i) as u8;
//        }
//    }

    loop {
        unsafe {
            asm!("hlt")
        }
    }
}

動かすために確認したこと

ELFヘッダーの確認

生成されたELFファイルのヘッダーをCのカーネルと、Rustのカーネルで比較しました。 ヘッダーの表示には、readelfコマンドを使います。

だいたい一緒になるはずです。 ヘッダーに含まれるEntry point addressが、UFEIアプリの中でちゃんと読めていることを確認しました。

Cカーネルのヘッダー

natu@Honoka:~/q/myproj/mikanos/kernel$ readelf -h kernel.elf
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x101000
  Start of program headers:          64 (bytes into file)
  Start of section headers:          11936 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         4
  Size of section headers:           64 (bytes)
  Number of section headers:         16
  Section header string table index: 14

Rustカーネルのヘッダー

natu@Honoka:~/q/myproj/mikanos/rust_kernel$ readelf -h target/x86_64-unknown-linux-gnu/debug/rust_kernel
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x101000
  Start of program headers:          64 (bytes into file)
  Start of section headers:          10520 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         6
  Size of section headers:           64 (bytes)
  Number of section headers:         15
  Section header string table index: 13

Rustカーネルのエントリーポイントの確認

Rustのカーネルを逆アセンブルして、Entry pointがRustのKernelMainになっているかを確認しました。 逆アセンブル表示は、objdump -d を使います。

natu@Honoka:~/q/myproj/mikanos/rust_kernel$ objdump -d target/x86_64-unknown-linux-gnu/debug/rust_kernel

target/x86_64-unknown-linux-gnu/debug/rust_kernel:     file format elf64-x86-64


Disassembly of section .text:

0000000000101000 <KernelMain>:
  101000:       48 81 ec a8 00 00 00    sub    $0xa8,%rsp
  101007:       48 89 bc 24 80 00 00    mov    %rdi,0x80(%rsp)
  10100e:       00
  10100f:       48 89 bc 24 90 00 00    mov    %rdi,0x90(%rsp)

OKですね。

Qemuが動かなくなったとき

最初の頃Rustで作ったカーネルが暴走して、その後Qemuが起動中に止まってしまうようになりました。 実績のあるCカーネルも起動しなくなったのでQemuに問題があると判断、良く分からないですがOVMF_VARS.fdの内容が変ってしまっているようです。何気なくgit statusしてみたら気がつきました。

github.com

ここからOVMF_VARS.fdを持ってきて、$HOME/osbook/devenv のファイルを上書きすればOKです。無事qemuが立ち上がるようになりました。

ブート画面

f:id:natsutan:20210414094238p:plain

左上に一行開けて白い線が表示されています。やりました。

この後

どうも最初に暴走していた所が、このループのようです。

    for i in 0..fb_size {
        unsafe {
            *adr.offset(i as isize) = (255 % i) as u8;
        }
    }

逆アセを見ると、iterっぽい関数が呼ばれていてそこで死んでそうでした。(動いてからの想像)

stackが上手く設定で来てなくて関数呼び出しが出来ないのか、rangeで作られるIteratorがnew的なサポートが必要なのか、そんな想像はしています。 次はrangeを動かして、画面を塗りつぶしたいです。

追記:loopではなく、(255 % i) as u8で落ちてそうです。