【サーバー育成記】(1)サーバーをたててみる

社会人も一年目を終わろうというある日、ふと思いました。

「そうだ、サーバーを育てよう」

はい、ということでサーバーを育ててみようと思います。
サーバーを育てるとは何か?
それは私にもよくわかりません。
ただ、世間には大きなサーバーや小さなサーバー、自宅サーバーやクラウドサーバーなど、サーバーにもなにか”立派なサーバー”と”そうじゃないサーバー”があるような気がするんです。
だからまずは”そうじゃないサーバー”を作ってみて、少しずつ"立派なサーバー"にしていくことができるのではないか?
サーバーを育てるとはそういう疑問に対する挑戦なのかもしれません。
私はインフラ系のエンジニアですし、サーバーを育てることで見えてくる世界もあるんじゃないかな、という気がしているんです。

サーバーの種を蒔く

用意したサーバーはAWSのt4g.micro, ストレージ 10GiB, Ubuntu 20.04.4 LTSのインスタンスです。
計算が間違っていなければ月額500円程度で利用できるはずです。

f:id:ymzkmtfm:20220328003915p:plain

ただ、サーバー種を蒔いただけでは育ちません。
サーバーを大きくするにはしっかりと水(webサービス)を撒いてあげる必要があります。
今回の水はこちらです。

github.com

なんでもよかったのですがせっかくなので自作しました。
映画やアニメの感想をアップロードしたら表示してくれるwebサービスです。 合計200行くらいのmain.go一本で作られています。
サーバーをたてて、水を蒔いたものが以下のリンクから確認できます。

http://ec2-44-203-40-2.compute-1.amazonaws.com/

それぞれの作品に書き込まれている評価や楽しみ方のテキストはなんでもよかったので適当なものが入っています。
他意はありません。

このサーバーの今後

パッと思いつくだけでこのサーバーにはいろいろな問題があります。
・ サーバーに名前がついていない(ドメインをとってない)
・ 環境が荒れている(本番環境/開発環境が整備されていない)
・ いい感じの比喩A(サービスの認証が雑)
・ いい感じの比喩B(データの更新/管理が雑)
こういった問題を少しづつ無くしていけばいつかこのサーバーも立派なサーバーに育ってくれると信じています。
まぁ私はあきらめが早いので次の更新は無いのかもしれませんが…

今回の費用

月約500円(予想)

【ゼロからのOS自作入門】MikanOSをRustに移植する 6章前半

前回の続きです

ymzkmtfm.hatenablog.com

ゼロからのOS自作入門 | 内田 公太 |本 | 通販 | Amazon

↑の6章前半(day06b)まで移植しました。
後半は大分時間がかかりそうなので忘れる前に前半だけまとめます。

成果物

github.com

知識の整理

f:id:ymzkmtfm:20210506110726p:plain
PCIシステム想定図

6章は特に新しい知識が多かったので忘れないようにここに理解した分(PCI周辺)だけまとめます。
・バスとバスをつなぐ装置をブリッジと呼びます
・バス0、デバイス0はホストブリッジ、バス0、デバイス0、ファンクション0はPCIバスです
・IOポートはメモリではありません
・コンフィグレーション空間は1ファンクションにつき1つが基本構成っぽいです
・コンフィグレーション空間の実態はアクセスする方法が確立されているレジスタの塊です(たぶん)

もちろん細かい部分は構成次第だと思うので鵜呑みにはしないで下さい。
(例えばPCI-PCIブリッジは私の環境には存在しません)
そもそも PCI ってなに?ということに関しては以下の記事がイメージを掴みやすかったです。

www.buffalo.jp

商品としては以下の様なものなんかがありました。
PCI ボードに USB2.0 ポートが4つ搭載されてます)

www.amazon.co.jp

私はここで早とちりして「USBポートが4つあるならファンクションも4つあるのかな?」と思っていました。
しかしどうもそういうわけではないようです。
冷静に考えればUSBの最大接続数 256 (たぶん)はデバイスに搭載できるファンクション数 8 よりも圧倒的に多いので当然といえば当然です。
そもそも USB の制御を行うのは HCI 達なので PCI の領分ではありません。
あと、想定するシステムにおけるデータの読み込みと書き込みについても少し勘違いしている部分がありました。

f:id:ymzkmtfm:20210506111428p:plain
IN命令とOUT命令を使ったデータの読み込み

