zebian.log

技術系備忘録とか

2025年を振り返る

毎年恒例、今年を振り返るシリーズ。

↓去年 zebian.hatenablog.com

人生初LTをした

kernelvm.connpass.com speakerdeck.com

Kernel/VMで人生初のLTをやってきた。3分枠だったので話す内容を調整するのが大変だった。流石に暗記とかは無理なので現地ではカンペガン見でやらせてもらった。

あまり上手く話せた感じはしなかったけど、思ったよりもウケてたのでわざわざ現地まで来てやってよかったなと思った。前々からLTは学生のうちに経験しておきたいなと思っていたので今回挑戦してみたという感じになる。

16ビットCPUを自作した

今年の前半はTang Primer 20KというFPGAボードに自作16ビットCPUを実装するということをやっていた。

技術ブログを書くのをサボっていたので詳細な解説は特に作ってはいないが、簡単にまとめると

  • RISC-V風16ビット固定長(全32命令)
  • CPUの設計はChisel
  • 専用アセンブラをRustで書いた
  • MMIO(メモリマップドI/O)をFPGAに実装し、LEDやディスプレイをメモリの読み書きで制御できる

といった感じで、割と本格的に作り込んだつもりではある。

最終的に簡単なブロック崩しゲームをアセンブラで実装したものを学校行事やNT東京などで展示した。こちらもたくさんの方に見に来ていただけたので出展してよかった。

コードはすべてGitHubに公開しているのでよかったらどうぞ。

その他

今年は気分転換でよく映画館に映画を観に行ったような気がする。AIで遊んだり絵を描いたりはいつものこととして、あとは12月に入ってから本をよく読むようになった。

学生生活最後の1年

就活が爆速で終わったため、今年1年間楽に過ごせるかと思ったら全くそんなことはなかった。

授業選択はあるとはいえ専門学校なので必修が多く、基本朝から夕方まで学校だし、会社事務所でシステムエンジニアのバイトをしており、仕事の方もまあまあ重たい内容をやっていたりして、、、

かといって稼げているかと言われたら全くそうでもなく、むしろ車通学のためガソリン代が馬鹿にならないし、今年は東京での出展なども多く、交通費とホテル代で大赤字だった。

一応良いこともあり、学校内でこんな低レイヤー変人活動をしているのはほぼ自分だけだったため、展示企画を任せてもらえたり、後輩に技術の面白さを伝えられたことは良かったかなと思う。良かったと思いたい。

あとはあまりにも忙しすぎて体調やメンタルを崩すこともよくあったため、今年はあえて頑張らないようにすることを目標に過ごしてたが、それも意外と難しかった。学生にしては本当によくやってると思う。来年こそゆるく生きたい。

まとめ

  • 今年はLTや出展などでリアルイベントの機会が多かった
  • とにかく時間と金が足りなかった
  • 来年から社会人になります

自作OS向けlibcとそのRustバインディングを書いた話

この記事は自作OS Advent Calendar 2025 25日目の記事です。

前回のアドベントカレンダーの記事はこちら。自作OSにDOOMを移植した話です。 zebian.hatenablog.com

また、開発している自作OSのリポジトリはこちらです。 github.com

libcを自作する

DOOMの移植に利用したdoomgenericはC言語で書かれているため、stdio.hstring.hなどにある関数を利用するためには、当然ながらlibcを自作OS環境向けに実装する必要がありました。

newlibやmuslを使っても良かったのですが、自作OSのシステムコールのインターフェースが独自のものであるということと、現状すべてのlibc関数が必要というわけではなかったため、今回はスクラッチで実装しました。

コード例

apps/libc/syscalls.h

#define SN_READ 0
#define SN_WRITE 1
// ...

extern int sys_read(int fd, void* buf, size_t buf_len);
extern int sys_write(int fd, const void* buf, size_t buf_len);

// ...

apps/libc/stdio.c

// ...

int fprintf(FILE *stream, const char *fmt, ...) {
    printf("[DEBUG]fprintf called\n");
    return -1;
}
// ...

int puts(const char *c) {
    int ret = sys_write(FDN_STDOUT, c, strlen(c));

    if (ret == -1)
        return -1;

    ret = sys_write(FDN_STDOUT, "\n", 1);
    if (ret == -1)
        return -1;

    return 0;
}
// ...

FILE *fopen(const char *filepath, const char *mode) {
    // ...

    int fd = sys_open(filepath, flags);
    if (fd == -1)
        return NULL;
    // ...

    FILE *file = (FILE *)malloc(sizeof(FILE));
    file->fd = fd;
    file->buf = NULL;
    file->stat = stat;
    file->pos = 0;
    return file;
}

// ...

libcの関数はとにかくたくさんあるため、ひたすら必要な関数を実装するほかありません。特にprintf関数の実装の難易度がかなり高めということだけ言っておきます。可変長引数のパース、フォーマット文字列を受け取って指定通りの文字列を組み立てる、数値変換などなど、、、

正直、関数の内部実装まで既存のlibcを真似る必要はありませんが、Linuxなど既存プラットフォームで動作するCのコードを自作OS上で動かす(移植する)場合、関数の引数や戻り値、ヘッダファイルのディレクトリ構成は揃えておいたほうが移植によるコードの変更を最小限に抑えることができます。

libcとしてコンパイルするためのオプション

SRC_FILES := stdio.c stdlib.c string.c syscalls.c printf.c window.c ctype.c sys/stat.c
OBJ_FILES := $(SRC_FILES:.c=.o)
LIB_FILE := libc.a

CFLAGS := -Werror -g -m64 -nostdlib -fno-builtin -fno-stack-protector -std=c11

# ...

libcとしてコンパイルするために必要なコンパイルオプション(gcc)は以下のとおりです。

  • -nostdlib: 標準ライブラリをリンクしない
  • -fno-builtin: 関数呼び出しの最適化を無効化
  • -fno-stack-protector: スタック保護機能を無効化

-fno-stack-protectorに関しては必須ではありませんが、このオプションを使わない場合、__stack_chk_fail__stack_chk_guardなどのスタック保護に関する関数を自前で実装する必要があります。

