この記事は自作OS Advent Calendar 2025 25日目の記事です。
前回のアドベントカレンダーの記事はこちら。自作OSにDOOMを移植した話です。
zebian.hatenablog.com
また、開発している自作OSのリポジトリはこちらです。
github.com
libcを自作する
DOOMの移植に利用したdoomgenericはC言語で書かれているため、stdio.hやstring.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 argcとchar const* argv[]であるため、それ以外の型で引数を受け取ろうとすると、voidは大丈夫ですが、実行時にスタックやレジスタが壊れてクラッシュするかもしれません。
ちなみに引数をスタック上に用意するのはカーネルの仕事であるため、カーネル側のコードを変更することで型を変えることもできます。
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() {
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()
.generate()
.expect("Failed to generate bindings");
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-rsをkernelから参照することで、libcで定義した構造体の型定義がカーネルから使えるようになりますが、libc-rsを参照してRustでユーザーアプリを作ることもできます。Rustでユーザーアプリを作る場合はno_std(標準ライブラリを使わない)環境にしないといけませんが、その場合VecやStringといった動的メモリ確保が必要な機能や、パニック時に必要なハンドラが利用できなくなります。
そのため、no_std環境では動的メモリ確保を行うためのglobal_allocatorとパニックハンドラpanic_handlerを実装する必要があります(ちなみにno_std環境でコンパイルするためにはpanic_handlerの実装は必須です)。
しかし、Rustでユーザーアプリを作るたびに毎回実装しなければならないとなると非常に面倒くさいので、libc-rs側で実装してしまえという話です。
global_allocatorやpanic_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"));
#[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"))]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("{:?}", info.message());
println!("{:?}", info.location());
unsafe {
exit(-1);
}
}
これら以外にも、引数を受け取る処理やprint!/println!マクロの定義など、no_std環境では様々な手続きが必要であるため、それらもまとめて実装してしまいます。
#[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
}};
}
#[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 {
}
unsafe {
GENERATION += 1;
}
compute_next_generation();
unsafe {
draw_board(&mut eg_fb, GENERATION);
}
}
}

featureで使い分ける
Rustにはfeatureという機能があり、特定の機能の有効/無効を切り替えるフラグを付けることができます。
doc.rust-lang.org
libc-rsはRust製ユーザーアプリでもカーネルでも参照されることになるのですが、global_allocatorやpanic_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.tomlでlibc-rsにkernelフラグを指定します。
apps/libc-rs/src/lib.rs
#[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の面倒くさい部分はバインディングライブラリで隠してしまおう。