OUT命令はIOポートに対してデータを出力する命令で、IN命令はIOポートからデータを受け取る命令です。
なのでデータを読むためにはOUT命令とIN命令を両方使用しなければなりません。
ずっとIOポートの概念が理解できずにメモリに読み書きしているものだと思っていました。
アドレスとか出てきますがメモリとは関係ありません。
(本文にはしっかり書いてあります。太字で)

本物と違うところ

・探索を再帰的に行っていない
入手したデータの保存方法がうまく思いつかなかったので諦めました。
Rustの実装ではあり得る組み合わせを全探索しています。

感想

・USBは電源つけっぱなしで抜き差しできるけど、PCIって電源つけっぱなしで抜き差しできるんですかね?
C++の写経はそろそろ限界かも…
・でも実行結果を眺めるだけってのもいやだしもう少し頑張る

【ゼロからのOS自作入門】MikanOSをRustに移植する 5章

前回の続きです。

ymzkmtfm.hatenablog.com

https://www.amazon.co.jp/%E3%82%BC%E3%83%AD%E3%81%8B%E3%82%89%E3%81%AEOS%E8%87%AA%E4%BD%9C%E5%85%A5%E9%96%80-%E5%86%85%E7%94%B0-%E5%85%AC%E5%A4%AA/dp/4839975868/ref=sr_1_1?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&dchild=1&keywords=%E3%82%BC%E3%83%AD%E3%81%8B%E3%82%89%E3%81%AE&qid=1619090804&sr=8-1

↑の5章をrustに移植しました。
今回はちょっと流し気味になっちゃいました。

成果物

github.com

本物と違うところ

printk未実装

ピュア Rust のグローバル変数は色んな意味で使えません。
今のところ困ってないので後回しにしました。
グローバル変数を使いやすくする crate には lazy_static や once_cell などがあるらしいです。
頃合いを見て実装したいと思います。

github.com

github.com

ハードコーディングしたフォント

なんかごめん

感想

・インクリメンタルなのに破綻しない構成になってるのすごい
・文字が表示できると結構感動する

【ゼロからのOS自作入門】MikanOSをRustに移植する 4章

前回

ymzkmtfm.hatenablog.com

続きです。

https://www.amazon.co.jp/%E3%82%BC%E3%83%AD%E3%81%8B%E3%82%89%E3%81%AEOS%E8%87%AA%E4%BD%9C%E5%85%A5%E9%96%80-%E5%86%85%E7%94%B0-%E5%85%AC%E5%A4%AA/dp/4839975868/ref=sr_1_1?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&dchild=1&keywords=%E3%82%BC%E3%83%AD%E3%81%8B%E3%82%89%E3%81%AE&qid=1619090804&sr=8-1

↑の4章をrustに移植しました。 3章で頭を悩ませた内容の解決策とかが書いてあったりしてなるほどなぁという感じでした。

成果物

github.com

本物と違うところ

C++の機能を使ってない

Rustで作ってるのでそりゃそうです。
本物のMikanOSでは配置newと継承を使って実行時に RGBResv8bitPerColorPixelWriter と BGRResv8bitPerColorPixelWriter の生成を切り替えています。
しかし、Rust には配置newも継承も(たぶん)ありません。
(そもそもクラスがないので比較しずらいのですが…)
そのため以下の様な実装は怒られます。

let v = match 式 {
    パターン1 => A::new(),  //A型を返す
    パターン2 => B::new(),  //B型を返す
}  //エラー:vの型が決定できない

これを避けるために一番最初に思いつく実装は以下の様な感じでしょう。
(後述の理由でこの実装は避けました)

struct Hoge<T: some_trait> {
  fuga: Box<T>,
}

Tは trait という Rust 特有の機能、Box は T を実装した構造体へのスマートポインタです。
traitはほぼ Interface ですが、Rust には構造体と構造体のふるまいしか定義できません。
(クラスがないので Interface と呼ぶのは多分不適切)
そのため構造体に共通のふるまいは trait を使って抽象化します。
上記の実装は T というふるまいを持った複数の構造体を Hoge という単一の型で実装できます。
このような trait を使った構造体はトレイトオブジェクトと呼ばれます。
ただし、トレイトオブジェクトにはメモリの動的確保が必須です。
(ふるまいしか書いてないので構造体のサイズが静的にわからない)
(Rustでは動的に型を解決する手法を動的ディスパッチと呼びます。たぶん)
現状メモリの動的確保ができないのでトレイトオブジェクトは使えません。
そこで今回実装したのは以下の様な実装です。

pub enum Writer {
    Rgb(RGBWriter),
    Bgr(BGRWriter),
}