インクルードガード(#ifndef/#define

Cでは同じヘッダファイルをいくつもインクルードすると定義が重複してしまい、コンパイルエラーになります。それを防ぐためにインクルードガードというものがあります。使い方は単純で、#ifndef#endifで定義を囲み、中にインクルードガード用の定数を定義するだけです。普段あまりCを書かないので今回初めてインクルードガードを書きました。めっちゃ便利。

#ifndef _STDIO_H
#define _STDIO_H

#include <stdarg.h>
extern int printf(const char* fmt, ...);

#endif

main関数

Cのmain関数は通常int main(int argc, char const* argv)ですが、-nostdlibコンパイルするとmain関数を書いても自動的に呼び出されません。そこでlibc側でエントリポイント_startを実装し、main関数を呼び出す処理を書くことで、擬似的にmain関数から開始することができます。

apps/libc/main.c

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wimplicit-function-declaration"

#include "stdio.h"

void _start(int argc, char const* argv[]) {
    exit((uint64_t)main(argc, argv));
}

#pragma GCC diagnostic pop

関数を呼び出す前にプロトタイプ宣言が必要になりますが、通常、main関数は引数や戻り値をvoidにするなど、柔軟な書き方をすることができます。main関数のプロトタイプ宣言をしてしまうと、宣言したその特定の型としてしか受け付けることができなくなってしまうため、ここではあえてプロトタイプ宣言をせず、#pragmaコンパイル時の警告を抑制し、リンカによる名前解決のみに頼ります。

プロトタイプ宣言がないため、コンパイラによる型チェックはされませんが、渡される引数はint argcchar const* argv[]であるため、それ以外の型で引数を受け取ろうとすると、voidは大丈夫ですが、実行時にスタックやレジスタが壊れてクラッシュするかもしれません。

ちなみに引数をスタック上に用意するのはカーネルの仕事であるため、カーネル側のコードを変更することで型を変えることもできます。

Rustバインディングを作る

bindgen

カーネルをRustで書いているにも関わらずlibcつまりはC言語を書いているため、システムコールといった橋渡しの部分で使われる専用の構造体や定数をlibc側とカーネル側で2重で書かなければいけません。

最初のうちはそれでもいいと思いますが、libcの構造体が増えるにつれてRustで再定義しなければいけない数も増えますし、構造体のメンバーの一部を変更、定数を変更するといった場合に二度手間になります。

そこでCのヘッダファイルからRustのバインディングコードを自動生成してくれるbindgenというクレートを使います。

#define IOMSG_CMD_REMOVE_COMPONENT 0x80000000
#define IOMSG_CMD_CREATE_COMPONENT_WINDOW 0x80000001
#define IOMSG_CMD_CREATE_COMPONENT_IMAGE 0x80000002

typedef struct {
    uint32_t cmd_id;
    uint32_t payload_size;
} iomsg_header;

ヘッダファイルに書いた定数や構造体から

pub const IOMSG_CMD_REMOVE_COMPONENT: u32 = 2147483648;
pub const IOMSG_CMD_CREATE_COMPONENT_WINDOW: u32 = 2147483649;
pub const IOMSG_CMD_CREATE_COMPONENT_IMAGE: u32 = 2147483650;

#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct iomsg_header {
    pub cmd_id: u32,
    pub payload_size: u32,
}
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
const _: () = {
    ["Size of iomsg_header"][::core::mem::size_of::<iomsg_header>() - 8usize];
    ["Alignment of iomsg_header"][::core::mem::align_of::<iomsg_header>() - 4usize];
    ["Offset of field: iomsg_header::cmd_id"]
        [::core::mem::offset_of!(iomsg_header, cmd_id) - 0usize];
    ["Offset of field: iomsg_header::payload_size"]
        [::core::mem::offset_of!(iomsg_header, payload_size) - 4usize];
};

自動でこのようなRustコードを生成してくれます。

ライブラリとして新しくRustプロジェクトlibc-rsを作り、バインディングコードの生成を指示するbuild.rsを書きます。

libc-rsは別のRustプロジェクトの依存に追加することで参照することができます。

apps/libc-rs/build.rs

fn main() {
    // Cargo.tomlのfeaturesで指定されたフラグを確認
    let is_for_kernel = std::env::var("CARGO_FEATURE_KERNEL").is_ok();
    if !is_for_kernel {
        println!("cargo:rustc-link-lib=static=c_with_main");
    }

    // ヘッダファイルを登録
    let mut builder = bindgen::Builder::default();
    for header in headers {
        builder = builder.header(header.to_str().unwrap());
    }

    let bindings = builder
        .use_core() // stdではなくcoreクレートを使用する型を生成
        .generate()
        .expect("Failed to generate bindings");
    // bindings.rsを生成する
    bindings
        .write_to_file(out_path.join("bindings.rs"))
        .expect("Failed to write bindings");
}

apps/libc-rs/src/lib.rs

include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

kernel/Cargo.toml

[package]
name = "kernel"
version = "0.1.0"
edition = "2021"
authors = ["Zakki <zakki0925224@gmail.com>"]
description = "A hobby operating system written in Rust."

[dependencies]
common = { path = "../common" }
libc-rs = { path = "../apps/libc-rs", features = ["kernel"] }
pci-ids = "0.2.5"

libc-rsとRust製ユーザーアプリ

libc-rskernelから参照することで、libcで定義した構造体の型定義がカーネルから使えるようになりますが、libc-rsを参照してRustでユーザーアプリを作ることもできます。Rustでユーザーアプリを作る場合はno_std(標準ライブラリを使わない)環境にしないといけませんが、その場合VecStringといった動的メモリ確保が必要な機能や、パニック時に必要なハンドラが利用できなくなります。

そのため、no_std環境では動的メモリ確保を行うためのglobal_allocatorとパニックハンドラpanic_handlerを実装する必要があります(ちなみにno_std環境でコンパイルするためにはpanic_handlerの実装は必須です)。

しかし、Rustでユーザーアプリを作るたびに毎回実装しなければならないとなると非常に面倒くさいので、libc-rs側で実装してしまえという話です。

global_allocatorpanic_handlerについての詳細な解説はWriting an OS in Rustという記事が参考になります。 os.phil-opp.com os.phil-opp.com

apps/libc-rs/src/lib.rs

#![no_std]

#[cfg(not(feature = "kernel"))]
use linked_list_allocator::LockedHeap;

include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

// heap
#[cfg(not(feature = "kernel"))]
#[global_allocator]
static ALLOCATOR: LockedHeap = LockedHeap::empty();

#[cfg(not(feature = "kernel"))]
#[doc(hidden)]
pub fn _init_heap() {
    let heap_size = 1024 * 1024;
    let heap = unsafe { malloc(heap_size as u64) as *mut u8 };
    unsafe {
        ALLOCATOR.lock().init(heap, heap_size);
    }
}

