Open16

RustでOSを作る

Kept1994Kept1994

環境 MacOS Monterey

環境構築は以下参考。
MacOSだとできなそうなのでDockerでUbunts環境を用意し、ここでビルドすることにする。
https://zenn.dev/takatom/scraps/906d78df34bcab

nightlyで試したが、ちょうどバグを踏んだらしい。
https://github.com/rust-lang/rust/issues/98378

nightlyの過去のversion(nightly-2022-06-18)に固定することにした。

Kept1994Kept1994

参考
https://zenn.dev/takatom/scraps/906d78df34bcab

Dockerfile
FROM ubuntu:latest

RUN apt update && apt upgrade -y && apt install -y curl build-essential

RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y

RUN apt install lld file -y

ENV PATH /root/.cargo/bin:$PATH

WORKDIR /rust

RUN rustup toolchain install nightly-2022-06-18 && rustup default nightly-2022-06-18

RUN rustup component add rust-src --toolchain nightly-2022-06-18-x86_64-unknown-linux-gnu
# bootloader
$ docker run -v ${pwd}:/rust/ <container_name> /bin/bash -c "cd /rust/bootloader && cargo +nightly-2022-06-18 rustc --target x86_64-unknown-uefi -Z build-std"

ターゲットは何向けにコンパイルするか。今回は独自OSになるのでx86_64-unknown-uefi
なお、jsonファイルを指定することでカスタム設定のターゲットとすることができる。
https://tomoyuki-nakabayashi.github.io/embedded-rust-techniques/04-tools/compiler.html

毎回オプションを指定するのは面倒。→ .cargo/config.toml に書くことで省略可能。

$ cargo rustc --target x86_64-unknown-uefi

https://zenn.dev/takatom/scraps/1bf98b3425a94d

Kept1994Kept1994
$ brew install qemu
$ qemu-system-x86_64 -version
QEMU emulator version 7.0.0
Copyright (c) 2003-2022 Fabrice Bellard and the QEMU Project developers

まずはQEMUでHello World。みかんOS本に従う。

$ qemu-img create -f raw disk.img 200M

200MiBの空のRawディスクイメージフォーマットのイメージを作成
https://docs.fedoraproject.org/ja-JP/Fedora/13/html/Virtualization_Guide/sect-Virtualization-Tips_and_tricks-Using_qemu_img.html
この段階でバイナリエディタで開くと0で埋められているのがわかる。

$ mkfs.fat -n 'MIKAN OS' -s 2 -f 2 -R 32 -F 32 disk.img

mkfs.<ファイルシステムの種類>でファイルシステムを構築できる。
オプションは以下参照
https://man7.org/linux/man-pages/man8/mkfs.fat.8.html

ファイルシステム構築後はバイナリエディタで開くと先頭にいくつか情報が書き込まれていることが確認できる。

$ mkdir -p mnt

マウントする場所を作る
マウントはファイルシステムを認識しディレクトリツリーをつなぐことなのでつなぐための場所を用意している

$ hdiutil attach -mountpoint mnt disk.img 
$ mkdir -p mnt/EFI/BOOT
$ cp hello.efi mnt/EFI/BOOT/BOOTX64.EFI

マウントしたことでファイルシステムにアクセスできるようになったのでパスを切り、ファイルをおく。

$ hdiutil detach mnt
$ curl -O https://raw.githubusercontent.com/uchan-nos/mikanos-build/master/devenv/OVMF_CODE.fd
$ curl -O https://raw.githubusercontent.com/uchan-nos/mikanos-build/master/devenv/OVMF_VARS.fd
$ qemu-system-x86_64 -drive if=pflash,file=OVMF_CODE.fd -drive if=pflash,file=OVMF_VARS.fd -hda disk.img 

OVMF

https://qiita.com/kakinaguru_zo/items/f74bcfbf9f75d7e7a913

Kept1994Kept1994

ブートローダを作る

これに習っていく
https://zenn.dev/yubrot/articles/d6e85d12ccf2c6

まずは動くものを。UEFIアプリケーション

本家ではEDK2を使用しているが、rust-rsクレートを利用することでより楽になるみたい。
UEFIアプリケーションの起動まではかなり楽になる。
https://skoji.jp/blog/2021/04/mikan-laranja-os.html