impl Writer {
    pub fn vertical_resolution(&self) -> usize {
        match self {
            Writer::Rgb(w) => w.vertical_resolution(),
            Writer::Bgr(w) => w.vertical_resolution(),
        }
    }

    pub fn horizontal_resolution(&self) -> usize {
        match self {
            Writer::Rgb(w) => w.horizontal_resolution(),
            Writer::Bgr(w) => w.horizontal_resolution(),
        }
    }

    pub fn write(&self, x: usize, y: usize, c: PixelColor) {
        match self {
            //安全性はframe_buffer_base依存
            Writer::Rgb(w) => unsafe { w.write(x, y, c) },
            Writer::Bgr(w) => unsafe { w.write(x, y, c) },
        }
    }
}

この実装は Enum を使っています。 Rust の Enum はとても柔軟なデータ構造になっていて、構造体を列挙することができます。
(この柔軟性の由来は関数型言語にあるとかなんとか)
しかも Enum にメソッドや trait を定義することすら可能です。
(詳細は Enumを定義する - The Rust Programming Language 日本語版 )
しかもこの実装は静的に可能です。(列挙しているので構造体のサイズは全て把握できる)
もちろん使うたびにパターンマッチングしなければいけないので条件分岐の数はあまり減っていませんが、それを差し引いても現状に照らし合わせると良いこと尽くめです。
(さらに、本当かどうかはわかりませんが trait の動的ディスパッチより Enum の方が早いという噂もあります。ただ理由はよくわかりませんでした。メモリの動的確保が遅いんでしょうか?)
Rust でトレイトオブジェクトと enum のディスパッチ速度比較 - Qiita
enum_dispatch - Speed up your dynamic dispatched trait method calls by up to 10x : rust
というわけでメモリの動的確保ができるまでちょっとした(列挙できる程度の)多相性は Enum でやっていこうと思います。

感想

・そろそろC++とRustの違いが大きくなってくるのかな?
・機能が増えていくの楽しい

【ゼロからのOS自作入門】MikanOSをRustに移植する 3章

前回

https://ymzkmtfm.hatenablog.com/entry/2021/03/28/165022

続きです。

https://www.amazon.co.jp/%E3%82%BC%E3%83%AD%E3%81%8B%E3%82%89%E3%81%AEOS%E8%87%AA%E4%BD%9C%E5%85%A5%E9%96%80-%E5%86%85%E7%94%B0-%E5%85%AC%E5%A4%AA/dp/4839975868/ref=sr_1_1?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&dchild=1&keywords=%E3%82%BC%E3%83%AD%E3%81%8B%E3%82%89%E3%81%AE&qid=1619090804&sr=8-1

↑の3章をrustに移植しました。 あとサボりすぎて1章進めるうちに1ヵ月経ってました。

成果物

github.com

本物と違う所

kernelにbase addrを設定していない

rust はリンカに ld.lld が指定できるので確実に設定可能です。
ただ設定の仕方がよくわからなかったので諦めました。
(リロケーションや静的リンクに関しては設定できました。)
結果としてバイナリがちょっと複雑になって単純にロードしても動きませんでした。
仕方ないので簡易的な(というよりも中身スカスカの嘘実装な)ELFローダを書いて対処しました。
もっといい方法があれば教えてください。

エラー処理

エラーがあったらメモリにマップされている VGA に出力します。
正常に動いてから作ったので動作未検証です。
(コードは↓から拝借しました)

github.com

なんで VGA なんだよと疑問に思う方もいらっしゃるかもしれません。
rust と uefi-rs の力でエラーがあったら大体しっかり panic() を呼んでくれます。
エラー処理はこの panic() に書いとけばまぁなんとかなるのですが色々あって UEFI の各種機能を呼ぶのは面倒くさいんです。
めんどくさくなった私の結論としてはもう出力してくれるメモリに直接書き込むことにしました。
それでちょうどいいのが VGAだったので今回の実装になっています。

躓いたところ

依存しているライブラリの拡張機能の使い方

Cargo.toml に feature=["exts", "alloc"] と書きましょう。

format! マクロと vec

extern crate alloc しましょう。
ただアロケータが何者でどこからどこまでやってるのかはよくわかってません。
しかもアロケータに関する諸々を実装する必要がありちょっとどうしたらいいか悩みました。
ただ uefi-rs はそこらへんも色々提供してくれてるみたいです。ありがたや。
マクロも自分で実装できるのかもしれませんが私にはよくわかりませんでした。

entry_pointの呼び出し方

最初は「rust で関数生ポインタとか操作できるのかな?」と思っていましたができました。
以下の二つは大変参考になりました。