// panic
#[cfg(not(feature = "kernel"))]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
    println!("{:?}", info.message());
    println!("{:?}", info.location());

    unsafe {
        exit(-1);
    }
}

これら以外にも、引数を受け取る処理やprint!/println!マクロの定義など、no_std環境では様々な手続きが必要であるため、それらもまとめて実装してしまいます。

// parse args macro
#[cfg(not(feature = "kernel"))]
#[doc(hidden)]
pub unsafe fn _parse_args(argc: usize, argv: *const *const u8) -> Vec<&'static str> {
    let mut args = Vec::new();
    for i in 0..argc {
        let ptr = *argv.add(i);
        let mut len = 0;
        while *ptr.add(len) != 0 {
            len += 1;
        }

        let slice = core::slice::from_raw_parts(ptr, len);
        let s = match str::from_utf8(slice) {
            Ok(s) => s,
            Err(_) => "",
        };
        args.push(s);
    }

    args
}

#[cfg(not(feature = "kernel"))]
#[macro_export]
macro_rules! parse_args {
    () => {{
        use core::arch::asm;

        let argc: usize;
        let argv: *const *const u8;
        unsafe {
            asm!("mov {}, rdi", out(reg) argc, options(nomem, nostack));
            asm!("mov {}, rsi", out(reg) argv, options(nomem, nostack));
        }

        $crate::_init_heap();
        let args = unsafe { $crate::_parse_args(argc, argv) };
        args
    }};
}

// print macros
#[cfg(not(feature = "kernel"))]
struct Writer;

#[cfg(not(feature = "kernel"))]
impl fmt::Write for Writer {
    fn write_str(&mut self, s: &str) -> fmt::Result {
        unsafe {
            printf(format!("{}\0", s).as_ptr() as *const _);
        }

        Ok(())
    }
}

#[cfg(not(feature = "kernel"))]
#[doc(hidden)]
pub fn _print(args: fmt::Arguments) {
    Writer.write_fmt(args).unwrap();
}

#[cfg(not(feature = "kernel"))]
#[macro_export]
macro_rules! print {
    ($($arg:tt)*) => ($crate::_print(format_args!($($arg)*)));
}

#[cfg(not(feature = "kernel"))]
#[macro_export]
macro_rules! println {
    () => ($crate::print!("\n"));
    ($($arg:tt)*) => ($crate::print!("{}\n", format_args!($($arg)*)));
}

libc-rs側で複雑な手続きを実装することによって、Rustでユーザーアプリを書く際に実装の手間が大幅に減ります。

libc-rsからシステムコールを呼び、embedded-graphicsクレートを使ってライフゲームを作りました。

apps/lifegame/src/main.rs

#![no_std]
#![no_main]

use embedded_graphics::{pixelcolor::Rgb888, prelude::*, primitives::*};
use libc_rs::*;

// ...

#[no_mangle]
pub unsafe fn _start() {
    let _args = parse_args!();

    let title = "lifegame\0";
    let cdesc_window = create_component_window(
        title.as_ptr() as *const _,
        100,
        100,
        WIDTH + 10,
        HEIGHT + 50,
    );
    if cdesc_window.is_null() {
        println!("Failed to create component window");
        exit(-1);
    }

    let fb = malloc((WIDTH * HEIGHT * 4) as u64);
    if fb.is_null() {
        println!("Failed to allocate framebuffer memory");
        exit(-1);
    }

    let cdesc_image =
        create_component_image(cdesc_window, WIDTH, HEIGHT, PIXEL_FORMAT_BGRA as u8, fb);
    if cdesc_image.is_null() {
        println!("Failed to create component image");
        exit(-1);
    }

    let mut eg_fb = Framebuffer {
        fb: fb as *mut u8,
        width: WIDTH,
        height: HEIGHT,
    };

    initialize_board();
    draw_board(&mut eg_fb, 0);

    loop {
        let start_time = sys_uptime();
        while sys_uptime() - start_time < DELAY_MS {
            // wait
        }

        unsafe {
            GENERATION += 1;
        }
        compute_next_generation();
        unsafe {
            draw_board(&mut eg_fb, GENERATION);
        }
    }
}

featureで使い分ける

Rustにはfeatureという機能があり、特定の機能の有効/無効を切り替えるフラグを付けることができます。 doc.rust-lang.org

libc-rsはRust製ユーザーアプリでもカーネルでも参照されることになるのですが、global_allocatorpanic_handlerは1プロジェクトにつき1つしか実装することができないという決まりがあります。もちろんカーネル側でも実装しているため競合することになるのですが、そこでfeatureを活用し、フラグ制御で実装を消します。

kernel/Cargo.toml

[package]
name = "kernel"
version = "0.1.0"
edition = "2021"
authors = ["Zakki <zakki0925224@gmail.com>"]
description = "A hobby operating system written in Rust."

[dependencies]
common = { path = "../common" }
libc-rs = { path = "../apps/libc-rs", features = ["kernel"] }
pci-ids = "0.2.5"

カーネルCargo.tomllibc-rskernelフラグを指定します。

apps/libc-rs/src/lib.rs

// heap
#[cfg(not(feature = "kernel"))]
#[global_allocator]
static ALLOCATOR: LockedHeap = LockedHeap::empty();

#[cfg(not(feature = "kernel"))]
#[doc(hidden)]
pub fn _init_heap() {
    let heap_size = 1024 * 1024;
    let heap = unsafe { malloc(heap_size as u64) as *mut u8 };
    unsafe {
        ALLOCATOR.lock().init(heap, heap_size);
    }
}

// ...

#[cfg(not(feature = "kernel"))]つまり「kernelフラグが立っているときは実装しない」ということになります。C言語で言う条件付きコンパイルのようなものです。

まとめ

  • libcを自作した。必要な関数だけをつまんで実装していくほうが良いと思う。
  • bindgenを使ってlibcのRustバインディングを作った。
  • no_std Rustの面倒くさい部分はバインディングライブラリで隠してしまおう。

Github CopilotではLSPがあなたにプロトコル実装を強制する

あえてクオリティを落とす勇気

完璧主義な自分が最近意識していることをまとめる。

正直技術系以外のポエミーな話題を書こうかかなり長い時間迷っていたが、最近ブログのネタが無くてほとんど更新できてないということと、案外自分のブログはあとから見返すことが多いので自戒を込めて書こうと思った。