bootloader/src/main.rs
#![no_std]
#![no_main]
#![feature(abi_efiapi)]

#[macro_use]
extern crate alloc;

use core::arch::asm;
use core::fmt::Write;
use uefi::prelude::*;
use uefi::table::runtime::ResetType;

#[entry]
fn efi_main(_image: Handle, mut system_table: SystemTable<Boot>) -> Status {
    // UEFIライブラリの初期化。
    // https://docs.rs/uefi-services/latest/uefi_services/fn.init.html
    uefi_services::init(&mut system_table).unwrap();

    system_table.stdout().reset(false).unwrap();

    writeln!(system_table.stdout(), "Hello, World!").unwrap();
    {
        let revision = system_table.uefi_revision();
        let (major, minor) = (revision.major(), revision.minor());
        log::info!("UEFI {}.{}", major, minor);
    }
    system_table.boot_services().stall(5_000_000);

    system_table.stdout().reset(false).unwrap();

    system_table.runtime_services()
        .reset(ResetType::Shutdown, Status::SUCCESS, None);
}

UEFIライブラリの初期化時に引数に指定したSystemTable<Boot>はUEFIアプリケーションを操作する窓口になる。
ただし常に全ての操作が可能ではない。

  • boot services -> 起動後のブートストラップステージでのみ
  • runtime services -> それ以降でのみ

stdout()で標準出力用のインターフェースとなるオブジェクトを取得。write!()ないしはwriteln!()で書き込める。
reset(false)でフラッシュする感じ。
unwrap_success()は使えなかった。

stall()で一定時間待つ。指定するのはマイクロ秒単位。アンダースコアは可読性向上のため。
https://doc.rust-jp.rs/rust-by-example-ja/primitives/literals.html

Kept1994Kept1994

メモリマップ取得

カーネルファイルを読み出してメモリに書き出すためにどの領域が空いているかを確認する必要がある。そのためにメモリマップを読む。

以下関数が用意されている。リファレンスに沿ってメモリマップのサイズを取得し、適切なバッファを用意してメモリマップを取得する。

memory_map_size
https://docs.rs/uefi/0.16.0/uefi/table/boot/struct.BootServices.html#method.memory_map_size
memory_map
https://docs.rs/uefi/0.16.0/uefi/table/boot/struct.BootServices.html#method.memory_map

メモリディスクリプタを取るとこまでできた。
ファイル出力は良くわからん、非本質なので一旦パス。

bootloader/src/main.rs
fn get_memory_map(boot_services: &BootServices) {
    let map_size = boot_services.memory_map_size().map_size;
    let mut memmap_buf = vec![0; map_size * 8];
    log::info!("buffer size: {}", map_size);
    let (_map_key, desc_itr) = boot_services.memory_map(&mut memmap_buf).unwrap();
    let descriptors = desc_itr.copied().collect::<Vec<_>>();
    descriptors.iter().for_each(|descriptor| {
        log::info!("{:?}, {}, {}, {}", descriptor.ty, descriptor.phys_start, descriptor.virt_start, descriptor.page_count);
    })
}

map_sizeの大きさそのままでbuffer用意したがTOO_SMALL的なエラーになる。
この辺は後でまた考える。

Kept1994Kept1994

サンプルのカーネル作り