github.com (180行目)

gitlab.redox-os.org (enter関数内73行目)

感想

・移植するときは使用しているツールも含めて移植可能か検討しておく方がいいかも
・Rust はコンパイラに従ってなんとなく型を直すだけで割と動くので面白い
・安全かどうかわからない unsafe を使いまくると Rust でもしっかりバグる(安全な unsafe は割と大丈夫)
コンパイル通るだけで感動する
・画面塗りつぶすだけで感動する

【ゼロからのOS自作入門】MikanOSをRustに移植する 1章・2章

https://www.amazon.co.jp/%E3%82%BC%E3%83%AD%E3%81%8B%E3%82%89%E3%81%AEOS%E8%87%AA%E4%BD%9C%E5%85%A5%E9%96%80-%E5%86%85%E7%94%B0-%E5%85%AC%E5%A4%AA/dp/4839975868

↑を進める傍らRustにも移植しているのでメモ書き程度に残しておく。

1章

uefi-rsが優秀なので特に言うことはない。

Cargo.toml

[package]
name = "rust-uefi"
version = "0.1.0"
authors = ["callus-corn <mtfm_ymzk@yahoo.co.jp>"]
edition = "2018"

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

[dependencies]
uefi = { git = "https://github.com/rust-osdev/uefi-rs.git" }

cargo/config.toml

[unstable]
build-std = ["core", "compiler_builtins"]
build-std-features = ["compiler-builtins-mem"]

[build]
target = "x86_64-unknown-uefi"

main.rs

#![feature(abi_efiapi)]
#![no_std]
#![no_main]

use uefi::prelude::*;
use core::panic::PanicInfo;
use core::fmt::Write;

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

#[entry]
fn efi_main(_handle: Handle, system_table: SystemTable<Boot>) -> Status {
    writeln!(system_table.stdout(), "Hello, world!").unwrap();

    loop {}
    //Status::SUCCESS
}

参考:

neriring.hatenablog.jp

os.phil-opp.com

2章

かなり詰まった。 詰まった個所は後述。

main.rs

#![feature(abi_efiapi)]
#![no_std]
#![no_main]

use uefi::prelude::*;
use uefi::table::boot::{MemoryType, MemoryAttribute};
use uefi::proto::loaded_image::LoadedImage;
use uefi::proto::media::fs::SimpleFileSystem;
use uefi::proto::media::file::{File, RegularFile, Directory, FileMode, FileAttribute};
use core::panic::PanicInfo;
use core::fmt::Write;

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

//数値→ascii列変換
fn u32_to_ascii(number: u32) -> [u8;8] {
    let mut result: [u8;8] = [0;8];
    let radix = 16;
    let len = result.len();
    for i in 0..len {
        let target_4bit = ((number >> i*4) % radix) as u8;
        if target_4bit <= 0x9 {
            result[i] = 0x30 + target_4bit;
        } else if target_4bit >= 0xa && target_4bit <= 0xf {
            result[i] = 0x57 + target_4bit;
        }
    }
    result
}

//数値→ascii列変換
fn u64_to_ascii(number: u64) -> [u8;16] {
    let mut result: [u8;16] = [0;16];
    let radix = 16;
    let len = result.len();
    for i in 0..len {
        let target_4bit = ((number >> i*4) % radix) as u8;
        if target_4bit <= 0x9 {
            result[i] = 0x30 + target_4bit;
        } else if target_4bit >= 0xa && target_4bit <= 0xf {
            result[i] = 0x57 + target_4bit;
        }
    }
    result
}