完璧主義と高いクオリティ

学校の課題でも仕事でも趣味でも、持ち前の完璧主義という性格のせいで割と何でも無意識に高いクオリティを目指そうとしてしまう、ということが長い間続いてきた。

普通に考えると、高いクオリティを目指すのは良いことではある。常にクオリティを上げるために自己研鑽できるし、周囲から良い評価をもらえて一目置かれるし、そのおかげで美味しい話にこぎつけたりもする。

しかし、我々は人間であり機械ではないため、常にそんなことをしていると普通に疲労とストレスが溜まる。そして抜けられない負のスパイラルへ、、、

  • 完璧主義という性格特性上、手を抜くのが許せない
  • 周りが褒めてくれるため自己承認欲求が高まる
  • 失敗するのが怖い(というよりも失敗によって周りから馬鹿にされることに対する恐怖)

クオリティが高いことだけが良いことではない

最近趣味で絵を描いていて気づいたこと。何時間もかけて丁寧に仕上げた絵よりも、数分で適当に描いたラフのほうが上手く見えるということがよくある。上手い下手は一旦置いておいて、高いクオリティを目指そうとするあまり、気合いが入りすぎて視野が狭くなっており、全体を俯瞰して見ることができていなかったということだと思う。

これは絵描き以外のことでも当てはまると思っていて、ある特定の領域だけ飛び抜けてクオリティが高いところで全体との辻褄が合わなくなるし、全体のクオリティがものすごく高かったとしても、例えばそれが課題や仕事だった場合、そもそも求められていることと頓珍漢な方向に伸びていたら意味がない

評価基準をカンストすることは決して無駄ではないが、価値に見合わない

課題(ここでは数学の問題集のような正解/不正解のブーリアン回答ではなく、作文や工作、研究課題などの評価基準が明確でないものをいう)や仕事でも、自分自身が求めるクオリティが、相手が求めるクオリティの何十倍も先を行っていたということは結構あったりする。

結局どれだけすごいことをこなしたとしても、相手の評価基準を超えたものは「すごい」としか認識できないため、その評価基準を上手いこと把握して、それを少し上回る程度で頑張ればいいのではないだろうか。

いや、むしろ頑張らなくていいのではと最近は思う。常に高得点を維持したところで成績がさほど重要でないなら意味がないし、仕事であればその働きに見合う給料を貰わないとただのボランティアである。究極のところ自己満足との戦いであり、そんな日々のつまらない課題や仕事で貴重なエネルギーや時間を消費するのはもったいない。完璧主義の使いどころを履き違えている、と考えるが正しいのかもしれない。

個人的AI使い分け

2025年現在、各AIをどのように使い分けているかをメモしておく。

  • ChatGPT(無料版)

    • 適当な雑談
    • 技術のニッチな領域に関する相談
    • 文章添削
    • コーディングなど
  • Gemini(Google AI Pro)

    • ネット検索/Deep Research
    • NotebookLM
    • マルチモーダル(画像/PDFに関する質問など)
    • カスタムGemでさまざまな人格を作って遊ぶ
    • ChatGPTがGPT-5に置き換えられて口調がウザくなってしまったので、今はほぼGeminiメイン
  • Grok(無料版)

    • Xのポストの検索/分析
  • Perplexity(無料版)

    • ネット検索
  • GitHub Copilot Pro

    • プレミアムリクエストを消費してClaude Sonnet 4.5を使う(Agentが強い)
    • プレミアムリクエストを消費しないモデルはどれがいいのか正直よくわからん

2024年を振り返る

毎年恒例、今年を振り返るシリーズ。去年はXの埋め込みを貼りすぎてページが重くなってしまったので、今回はもっとざっくりと書く。

技術関係

自作OS開発は通年営業として、今年はセキュリティにも(少しだけ)力を入れた開発をした。学校の課題も兼ねているなど、割と大がかりなものはブログにまとめていたりするので、ぜひ読んでほしい。

zebian.hatenablog.com zebian.hatenablog.com zebian.hatenablog.com zebian.hatenablog.com

就活の年!

今年を一言で表すならば、「就活の年」だったと思う。6月頃にサマーインターンのエントリーを行い、課題を送ったり面接を受けたり、、、夏休み期間中に3社インターンに参加することができた。

3社ともセキュリティ関係の会社で、内容も盛りだくさんでとても勉強になったし、交通費やホテル代なども全額支給していただき、地方住みにとってはかなりありがたかった。

会社の人もそうだし、他の参加者の方も技術に長けた人間ばかりだったため、技術系の会話が弾んでとても楽しかったし、インターンを通して人脈が広がったりと、とにかくいいことづくめだった。貴重な機会に感謝。

そのうちのある1社がとても気に入り、11~12月頃に早期選考を受け、無事内定をいただくことができた。個人的にとても満足な結果であったため、内定を承諾し、年が明ける前に就活を終わらせることができた。

就活期間が半年足らずというかなりの爆速スケジュールで、同級生の誰よりも早く内定が決まり、想像していたよりもあっけなかった。いろいろな人にアドバイスをいただいてここまでこぎつけることができたので、とにかく感謝しかない。

観光名所巡り

せっかく自分の車を持っているので、今年は地元の観光名所をたくさん回った。インスタの方に写真を上げているのでよかったら見てほしい。

https://www.instagram.com/zakki0925224_/

↑みたいな悩みもありほとんどソロなので、今後はもっと大勢で行きたい。

お絵かき

ChromebookからXiaomi Pad6に移行し、お絵描きアプリが使えるようになったり、マシンスペック的にも快適に描けるようになった。相変わらず下手くそなのでもっと上手くなりたい。

総括

一年が終わるの早すぎる。来年は交通費を貯めてもっと行動の幅を広げたい。

DOOMチャレンジ on 自作OS

はじめに

この記事は自作OS Advent Calendar 2024 19日目の記事です。

DOOMチャレンジ達成当日にXに投稿したところ、たくさんの反響をいただきました。

DOOMチャレンジとは

id Softwareが1993年にリリースしたFPSゲームDOOM」を、様々な電子機器やターゲットに移植する文化のことをDOOMチャレンジといいます。 直近では任天堂が新発売した目覚まし時計「Alarmo」への移植が行われました(笑)。

DOOMチャレンジについてはGIGAZINEでよく記事にされるので、興味のある人は眺めてみると面白いと思います。 gigazine.net gigazine.net gigazine.net