メモリマップを取得し空いてるメモリの場所を特定したら、そこにカーネルファイルを置きたいのでそのためのサンプルとなるカーネル(elfバイナリ)をdisk.img`に同梱する。ここでは中身はhaltするだけのもの。

kernel/src/main.rs
#![no_std]
#![no_main]
// #![feature(start)]

use core::arch::asm;
use core::panic::PanicInfo;

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    unsafe {
        loop {asm!("hlt")}
    }
}

#[no_mangle]
pub extern "sysv64" fn kernel_main() -> ! {
    unsafe {
        loop {asm!("hlt")}
    }
}

カーネルのビルド時にはターゲットをカスタム設定する必要がある。
jsonファイルの名称は任意で、ビルドオプションのtargerにその名前を指定できれば良い。
ブートローダの方と同じく.cargo/config.tomlに記述することで省略可能。

x86_64-unknown-none-xxx
"pre-link-args": {
    "ld": [
      "--entry=kernel_main",
      "--image-base=0x100000",
      "--static"
    ]
  },

その中でコードのエントリポイントの関数名kernel_mainをjsonのリンカの設定に書く。
同じく、ここでリンク時にカーネルファイルを0x100000に配置するよう設定している。

参考)
https://os.phil-opp.com/minimal-rust-kernel/
https://yoshitsugu.net/posts/2019-06-04-haribote-os-in-rust-day3.html

#![feature(start)]は古い?かも。

Kept1994Kept1994

ファイルを読み出す

参考
https://segfo-ctflog.blogspot.com/2021/04/osrust3uefi.html

Directoryを取得する

bootloader/src/main.rs
    let loaded_image = boot_services.handle_protocol::<LoadedImage>(_image).unwrap().get();
    let device = unsafe{(*loaded_image).device()};
    let file_system = boot_services.handle_protocol::<SimpleFileSystem>(device).unwrap().get();
    let mut root_dir = unsafe{(*file_system).open_volume().unwrap()};

handle_protocolでboot_servicesからプロトコルを読み出す(?)
LoadedImageプロトコルを読み込み、*で生のポインタとしてdevice()関数を呼ぶ。

取得したSimpleFileSystemにおいてopen_volume()を用いるとルートディレクトリを取得できる。
https://docs.rs/uefi/0.3.2/uefi/proto/media/fs/struct.SimpleFileSystem.html#method.open_volume

Directory、FileHandle、RegularFileのインターフェースは以下らしい。
https://github.com/rust-osdev/uefi-rs/blob/main/src/proto/media/file/mod.rs

ドキュメントを見てもDirectoryにはread_entry()しかなさそうに思うが実装を見るとopen()とかがあるらしい。試しにルートディレクトリから確認できる範囲のファイルを確認してみる。バッファサイズは適当に決め打ちしとく。

bootloader/src/main.rs
let mut buf = vec![0; 4096];
let res = root_dir.read_entry(&mut buf).unwrap();
log::info!("{:?}", res);
let res = root_dir.read_entry(&mut buf).unwrap();
log::info!("{:?}", res);
let res = root_dir.read_entry(&mut buf).unwrap();
log::info!("{:?}", res);

$ hdiutil attach -mountpoint $(MOUNT_POINT) $(DISK_IMG_PATH)を実行しmnt/にマウントした状態でmnt/以下のフォルダ構成を確認すると、ここでの出力結果と一致することがわかる。
このことからroot_dirはルートディレクトリ(mnt/)の情報を持った何らかのオブジェクトだとわかる。

Kept1994Kept1994

open()を見ると以下の定義とわかる。

fn open(
        &mut self,
        filename: &CStr16,
        open_mode: FileMode,
        attributes: FileAttribute,
    ) -> Result<FileHandle> 

CStr16とかいう新しいやつが出てきた。rustは標準がutf-8なのでutf-16に変換する必要があるということ?サンプルコードがあるのでそれに従う。
https://docs.rs/uefi/latest/uefi/struct.CStr16.html

use uefi::CStr16;

let mut buf = [0; 4];
CStr16::from_str_with_buf("ABC", &mut buf).unwrap();

みたいな感じで&str型から変換する。

FileModeは3種類ある。
https://docs.rs/uefi/0.3.2/uefi/proto/media/file/enum.FileMode.html

FileAttributeはよくわからん
https://docs.rs/uefi/0.3.2/uefi/proto/media/file/struct.FileAttribute.html

RegularFileを取得する。

https://docs.rs/uefi/0.3.2/uefi/proto/media/file/struct.RegularFile.html

Use FileHandle::into_kind or RegularFile::new to create a RegularFile. In addition to supporting the normal File operations, RegularFile supports direct reading and writing.

RegularHandleでは直接ファイルを読み書きできるみたい。
なのでRegularFile::new()に食わせてFileHandleからRegularHandleへ変換する。

ここで受け取るRegularHandleのread関数でファイルを読み出し、バッファに格納する。
https://github.com/rust-osdev/uefi-rs/blob/main/src/proto/media/file/regular.rs

pub fn read(&mut self, buffer: &mut [u8]) -> Result<usize, Option<usize>> { ... }

この状態で読み取ったbufferを覗くと何かが読めてることが確認できる(バイナリなので読めはしない)

    // RegularFile取得
    let mut cstr_buf = [0u16; 32];
    let cstr_file_name = CStr16::from_str_with_buf("kernel.elf", &mut cstr_buf).unwrap();
    let file_handle = root_dir.open(cstr_file_name, FileMode::Read, FileAttribute::empty()).unwrap();
    let mut file = unsafe {RegularFile::new(file_handle)};
    // サイズ取得
    let file_size = file.get_boxed_info::<FileInfo>().unwrap().file_size() as usize;
    // バッファへの読み込み
    let mut buf = vec![0; file_size];
    file.read(&mut buf);
    log::info!("{:?}", buf);
Kept1994Kept1994

カーネルをメモリ上に配置する。

今bufferには読み出してきたカーネルelfが乗っている。

参考
https://msyksphinz.hatenablog.com/entry/2017/11/03/204148

goblinクレートを使うことでバイナリを解析できる。

program_headers()を用いることでヘッダ情報をProgramHeadersのイテレータとして取得できる。

ここでelfバイナリのヘッダについて調べる。
https://docs.oracle.com/cd/E19683-01/817-2466/chapter6-83432/index.html

PT_LOAD
p_filesz と p_memsz により記述される読み込み可能セグメントを指定します。ファイルのバイト列は、メモリーセグメントの先頭に対応付けられます。セグメントのメモリーサイズ (p_memsz) がファイルサイズ (p_filesz) より大きい場合、不足するバイトは、値 0 を保持しセグメントの初期化領域に続くように定義されます。ファイルサイズがメモリーサイズより大きくなることは許可されません。プログラムヘッダーテーブルの読み込み可能セグメントエントリは、p_vaddr 構成要素で整列され、昇順に現れます。

PT_LOADをかき集めたら良い。
この辺もう少し理解したい。

    for ph in elf.program_headers.iter() {
        log::info!("Program header: {} {} {} {}",elf::program_header::pt_to_str(ph.p_type),ph.p_offset,ph.p_vaddr,ph.p_memsz);
    }
Kept1994Kept1994

elfヘッダについて詳しい
https://smallkirby.hatenablog.com/entry/2019/02/26/001346

readelfでelfヘッダの内容を表示できる。

$ readelf -l kernel/target/x86_64-unknown-none-shimejios/debug/kernel.elf

Elf file type is EXEC (Executable file)
Entry point 0x101130
There are 4 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000100040 0x0000000000100040
                 0x00000000000000e0 0x00000000000000e0  R      0x8
  LOAD           0x0000000000000000 0x0000000000100000 0x0000000000100000
                 0x0000000000000120 0x0000000000000120  R      0x1000
  LOAD           0x0000000000000120 0x0000000000101120 0x0000000000101120
                 0x0000000000000016 0x0000000000000016  R E    0x1000
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x0

    :
    :

Program Headerは以下で取得できる。

for ph in elf.program_headers.iter() {
    // 処理
}

以下参考になりそう。
https://monoist.itmedia.co.jp/mn/articles/1106/14/news001_3.html

Kept1994Kept1994

ELF

仮想アドレスと物理アドレス

OSがプログラムをメモリ上にロードする時にはその時に空いているアドレスが割り当てられる。そのため、実行するプログラムのコード内では常に0番地から始まるものとして書かれていても実際には0番には置かれていないことが発生する。そのため、OSは実行時にベースアドレスレジスタに先頭のアドレスを格納し、プログラム内のアドレスを相対的なものとし、都度ベースアドレスからの相対的なアドレスを求めることで実際のアドレスを示すように調整している。
この実際のアドレスを物理アドレス。プログラム内の0番から始まるものとした相対的なアドレスを仮想アドレスとしている。
https://blueeyes.sakura.ne.jp/2018/05/17/1233/

この相対的なアドレス(仮想アドレス)で表現されるものを再配置可能なプログラムといい、メモリ上のどこに展開されても正しく動かせるものとなる。
http://www.hino.meisei-u.ac.jp/is/iga/lecture/os/No5org.pdf

ELFには以下4種類あるらしい (e_type)

共有オブジェクトファイルはPIC(position-independent code)としてビルドされるが、通常のアプリケーションは絶対アドレスを用いて記述されている(最近では"いた"が正しいのかも)。セキュリティ強化目的にも有用。
PIE(position-independent executable)は実行ファイルについても相対アドレスを用いたもの。
→kernelからすると相対アドレスで書かれたPIEは共有オブジェクト(ELF_DYN)の認識になるらしい。

https://sugawarayusuke.hatenablog.com/entry/2018/01/22/025515

PIC(位置独立コード)

  • .soファイルなど。末尾にversion番号がつくことあり。
  • 共有ファイルは複数のプログラムで共有される。
  • プログラム実行時、そのプロセスの仮想メモリ空間に共有ファイルをロードする。共有ファイルが必要になると、PLT(Procedure Linkage Table)内のエントリを確認し、GOT(Global Offsets Table)で対応する関数へジャンプする(なお、初回はいろいろやってる。リンク詳細)。

https://keichi.dev/post/plt-and-got/
https://qiita.com/saikoro-steak/items/f9bf534f8fc5f2be3b0e

Kept1994Kept1994

ピクセルを描く

みかん本P82
まずはGraphicsOutputの中身を見てみる。

bootloader/src/main.rs
use uefi::proto::console::gop::{GraphicsOutput, Mode, ModeInfo, PixelFormat, PixelBitmask};
fn efi_main(_image: Handle, mut system_table: SystemTable<Boot>) -> Status {
    // (中略)
    let gop = boot_services.locate_protocol::<GraphicsOutput>().unwrap().get();
    let mode = unsafe{(*gop).query_mode(0_u32)}.unwrap();  // ★ この2行を1行にまとめるとエラーする。
    let mode_info = mode.info();  // ★
    let res = mode_info.resolution();
    let stride = mode_info.stride();
    let format = mode_info.pixel_format();
    // let mask = mode_info.pixel_bitmask().unwrap();
    log::info!("{:?}", res);  // 640 480 なんか見たことある縦横の長さ
    log::info!("{}", stride);  // 640
    log::info!("{:?}", format); // Bgr

unsafeのスコープを出るから?

temporary value is freed at the end of this statement

参考
https://zenn.dev/misawa/books/zero2os-notes/viewer/chapter3
https://zenn.dev/acky/scraps/c7960d8995e26e

Kept1994Kept1994

1ピクセルあたり4バイトなのでその分係数を掛けてindexとする。strideは1行の横の長さなので上から何行目かを掛ける。
一応は白く塗りつぶせた。(QEMUのサイズの問題か縦は750くらいにしないと完全に真っ白にはならなかった。)

bootloader/src/main.rs
const BYTES_PER_PIXEL: usize = 4;
let gop = boot_services.locate_protocol::<GraphicsOutput>().unwrap().get();
let gop = unsafe{&mut (*gop)};
let mode = unsafe{gop.query_mode(0_u32)}.unwrap();
let mode_info = mode.info();
let (horizontal, vertical) = mode_info.resolution();
let stride = mode_info.stride();
let format = mode_info.pixel_format();
let mut fb = unsafe{gop.frame_buffer()};
for x in 0..horizontal {
    for y in 0..vertical {
        unsafe {
            fb.write_value((stride * y + x) * BYTES_PER_PIXEL, [255_u8, 255_u8, 255_u8]);
        }
    }
}

本に習いBufferFrameConfig構造体を作り、baseとしてgop.frame_buffer()を置くべきなのかも。
kernelのエントリポイント(関数ポインタ化したやつ)を呼ぶ時の引数にFrameBufferを渡す際に縦横の長さとかが含まれているようにしたい。

Kept1994Kept1994

みかん本P98。Config構造体を作る。

bootloader/src/main.rs
struct FrameBufferConfig<'a> {
    frame_buffer_base:  &'a mut FrameBuffer<'a>,
    horizontal_resolution: usize,
    vertical_resolution: usize,
    stride: usize,
    format: PixelFormat
}
    let bfconfig = FrameBufferConfig {
        frame_buffer_base: &mut gop.frame_buffer(),
        horizontal_resolution: mode.info().resolution().0,
        vertical_resolution: mode.info().resolution().1,
        stride: mode.info().stride(),
        format: mode.info().pixel_format()
    };

ライフタイム
https://blog-mk2.d-yama7.com/2020/12/20201230_rust_lifetime/