#[entry]
fn efi_main(handle: Handle, system_table: SystemTable<Boot>) -> Status {

    writeln!(system_table.stdout(), "Hello, world!").unwrap();

    //↓メモリマップの取得
    //メモリマップを書き込むバッファ(サイズは適当)
    let memory_map_buffer: &mut [u8] = &mut [0; 4096*4];
    //帰ってくるのはmap_keyとdescriptorのイテレータ(イテレータの中にメモリマップがある)
    //このResultはuefi-rs独自実装のためunwrap_successを使う。
    let (_memory_map_key, descriptor_iter) = system_table.boot_services().memory_map(memory_map_buffer).unwrap_success();
    //↑メモリマップの取得
    
    //↓ルートディレクトリを開く
    //ほしいプロトコルを指定してHandleを渡す。帰ってくるのはUnsafeCell<プロトコル>なのでgetで中身を取り出す
    let loaded_image = system_table.boot_services().handle_protocol::<LoadedImage>(handle).unwrap_success().get();
    //生ポインタを解決するのでunsafe
    let device;
    unsafe {
        device = (*loaded_image).device();
    }
    let file_system = system_table.boot_services().handle_protocol::<SimpleFileSystem>(device).unwrap_success().get();
    //再度生ポインタ
    let mut root_dir: Directory;
    unsafe {
        root_dir = (*file_system).open_volume().unwrap_success();
    }
    //↑ルートディレクトリを開く

    //↓メモリマップの保存
    //保存するファイルの作成とFileHandleの取得
    let memory_map_file_handle = root_dir.open("\\memmap",FileMode::CreateReadWrite,FileAttribute::empty()).unwrap_success();
    //RegularFileに変換する必要あり(unsafe)
    let mut memory_map_file: RegularFile;
    unsafe {
        memory_map_file = RegularFile::new(memory_map_file_handle);
    }
    //ヘッダの書き込み
    let header: &[u8] = "Type, PhysicalStart, NumberOfPages, Attribute\n".as_bytes();
    memory_map_file.write(header).unwrap_success();
    //メモリディスクリプタの書き込み
    for descriptor in descriptor_iter {
        let memory_type:u32 = match descriptor.ty {
            MemoryType::RESERVED => 0,
            MemoryType::LOADER_CODE => 1,
            MemoryType::LOADER_DATA => 2,
            MemoryType::BOOT_SERVICES_CODE => 3,
            MemoryType::BOOT_SERVICES_DATA => 4,
            MemoryType::RUNTIME_SERVICES_CODE => 5,
            MemoryType::RUNTIME_SERVICES_DATA => 6,
            MemoryType::CONVENTIONAL => 7,
            MemoryType::UNUSABLE => 8,
            MemoryType::ACPI_RECLAIM => 9,
            MemoryType::ACPI_NON_VOLATILE => 10,
            MemoryType::MMIO => 11,
            MemoryType::MMIO_PORT_SPACE => 12,
            MemoryType::PAL_CODE => 13,
            MemoryType::PERSISTENT_MEMORY => 14,
            _ => 0xffff_ffff,
        };
        let physical_start = descriptor.phys_start;
        let number_of_pages = descriptor.page_count;        
        let attribute: u64 = match descriptor.att {
            MemoryAttribute::UNCACHEABLE => 0x1,
            MemoryAttribute::WRITE_COMBINE => 0x2,
            MemoryAttribute::WRITE_THROUGH => 0x4,
            MemoryAttribute::WRITE_BACK => 0x8,
            MemoryAttribute::UNCACHABLE_EXPORTED => 0x10,
            MemoryAttribute::WRITE_PROTECT => 0x1000,
            MemoryAttribute::READ_PROTECT => 0x2000,
            MemoryAttribute::EXECUTE_PROTECT => 0x4000,
            MemoryAttribute::NON_VOLATILE => 0x8000,
            MemoryAttribute::MORE_RELIABLE => 0x10000,
            MemoryAttribute::READ_ONLY => 0x20000,
            MemoryAttribute::RUNTIME => 0x8000_0000_0000_0000,
            _ => 0,
        };

        //上手く変換できなかったのでゴリ押し
        //絶対にもっといい方法がある
        let buffer: &mut [u8] = &mut [0;63];
        let memory_type = u32_to_ascii(memory_type);
        let physical_start = u64_to_ascii(physical_start);
        let number_of_pages = u64_to_ascii(number_of_pages);
        let attribute = u64_to_ascii(attribute);

        //memory_typeゴリ押し
        let memory_type_len = memory_type.len();
        //下駄。paddingといっていいんだろうか?
        let padding = 0;
        for i in 0..memory_type_len {
            buffer[padding+i] = memory_type[memory_type_len-i-1];
        }
        buffer[padding+memory_type_len] = 0x2c;//,
        buffer[padding+memory_type_len+1] = 0x20;//空白

        //physical_startゴリ押し
        let physical_start_len = physical_start.len();
        let padding = memory_type_len + 2;
        for i in 0..physical_start_len {
            buffer[padding+i] = physical_start[physical_start_len-i-1];
        }
        buffer[padding+physical_start_len] = 0x2c;//,
        buffer[padding+physical_start_len+1] = 0x20;//空白

        //number_of_pagesゴリ押し
        let number_of_pages_len = number_of_pages.len();
        let padding = memory_type_len + 2 + physical_start_len + 2;
        for i in 0..number_of_pages_len {
            buffer[padding + i] = number_of_pages[number_of_pages_len-i-1];
        }
        buffer[padding+number_of_pages_len] = 0x2c;//,
        buffer[padding+number_of_pages_len+1] = 0x20;//空白

        //attributeゴリ押し
        let attribute_len = attribute.len();
        let padding = memory_type_len + 2 + physical_start_len + 2 + number_of_pages_len + 2;
        for i in 0..attribute_len {
            buffer[padding+i] = attribute[attribute_len-i-1];
        }
        buffer[padding+attribute_len] = 0x0a;//LF

        memory_map_file.write(buffer).unwrap_success();
    }
    //書き込みの反映。自分の環境ではこれを書かないと変更が反映されなかった
    memory_map_file.flush().unwrap_success();
    //↑メモリマップの保存

    writeln!(system_table.stdout(), "Kernel did not execute").unwrap();

    loop {}
    //Status::SUCCESS
}