DOOMチャレンジ on 自作OS

タイトルの通り、私の自作OS上でDOOMチャレンジ(移植)をしようという試みです。DOOMは公式によってソースコードが公開されており、先述のように様々なハードウェア・OS向けに派生が作られ続けています。私の自作OSではELFバイナリ(Linux等で用いられる実行ファイル形式)の実行をサポートしており、LinuxDOOMがそのまま動作するというわけではありませんが、少なくとも謎ハードウェアへ移植するよりかはソースコードの改造が遥かに簡単に済みます。

また、私の自作OSはグラフィック関連の実装が最小限であり、絵面が地味になりがちであるため、そろそろ誰が見ても面白いと思えるようなインパクトが欲しいという思いがあり、DOOMチャレンジを行いましたが、それ以外にも、半年ほど前にXのフォロワーさんが書かれた記事を読ませていただいたことが今回のきっかけとなりました。

謎ハードウェアへ移植する話はよく耳にしますが、自作OSへの移植も文化としてあるのかどうかはよくわかりません。

自作OS

github.com

Rustで開発しているx86_64向けOSです。2年ほど開発を続けており、書いては消してを繰り返し、実装途中で放置しているドライバなどもあったりしていて、デジタル盆栽というよりかは最近は闇鍋になりつつあります。今回の実装で重要な部分を抜き出すと、現状の自作OSは以下のような仕様になっています。

  • プロセススケジューリングは未実装で、割り込みやシステムコール、ユーザーアプリの呼び出し以外でタスクの切り替えは行われない
  • 仮想ファイルシステム、オンメモリなFAT32に対応、ただし書き込み不可
  • PS2キーボード/マウス対応
  • Local APICタイマーによる時間計測
  • 簡単なウィンドウマネージャ(ウィンドウの生成、破棄、マウスポインターの操作とウィンドウのドラッグが可能)
  • オリジナルなシステムコール(ユーザーアプリがOSの機能を呼び出す命令)と、libc(標準Cライブラリ)の再実装
  • ユーザーアプリはCまたはRust(自作libcのRust binding)で書き、自作libcとスタティックリンクされたELFバイナリをOSでロードすることができる

全体的な構成図

doomgeneric

github.com

今回利用するDOOMソースコードになります。doomgenericと呼ばれる派生実装を基にしています。doomgenericは移植に特化したソースコードであり、5つの関数を移植したいプラットフォームに合わせて実装するだけで、ゲームを動かすことができます。

DOOMソースコード」という呼び方には少し語弊があり、正確にはDOOMを動かすゲームエンジンソースコードです。実行するためにはDOOMのwadファイル(ゲームデータ)が必要で、デモ版のDOOMシェアウェアとして無料で公開されています。

移植編1 - 自作OS向けにMakefileを書く

doomgenericはWindowsLinuxFreeBSDなどいくつかのプラットフォーム向けにコンパイルできるように設計されています。移植は非常にシンプルで、各プラットフォームごとのMakefileと、プラットフォーム固有なコードが書かれたdoomgeneric_<YOUR_PLATFORM>.cを用意するだけです。

そのままmakeコマンドでコンパイルすると、X11 on Linux向けのELFバイナリが吐き出されます。-iwadとWADファイルのパスを指定すると、ゲームが起動します。

$ make
$ ./doomgeneric -iwad <PATH_TO_DOOM_WAD_FILE>.wad

doomgenericではサウンド出力が実装されていないため、音声は再生されません。再生するためには自力で実装する必要がありますが、今回は行いませんでした。

既存のMakefileを参考に、自作OS向けMakefileを書きました。以下が完成形になります。

myos-x86_64/third-party/doom-for-myos/Makefile.myos

################################################################
#
# $Id:$
#
# $Log:$
#

ifeq ($(V),1)
    VB=''
else
    VB=@
endif

LIBCDIR := ../../apps/libc
SFDIR := ./berkley-softfloat-3
LIB := -L$(LIBCDIR) -lc -L$(SFDIR)/build/Linux-x86_64-GCC $(SFDIR)/build/Linux-x86_64-GCC/softfloat.a

CC=gcc  # gcc or g++
LD=ld.lld
CFLAGS=-I $(LIBCDIR) -I $(SFDIR)/source/include -O2 -Wall -g -m64 -nostdlib -fno-builtin -mno-mmx -mno-sse -msoft-float -std=c11 -DARCH_MYOS
LDFLAGS=-z norelro --static --image-base=0x10000000

# subdirectory for objects
OBJDIR=build
OUTPUT=doomgeneric

SRC_DOOM = dummy.o am_map.o doomdef.o doomstat.o dstrings.o d_event.o d_items.o d_iwad.o d_loop.o d_main.o d_mode.o d_net.o f_finale.o f_wipe.o g_game.o hu_lib.o hu_stuff.o info.o i_cdmus.o i_endoom.o i_joystick.o i_scale.o i_sound.o i_system.o i_timer.o memio.o m_argv.o m_bbox.o m_cheat.o m_config.o m_controls.o m_fixed.o m_menu.o m_misc.o m_random.o p_ceilng.o p_doors.o p_enemy.o p_floor.o p_inter.o p_lights.o p_map.o p_maputl.o p_mobj.o p_plats.o p_pspr.o p_saveg.o p_setup.o p_sight.o p_spec.o p_switch.o p_telept.o p_tick.o p_user.o r_bsp.o r_data.o r_draw.o r_main.o r_plane.o r_segs.o r_sky.o r_things.o sha1.o sounds.o statdump.o st_lib.o st_stuff.o s_sound.o tables.o v_video.o wi_stuff.o w_checksum.o w_file.o w_main.o w_wad.o z_zone.o w_file_stdc.o i_input.o i_video.o doomgeneric.o doomgeneric_myos.o
OBJS += $(addprefix $(OBJDIR)/, $(SRC_DOOM))

all:  $(OUTPUT)

clean:
  rm -rf $(OBJDIR)
  rm -f $(OUTPUT)
  rm -f $(OUTPUT).gdb
  rm -f $(OUTPUT).map

$(OUTPUT):   $(OBJS)
   @echo [Linking $@]
  make -C $(LIBCDIR)
  make -C $(SFDIR)/build/Linux-x86_64-GCC
  $(VB)$(LD) $(OBJS) $(LIB) -o $@ $(LDFLAGS) --Map=$(OUTPUT).map
   @echo [Size]
   -$(CROSS_COMPILE)size $(OUTPUT)

$(OBJS): | $(OBJDIR)

$(OBJDIR):
  mkdir -p $(OBJDIR)

$(OBJDIR)/%.o:   %.c
   @echo [Compiling $<]
  $(VB)$(CC) $(CFLAGS) -c $< -o $@

print:
   @echo OBJS: $(OBJS)

既存のlibcの代わりに自作libcをリンクしたいため、-nostdlib/-fno-builtinフラグで、自動的にlibc関数が呼ばれてしまうことを抑制し、標準ライブラリに依存しない形にしています。また-nostdlibフラグを設定すると、main関数を呼び出すスタートアップコードがリンクされなくなるため、エントリポイントにmain関数を使用することはできず、代わりに_start関数から始める必要があります。私の自作libcでは、libc側に_start関数を実装し、main関数を呼び出すスタートアップコードを独自に定義しており、この問題を回避しています。

移植編2 - 不足している自作libc関数の追加

プラットフォーム固有コードであるdoomgeneric_myos.cを以下のように仮実装しました。

myos-x86_64/third-party/doom-for-myos/doomgeneric_myos.c

#include "doomgeneric.h"
#include "doomkeys.h"

#include <stdio.h>

void DG_Init()
{
}

void DG_DrawFrame()
{
}

void DG_SleepMs(uint32_t ms)
{
}

uint32_t DG_GetTicksMs()
{
}

int DG_GetKey(int *pressed, unsigned char *doomKey)
{
}

// 必須ではない関数
void DG_SetWindowTitle(const char *title)
{
}

int main(int argc, char **argv)
{
    doomgeneric_Create(argc, argv);

    while (1)
        doomgeneric_Tick();

    return 0;
}

5つの関数DG_Init DG_DrawFrame DG_SleepMs DG_GetTicksMs DG_GetKeyに自作OS向けの実装を行っていきます。それ以外のコードは全プラットフォーム共通の処理であるため、書き換える必要はありません。

自作libcにdoomgenericで呼ばれている関数を確認して仮実装します。これをコンパイルエラーがなくなるまで繰り返します。以下のような関数が実装の対象となる代表的な例です。

  • int toupper(int c)
  • int tolower(int c)
  • int snprintf(char *buf, size_t size, const char *format, ...)
  • int vsnprintf(char *buf, size_t bufsize, const char *format, va_list arg)
  • FILE *fopen(const char *filename, const char *mode)
  • int fclose(FILE *stream)
  • size_t fread(void *buf, size_t size, size_t count, FILE *stream)
  • void *malloc(size_t len)
  • void free(void *ptr)

以下のようにデバッグプリント追加しておくと、今後のデバッグが楽になります。

int vfprintf(FILE *stream, const char *fmt, va_list ap)
{
    printf("[DEBUG]vfprintf called\n");
    return -1;
}

int sscanf(const char *buf, const char *fmt, ...)
{
    printf("[DEBUG]sscanf called\n");
    return -1;
}

移植編3 - 自作OS上でのデバッグ

doomgenericを自作OS向けにコンパイルすることに成功したら、実際に自作OS上でバイナリを実行しながら、追加した関数の内部実装を行います。

OSが起動して初期化が完了すると、自動的にシェルアプリが起動するようになっています。execコマンドでELFバイナリを起動することができます。

libc関数の内部実装については、既存のglibcやnewlibといったコードを参考にしました。今回は車輪の再発明が目的ではないため、コピペ実装も多々あります。

出力を確認しながら、ひたすら関数の実装とデバッグを行う

自作OS上で動かすユーザアプリをGDBデバッグすることはできない(デバッグ環境を整えることが非常に面倒)ため、先述の通り、ほとんどデバッグプリントを使って力技で解決しました。今後はこれらデバッグを容易にする仕組みづくりも行っていきたいと思っています。

移植編4 - SSE命令問題

SSE (Streaming SIMD Extensions)とは、IntelのCPUに内蔵されているSIMD拡張命令セットで、浮動小数点演算を高速化するために利用されます。x86_64向けにC言語floatを使用するコードをコンパイルすると、SSEx87 FPU(浮動小数点演算処理装置)、またはsoftfloat(ソフトウェアによる浮動小数点演算エミュレーション)を使用する必要があり、そうでなければそもそもコンパイルエラーになります。

gccコンパイルすると、floatで専用レジスタxmm0に対してpxor(SSE2命令)が使用される

x87 FPUを使用するためにはコンパイラオプションを設定する

float型を返す関数では専用レジスタxmmを利用する必要があるため、SSEを無効化するとコンパイルエラーになる(ABI違反)

doomgenericではfloat型が使用されており、これを自作OS上で動作させるためには、OS側でSSE命令またはx87 FPUを有効化する処理と、コンテキストスイッチ時に専用レジスタを退避するコードを実装する必要があります。