(cargo関係は変更なし)

詰まったところ

・対応する実装が分からない

例えばgBS->OpenProtocol()を移植しようと思ってもuefi-rsからは直接使えません。

SystemTable.boot_services().handle_protocol::(handle)とか、uefi-rsでいい感じに移植されている実装を使う必要があります。

そのためには実装に結構目を通さなくては行けなくて、最初の一回目としてはなかなかしんどかったです。

ただ、実装に目を通していくうちにuefi-rsは中々使いやすそうだということもわかってきたのでやったかいはあったかなと思います。(2章の段階でで言うには早いかもしれません)

また、恐らく extern "efiapi" とかで直接UEFIを叩くことはできると思います。

しかし、それはそれでポインタを結構使わないといけないのでなかなか修羅の道だと思います。

・そもそも何を移植しているのかわからない

これはProtocol関係の移植をしているときに結構悩みました。

何を移植しようとしているのか?ということと、どうすれば移植できるのか?ということの両方が欠けていて途方に暮れた形になります。

結局UEFI関係の調べ物を少ししていくうちにぼんやりと概念を理解して、uefi-rsからも比較的簡単に使えることが分かって、ようやく実装にこぎつけられました。

・stdが使えない

これは結構苦痛でした。 出来上がったコードにも苦しさがにじみ出ています。

「format!マクロが使えなくて文字列に変換できない」とか「Stringが使えなくてto_string()が使えない」とか

ここら辺を解決するには結局Rust力が必要で、私は力及ばずゴリ押しする羽目に…。

stdに限らずuefi-rsのエラーがRust力不足で理解できず実装方針を変更することも度々ありました。

感想

ここまでの感想としては

uefi-rs は結構すごい

・「何を作るのか?」「どうやって作るのか?」ということのうち最低でも片方は分かってないと結構しんどい

・rustはコンパイルに失敗するとうるさいのに、成功するとすごく静かでキュンとする

という感じです。

東京工業大学「システム開発プロジェクト応用第二」第一回前半 環境構築メモ

現在、東京工業大学から自作OSの講義動画が公開されている。

 

www.youtube.com

 

どうやらこの講義を担当している特任助教は『ゼロからのOS自作入門』の著者でもあるらしく同著を教科書として採用しているらしい。

 

www.amazon.co.jp

 

『ゼロからのOS自作入門』は既に予約してあるが、せっかくなので暇を見て少しづつ講義動画の方も追ってみようと思います。

ただ、解説として必要な点は概ね講義中に網羅されているだろうし書籍に載っているであろう情報を書いてもしょうがないので記事には躓いた点をピックアップする形で進めていきます。

 

この記事で取り扱う範囲

f:id:ymzkmtfm:20210305160038p:plain

目標とやること

講義ではブートローダーの作成ぐらいまでやっているようですが自分がやっているのは

・実行環境の構築

・イメージファイルの作成

くらいなので(大体動画50分くらい)そこだけ取り扱います。

 

BOOTX64.EFI(Hello, worldプログラム)の作成

EFIファイルってなに?とかそこら辺の話は『ゼロから作るOS自作入門』に書いてあるらしい。

後の方の講義で解説されるかもしれないのでここでは特に解説しません。

 

ファイルそのものは著者のリポジトリに置いてある。

 

github.com

 

ダウンロードしてしまうのが一番手っ取り早いと思う。

名前がhello.efiになっているのでBOOTX64.EFIに変更しておく。

名前を変更しないと『Hello, world!』と表示されるべき場所に『>>Start PXE over IPv4』と表示されてしまう。(何を意味しているのかは分からない)

恐らくファイルには命名規則があるのだと思う。

 

f:id:ymzkmtfm:20210305143850p:plain

hello.efiのまま実行した結果

 

余談ですがバイナリエディタで手打ちするのは結構しんどいです。

 

イメージファイルの作成

 

イメージファイルそのものについてはここらへんを見れば十分だと思います。

 

https://wa3.i-3-i.info/word15853.html

ja.wikipedia.org

 

リンク先を見るのが面倒な人用に誤解を恐れずにまとめると『ファイルをまとめたファイル』(ISOイメージファイルの場合はCD/DVD用)です。

『ファイルをまとめたファイル』という字面は一見すると奇妙ですが圧縮していないZIPファイルみたいなものだと捉えていれば今回の作業には困らないと思います。

また、今回作成したイメージファイルの拡張子は.imgとなっています。

これは歴史的にはフロッピー用みたいですが、現在は何にでも使えるイメージファイルという使われ方をするみたいです。(要調査)

 

ja.stackoverflow.com

 

実際に行う作業は講義動画にも出ていますが,下記の通りです。

f:id:ymzkmtfm:20210305171803p:plain

イメージファイルの作成

このコマンドを実行することで

①disk.imgという200MBのイメージファイルを作成

②disk.imgの中に/EFI/BOOT/というディレクトリを作成

③BOOTX64.EFIを書き込む

という処理をしています。(①と②と③の境界は私が適当に決めました)

備忘録も兼ねて各コマンドが何をしているのかすごく簡単に解説します。

 

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

access.redhat.com

disk.imgという200MBのイメージファイルを作成している。

 

・オプションについて

-f format:フォーマットを指定するオプション。デフォルトでformatはrawなので無くても動く。

 

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

イメージファイルにファイルシステムを作成している。

イメージファイルは『ファイルをまとめたファイル』ですが、ファイルのまとめ方(ファイルシステム)にもいろいろ存在している。

ここで使用しているのはFAT32

後の方にファイルシステムの講義があると思うので詳細はその時まで放置。

 

https://wa3.i-3-i.info/word17344.html

ja.wikipedia.org

 

mkfs.fatで一つのコマンド。

/usr/sbinとかをみるとほかのファイルシステム用のコマンドも見つかる。

mkfsというコマンドも存在するが、どうやら他のファイルシステムを作成するコマンドを実行するコマンドっぽい?

多分mkfsでも作成できるんじゃないかと思う。

 

・オプションについて

-n:ボリューム(論理的な記憶領域)に11文字以下の名前を付けるオプション。デフォルトでは付けない。どこに名前が保存されるのかはわからなかった。

-s:1クラスタ(FATの記憶単位)を構成するセクタ(物理的な記憶単位)の数を指定するオプション。デフォルト動作はよくわからなかった。

-f:file allocation tableの作成個数を指定するオプション。デフォルトでは2。1だと多分不整合が起きたときにマズいんだろうと思う。

-R:Reserved sectors areaの設定らしいがよくわからなかった。デフォルトは32。ブートセクタは1らしい。

en.wikipedia.org

-F:FAT12FAT16FAT32からどれを使うのか指定するオプション。デフォルト動作はよくわからない。

 

mkdir -p mnt

マウントポイントを作成している。

Windowsだと外部記憶装置は(E:)とか勝手に名前を決めてくれるが、Linuxとかだと自分で名前を決めなきゃいけない(多分)。

 

・オプションについて

-p:『ディレクトリの中のディレクトリ』みたいなディレクトリを作る際に、存在しないディレクトリを全て作ってくれるオプション。ここでは必要ないはず。何か意図があるのかもしれない。

 

sudo mount -o loop disk.img mnt

さっき作成したmntディレクトリにdisk.imgをマウントしている

 

・オプションについて

-o:オプションを使用するというオプション(冗談みたいだがそう読める)。loopを指定するとループデバイスとして認識する。ざっくりいうと、本当は物理デバイスに書き込んでから読み書きするところをイメージファイルに直接読み書きできるようになる。

xtech.nikkei.com

 

sudo mkdir -p mnt/EFI/BOOT
sudo cp BOOTX64.EFI mnt/EFI/BOOT/BOOTX64.EFI
sudo umount mnt

いわずもがな

 

実行環境の構築

 この記事では

VirtualBox on Windows

QEMU on WSL2

QEMU on Ubuntu Desktop 20.04.2.0 LTS

・実機(LB-J772X-SH2-KK)

で作成したイメージファイルを実行した。

個人的にオススメはQEMU on WSL2。

 

VirtualBox on Windows