x87 FPUはx86時代の古い命令サブセットであり、現代のx86_64アーキテクチャではSSEとSSE2がデフォルトで利用可能であるため、まずSSE命令で動かすことを目標にしていましたが、SSE命令の実行時に発生する一般保護例外(General Protection Fault、#GP)をどうしても解決できず、2週間ほどIntel SDMと格闘した末に諦めました。

かなりの時間を費やしてしまったため、一旦気分転換でsoftfloatを検証したところ、想像よりもあっさり動かすことができました。そのため、今回はsoftfloatを採用し、x87 FPUは検証すらしていません(よくない)。

softfloatを使用する場合、コンパイラ浮動小数点演算をエミュレーションする命令を吐いてくれないので、サードパーティ製ライブラリを使用するか、自力で実装する必要があり、手順が少し複雑になります。また、ハードウェア演算と比較して動作が遅くなるという懸念もあります。

github.com

今回はberkeley-softfloat-3を使用しました。

softfloatライブラリで置き換えるためにはfloat型をfloat32_t型で置き換え、演算やキャストで専用の関数を利用する必要があります。また、X11 on Linux向けのMakefileを書き換えたくなかったため、条件付きコンパイルも利用する必要があり、置き換えは以下のようになりました。

myos-x86_64/third-party/doom-for-myos/g_game.c

#include <string.h>
#include <stdlib.h>
#include <math.h>

#ifdef ARCH_MYOS
#include <softfloat.h>
#endif

...

boolean G_CheckDemoStatus(void)
{
    int endtime;

    if (timingdemo)
    {
#ifdef ARCH_MYOS
        float32_t fps;
#else
        float fps;
#endif
        int realtics;

        endtime = I_GetTime();
        realtics = endtime - starttime;
#ifdef ARCH_MYOS
        fps = f32_div(i32_to_f32(gametic * TICRATE), i32_to_f32(realtics));
#else
        fps = (float)(gametic * TICRATE) / (float)realtics;
#endif

        // Prevent recursive calls
        timingdemo = TRUE;
        demoplayback = FALSE;

        I_Error("timed %i gametics in %i realtics (%f fps)",
                gametic, realtics, fps);

...

移植編5 - doomgenericにおける自作OS依存部

doomgeneric_myos.cに戻り、プラットフォーム依存である5つの関数を実装していきます。これらの関数は以下のような役割を持ちます。

関数 役割
void DG_Init() ウィンドウの生成等、初期化処理を行う。
void DG_DrawFrame() 画面バッファ変数DG_ScreenBufferの準備完了後に呼び出され、画面描画の処理を行う。
void DG_SleepMs(uint32_t ms) 引数で受け取ったミリ秒の間、処理をスリープする。
uint32_t DG_GetTicksMs() doomgenericが起動してから現在までの経過時間をミリ秒単位で返す。
int DG_GetKey(int *pressed, unsigned char *doomKey) キーボードイベント。
pressedにキーを押したor離したを、doomKeyにどのキーが押されたのかを渡す。

簡単なフローチャート

X11 on Linux向けのコードであるdoomgeneric_xlib.cを参考に、doomgeneric_myos.cの実装は次のようになりました。

#include "doomgeneric.h"
#include "doomkeys.h"

#include <stdio.h>
#include <syscalls.h>
#include <window.h>
#include <ctype.h>

static uint32_t init_ticks_ms = 0;
static WindowDescriptor *wdesc = NULL;
static char before_input_key = '\0';
static char input_key;

#define KEYQUEUE_SIZE 16

static unsigned short s_keyQueue[KEYQUEUE_SIZE];
static unsigned int s_KeyQueueWriteIndex = 0;
static unsigned int s_KeyQueueReadIndex = 0;

static unsigned char convertToDoomKey(char key)
{
    key = tolower(key);

    switch (key)
    {
    case '\n':
        return KEY_ENTER;
    case 'e':
        return KEY_ESCAPE;
    case 'a':
        return KEY_LEFTARROW;
    case 'd':
        return KEY_RIGHTARROW;
    case 'w':
        return KEY_UPARROW;
    case 's':
        return KEY_DOWNARROW;
    case ' ':
        return KEY_USE;
    case 'i':
        return KEY_FIRE;
    case 'o':
        return KEY_RSHIFT;
    default:
        return key;
    }
}

static void addKeyToQueue(int pressed, char key)
{
    unsigned short keyData = (pressed << 8) | convertToDoomKey(key);

    s_keyQueue[s_KeyQueueWriteIndex] = keyData;
    s_KeyQueueWriteIndex++;
    s_KeyQueueWriteIndex %= KEYQUEUE_SIZE;
}

void DG_Init()
{
    // tick
    init_ticks_ms = (uint32_t)sys_uptime();

    // window
    wdesc = create_window("DOOM", 0, 0, DOOMGENERIC_RESX + 8, DOOMGENERIC_RESY + 30);
    if (wdesc == NULL)
    {
        printf("Failed to create window\n");
        return;
    }

    if (add_image_to_window(wdesc, DOOMGENERIC_RESX, DOOMGENERIC_RESY, PIXEL_FORMAT_BGRA, (char *)DG_ScreenBuffer) == -1)
    {
        printf("Failed to add image to window\n");
        return;
    }
}

void DG_DrawFrame()
{
    if (wdesc != NULL)
        flush_window(wdesc);

    if (sys_read(FDN_STDIN, &input_key, 1) == -1)
        return;

    if (input_key == '\0')
        return;

    if (input_key != before_input_key && before_input_key != '\0')
        addKeyToQueue(0, before_input_key);

    addKeyToQueue(1, input_key);
    before_input_key = input_key;
}

void DG_SleepMs(uint32_t ms)
{
    uint32_t start = DG_GetTicksMs();
    for (;;)
    {
        if (DG_GetTicksMs() - start >= ms)
            break;
    }
}

uint32_t DG_GetTicksMs()
{
    return (uint32_t)sys_uptime() - init_ticks_ms;
}

int DG_GetKey(int *pressed, unsigned char *doomKey)
{
    if (s_KeyQueueReadIndex == s_KeyQueueWriteIndex)
        return 0;

    unsigned short keyData = s_keyQueue[s_KeyQueueReadIndex];
    s_KeyQueueReadIndex++;
    s_KeyQueueReadIndex %= KEYQUEUE_SIZE;

    *pressed = keyData >> 8;
    *doomKey = keyData & 0xff;

    return 1;
}

void DG_SetWindowTitle(const char *title)
{
}

int main(int argc, char **argv)
{
    doomgeneric_Create(argc, argv);

    while (1)
        doomgeneric_Tick();

    return 0;
}
ticksの取得

DG_GetTicksMsにて、doomgenericが起動してから現在までのミリ秒単位の経過時間=ticksの取得が正常に行われる必要があり、例えばこれが仮実装で、常に0を返す実装にしていると、画面描画やキーボード入力の受け付けが行われず、処理が先に進まなくなります。

ここで利用しているsys_uptimeは自作OSのシステムコールで、OSが起動してから現在までの経過時間をミリ秒単位で返す実装になっています。

DG_Initにてstatic変数init_ticks_mssys_uptimeで取得した値を代入し、これがdoomgenericが起動した時間の基準になり、DG_GetTicksMsにてticksの計算を可能にしています。

画面描画

自作OSに実装されているウィンドウマネージャを利用して、画面描画を行っています。

DG_Initにてウィンドウを生成する関数であるcreate_window、ウィンドウに画像コンポーネントを追加する関数であるadd_image_to_windowを呼び出し、DG_DrawFrameではウィンドウを再描画するための関数であるflush_windowを呼び出しています。これらはウィンドウ関連のラッパー関数であり、それぞれ内部で専用のシステムコールを呼び出しています。

キーボード入力

DG_DrawFrameにてウィンドウが再描画された後にキーボード入力を受け取っています。ユーザーアプリからキーボード入力イベントを直接取得することはできず、代わりに標準入力から1文字受け取り、それをキーボード入力に見立てています。

OSはキーボードが入力され、割り込みが発生すると、どのキーが押されたか、もしくは離されたかを識別するキーコードを受け取ります。そのキーコードが解釈でき次第、非同期で標準入力バッファに、キーボードに対応するASCII文字をpushします。

ユーザーアプリからreadシステムコールsys_readにおいて、FDN_STDIN(=0番)に対してreadすることによって、標準入力バッファから文字を読み出すことができます。

以下OS側のsys_readの実装です。FDN_STDINに対してreadする場合、本来はエンターキーが入力されるまで無限に待機し続けるのですが、今回のように1文字入力した後即復帰してほしいケースのために、バッファサイズが1と指定された場合のみ特別扱いしています(今コードを見返したらバッファサイズが0や負数の場合も特別扱いされてしまっているので後で修正します)。

myos-x86_64/kernel/src/arch/syscall.rs

...

fn sys_read(fd: FileDescriptorNumber, buf_addr: VirtualAddress, buf_len: usize) -> Result<()> {
    match fd {
        FileDescriptorNumber::STDOUT | FileDescriptorNumber::STDERR => {
            return Err(Error::Failed("fd is not defined"));
        }
        FileDescriptorNumber::STDIN => {
            if buf_len > 1 {
                let mut input_s = None;
                while input_s.is_none() {
                    if !console::is_ready_get_line() {
                        super::hlt();
                        continue;
                    }

                    super::disabled_int(|| {
                        input_s = console::get_line()?;
                        Result::Ok(())
                    })?;
                    break;
                }

                let c_s = CString::new(input_s.unwrap())
                    .unwrap()
                    .into_bytes_with_nul();
                buf_addr.copy_from_nonoverlapping(c_s.as_ptr(), buf_len);
            }
            // buf_len == 1
            else {
                let ascii = super::disabled_int(|| console::get_ascii())?;
                buf_addr.copy_from_nonoverlapping(&(ascii as u8), 1);
            }
        }
        fd => {
            let data = vfs::read_file(&fd)?;

            if buf_len < data.len() {
                return Err(Error::Failed("buffer is too small"));
            }

            buf_addr.copy_from_nonoverlapping(data.as_ptr(), data.len());
        }
    }

    Ok(())
}

...

キーボード入力を受け取った後、addKeyToQueueconvertToDoomKeyにて、ゲーム内で使用するキーコードに変換しています。それぞれのキーの対応についてはconvertToDoomKey内の実装で確認できます。

変換されたキーコードはキューに追加され、DG_GetKeyによってキーの判定を行うタイミングで取り出されます。

標準入力からの受け取りでは、入力されたASCII文字の情報しか得られず、Ctrlやファンクションキーなどは入力できません。また、キーが押されたか、もしくは離されたかの識別もできないため、最低限プレイできるようにするための苦肉の策として、次のキーが押されるまで前のキーは押しっぱなし判定になります。そのため、実際のゲームプレイでは、別のキーが押されるまで前後左右移動や攻撃をし続けるため、照準合わせや視点移動が通常よりも難しめになっています(操作に慣れるとそれはそれで結構面白い)。

移植編6 - 画面描画用に新しいシステムコール生やす

今回新しく、ウィンドウに画像コンポーネントを追加するsys_add_image_to_windowと、ウィンドウを再描画するsys_flush_windowの2つのシステムコールをOSに実装しました。

myos-x86_64/kernel/src/arch/syscall.rs

...

fn sys_flush_window(wd: LayerId) -> Result<()> {
    simple_window_manager::flush_window(&wd)
}

fn sys_add_image_to_window(
    wd: LayerId,
    width: usize,
    height: usize,
    pixel_format: PixelFormat,
    framebuf_virt_addr: VirtualAddress,
) -> Result<()> {
    let image = simple_window_manager::components::Image::create_and_push_from_framebuf(
        0,
        0,
        width,
        height,
        framebuf_virt_addr,
        pixel_format,
    )?;
    simple_window_manager::add_component_to_window(&wd, Box::new(image))?;

    Ok(())
}

...

ウィンドウやコンポーネントの管理はsimple_window_managerが担っており、複数レイヤー構造を実現するmulti_layerによって、フレームバッファに描画されます。

myos-x86_64/third-party/doom-for-myos/doomgeneric_myos.c

...
if (add_image_to_window(wdesc, DOOMGENERIC_RESX, DOOMGENERIC_RESY, PIXEL_FORMAT_BGRA, (char *)DG_ScreenBuffer) == -1) {
...

add_image_to_windowでは、引数に画像サイズ、画像のピクセルデータとなるバッファの参照、ピクセルフォーマット(データの並び順がRGBなのか、BGRなのか等)といった情報を渡します。今回使用されるピクセルフォーマットは32ビット(各色8ビット)整数値であり、透明度を表すアルファ値も存在しますが、OS側で半透明なピクセルを描画する機能を実装していないため、アルファ値のみ無視しています。

画面全体の再描画は、タイマー割り込み発生時に定期実行されますが、その際multi_layerは各レイヤーが持っているバッファをフレームバッファに書き込みます。画像コンポーネントは内部にレイヤー用バッファとピクセルデータのバッファの参照を保持しており、画像を更新する際、一度ピクセルデータのバッファの内容を、レイヤー用バッファにコピーする必要があります。OS側はピクセルデータのバッファの中身がいつどのように変更されたのか知る由もないため、ユーザアプリ側から明示的にコピーを行うため、システムコールsys_flush_windowを用意しました。これにより、ウィンドウとそれに所属するコンポーネントの再描画が行われます。

感想

これまでに紹介したもの以外にも、実装の過程で遭遇した様々なバグの話や未実装な機能など、ネタはたくさんありますが、長くなりそうなのでここまでにしておこうと思います。

まだまだOSの設計や実装が雑で、改善が必要な箇所はたくさんあると思いますが、自作OSの世界では、OSのコードを書く人とハードウェアが「世界の神」であるため、どんな実装であろうと、バグがあろうとそれを仕様と言い張ることができます(暴論)。

それはさておき、自作OS開発は自分のモチベーションとの戦いなので、強い執念がない限り、無理して既存のOSの再現実装をする必要はないのでは、というのが経験則から来る持論です。

自作OS、ハードウェアやOSの気持ちが理解できるようになるので楽しいし、就活にも繋がるのでメリットしかない!!みんなもやろう!!!

参考サイト

drewdevault.com www2.katsuster.net chikuwait.hatenablog.com