適当にダウンロード

www.virtualbox.org

新規作成しようとするとタイプとバージョンを聞かれるので

タイプ:Other

バージョン:Other/Unknown(64-bit)

で動作した。他は雰囲気で作成。

あと設定→システムから拡張機能の『EFIを有効化(一部のOSのみ)』を有効化しておきます。

起動するとコントロールを奪われます。復帰は右CTRLです。

 

次に作成したimgファイルをUEFI BIOSが参照できる場所に配置します。

ただ、imgファイルを読み込もうとすると『ディスクイメージファイルを開けませんでした。』とか『VERR_NOT_SUPPORTED』などと言われてエラーになります。

f:id:ymzkmtfm:20210306042509p:plain

イメージファイルの読み込みエラー

そこで以下のコマンドでイメージファイルをiso形式(光学ドライブ)とvdi形式(VirtualBox用HDD)に変換しました。

 

・iso形式

$ mv disk.img disk.iso

 

・vdi形式

$ qemu-img convert -f raw -O vdi disk.img disk.vdi

 

docs.openstack.org

 

基本的に作成したイメージファイルはどのストレージデバイスに載せても動きます。

ただ、USBだけはうまく認識してくれませんでした。(理由不明)

 

また、UEFI Internal Shellの優先順位が他のデバイスより高い場合は以下の様な画面が表示されました。

f:id:ymzkmtfm:20210306043703p:plain

UEFI Internal Shell

exitを実行するとUEFIの設定画面が開くので

Boot Maintenance Manager -> Boot Options -> Change Boot Order

から優先順位を変更すれば起動できました。

f:id:ymzkmtfm:20210306044957p:plain

Virtual BoxでHello, world!

 

QEMU on WSL2

 WSL2でQEMUを使うためにインストールしたものは以下の四つです。

qemu-utils (qemu-img)

qemu-system-x86 (qemu-system-x86_64)

qemu (QEMU本体)

・ovmf (OVMF_CODE.fd, OVMF_VARS.fd)

OVMF_CODE.fdとOVMF_VARS.fdは/usr/share/OVMF/の中に存在する。

/usr/share/ovmf/というディレクトリもなぜか作られていた。

しかし中にはOVMF_CODE.fdもOVMF_VARS.fdもなかった。

代わりに何かの鍵(pem)が入っていた。

 

ただし、上記の四つをインストールしただけではX Window Systemが存在しないので

gtk initialization failed

というエラーを起こす。

 

f:id:ymzkmtfm:20210306053508p:plain

X Window Systemを使えない場合のエラー

 

WSL2でX Window Systemを使うためにはWindows側で設定が必要です。

基本的にここの通りにしたらうまくいきました。

 

qiita.com

 

ただ、Extra settingsはいじってあります。 

f:id:ymzkmtfm:20210306144309p:plain

Extra settings

Native openglにチェックを入れるとQEMU

libGL error: No matching fbConfigs or visuals found
libGL error: failed to load driver: swrast

と怒られます。(実行はしてくれます)

f:id:ymzkmtfm:20210306144607p:plain

Native opengl にチェックを入れた場合のエラー

 

残りのパラメータを変えたときの挙動は結構不安定で、QEMUが認証エラーを返す時もあればQEMUは起動はするけど何も表示されないという時もありました。(スクショとれず)

Xserverが起動出来たらネットワークの設定をしてやるとHello, world!が表示されました。

f:id:ymzkmtfm:20210306150015p:plain

ネットワークの設定

f:id:ymzkmtfm:20210306150034p:plain

QEMUでHello, world!
QEMU on Ubuntu Desktop 20.04.2.0 LTS

基本的にWSL2と同じです。

X Window Systemがデフォルトで入ってるのでそこら辺の設定をしなくても動きます。

 

実機

実行できませんでした。

Secure Boot Violation 

Invalid signature detected. Check Secure Boot Policy in Setup

というエラーが出ます。

なんとなく言ってることは分かりますが解決方法は分かりません。

f:id:ymzkmtfm:20210306051635j:plain

Secure Boot Violation

おまけ

Ubuntu 20.04.2.0.LTSに/EFI/BOOT/BOOTX64.EFI相当のファイルが存在するか確認した。

どうやらUbuntu 20.04.2.0.LTSにはそもそも/EFIが存在しないらしい。

ただし/boot/efiというディレクトリがあり、その中には確かにEFI/Boot/bootx64.efiが存在していた。

f:id:ymzkmtfm:20210306053338p:plain

/boot/efi/EFI/Boot/bootx64.efi