ZigでWriting an OS in 1,000 Linesをやる
2023-11-21
自作 OS で学ぶマイクロカーネルの設計と実装(通称エナガ本)の補足資料として公開されているWriting an OS in 1,000 Lines
を、できるだけZig
でやってみることにした。
目次
成果物
repository
は以下。
さいしょに
エナガ本
は一通り読んでいたのだが、機能に対していくつかのOS
の実装を眺める。という構成になっており、個人的には「同じような難易度でRV32
を対象に 0->1 でシンプルなOS
を作る書籍があるといいなあ」と思いながら読んでいたのだが、まさに欲していたものが公開されたので大喜びで実装を開始した。
自分はそのまま写経してしまうと頭に入らないため、異なる言語で書いてみたり、何らかの制約を課して実施することが多い。今回はZig
で書いてみることにした。 概ね問題なかったのだが、virtio
以降はよい解決方法が分からなかったのとモチベーションの問題でpending
としている。これは後述する。
内容に関しては「このbook
を読もう」(個人的には本当に素晴らしい本だと感じた。)以外にないので、Zig
でやったという点に関して差分やハマった箇所を記録しておくものとする。
メモ/記録
build.zig
少し省略しているが、基本的には以下のように書くことで今回のプロジェクトはbuild
できた。 RISC-V
の各feature
のon/off
もわかりやすく、C
との統合も簡単にできる。
ただ、情報がまだ少なかったり、web 上の記事はすでにIF
が変わってしまっていたりで苦労する面もあった。
const std = @import("std");
const Builder = @import("std").build.Builder;
const Step = @import("std").build.Step;
const Target = @import("std").Target;
pub fn build(b: *Builder) void {
// ...omitted
const Feature = @import("std").Target.Cpu.Feature;
const features = Target.riscv.Feature;
var disabled_features = Feature.Set.empty;
var enabled_features = Feature.Set.empty;
disabled_features.addFeature(@intFromEnum(features.a));
disabled_features.addFeature(@intFromEnum(features.d));
disabled_features.addFeature(@intFromEnum(features.e));
disabled_features.addFeature(@intFromEnum(features.f));
disabled_features.addFeature(@intFromEnum(features.c));
enabled_features.addFeature(@intFromEnum(features.m));
const target = std.zig.CrossTarget{ .os_tag = .freestanding, .cpu_arch = .riscv32, .abi = Target.Abi.none, .ofmt = .elf, .cpu_features_sub = disabled_features, .cpu_features_add = enabled_features };
const exe = b.addExecutable(std.Build.ExecutableOptions{ .name = "kernel", .root_source_file = std.build.LazyPath.relative("./src/main.zig") });
exe.target = target;
exe.setLinkerScript(std.build.LazyPath.relative("./kernel.ld"));
exe.addIncludePath(std.build.LazyPath.relative("./src"));
exe.addCSourceFiles(&.{ "src/common.c", "src/kernel.c" }, &.{
"-std=c11",
"-Wall",
});
// ...omitted
b.installArtifact(exe);
}
ただし、この本が進むとuserLand
をビルドしてからkernel
のビルド時に埋める必要があるのだが、これをどのように書くべきなのかは結局分かっていない。 以下のようにoption
でuser
かkernel
かを分けつつkernel
ビルド時にはb.exec
でuser
をビルドしてしまったが正しい方法ではなさそう。
pub fn build(b: *Builder) void {
if (b.args) |args| {
if (std.mem.eql(u8, args[0], "--user")) {
// build userland
}
}
_ = b.exec(&[_][]const u8{
"zig",
"build",
"--",
"--user",
});
// ...omitted
_ = b.exec(&[_][]const u8{
"llvm-objcopy",
"--set-section-flags",
".bss=alloc,contents",
"-O",
"binary",
"zig-out/bin/user",
"user.bin",
});
_ = b.exec(&[_][]const u8{
"llvm-objcopy",
"-Ibinary",
"-Oelf32-littleriscv",
"user.bin",
"user.bin.o",
});
exe.addObjectFile(std.build.LazyPath.relative("./user.bin.o"));
b.installArtifact(exe);
}
asm
たとえば以下のような__asm__
を含んだC
のコードは次のように書くことができた。
- C
__attribute__((section(".text.boot")))
__attribute__((naked))
void boot(void) {
__asm__ __volatile__(
"mv sp, %[stack_top]\n"
"j kernel_main\n"
:
: [stack_top] "r" (__stack_top)
);
}
- Zig
pub export fn boot() callconv(.Naked) void {
_ = asm volatile (
\\ mv sp, %[stack_top]
\\ j kernel_main
:
: [stack_top] "r" (&__stack_top),
);
}
ただし、以下のようにoutput
がある場合にうまくZig
で書けないケースがあった。一部experimental
なケースもありそうだったのでそのようなケースに関してはそのままC
で書いた。
__asm__ __volatile__("ecall"
: "=r"(a0), "=r"(a1)
: "r"(a0), "r"(a1), "r"(a2), "r"(a3), "r"(a4), "r"(a5),
"r"(a6), "r"(a7)
: "memory");
C interop
前述したようにC
で書いてしまってそれをZig
から呼びたいケースがいくつかある。このような場合は以下のようにすることで容易に呼び出すことができた。
const c = @cImport({
@cInclude("kernel.h");
});
fn foo() void {
c.printf("foo!!!\n");
}
Variadic function
以下のようにいくつか、やりようはありそうだが今回のprintf
ではうまくいかず素直にC
で定義して呼ぶことにした。
How to initialize variadic function arguments in Zig?
virtio
16. ディスクの読み書き
は一通り実装し、イメージを掴めたもののうまく動作せず、今回はpending
とした。ただ、これまでまったくイメージがつかなかったvirtio
の理解が進んだのは良かった。
ディスクリプタの配置などは以下に記載がある。
https://docs.oasis-open.org/virtio/virtio/v1.0/cs01/virtio-v1.0-cs01.html
基本的にはpacked
になるわけだが、そこをうまく解決できていない。 例えば上記からZig
で表現しようとすると以下のようになるが、これはbuild
できない。
const virtio_blk_req = packed struct {
type: u32,
reserved: u32,
sector: u64,
data: [512]u8,
status: u8,
};
これについては以下のissue
がある。packed struct
がarray
をもてないという話だ。
Packed structs can't contain arrays
またこのissue
では以下のようなコメントもありC
と同じような構造を表現するのが難しそうであり、一旦pending
にしている。このあたりはもう少し理解が深まったら再着手したい。
from packed struct docs "There is no padding between fields." Does that mean it should give back struct size as a total size of each type? u16+u8 = 3 ? Now it returns struct size = 4 with those types in it. In C struct with packed attribute it would gives 3 bytes as expected.
a zig packed struct is not a c packed struct as has been explained above. a zig packed struct is for bit packing fields into an integer representation. and a u24 would be padded to a u32 which is why you get 4 instead of 3. for 3 you would need to do @bitSizeOf(T) / 8
panic
panic
で少し時間を溶かした。 例えば以下はbuild
できる。
var i: u32 = 0;
pub fn panic() void {}
pub export fn _start() void {
}
が以下のようにするとbuild
できない。
var i: u32 = 0;
pub fn panic() void {}
pub export fn _start() void {
i = i + 1;
}
これには少しハマったが意図した挙動のようだ。
it indicates that the panic function it found does not match the expected, correct signature of a panic function fn([]const u8, ?*builtin.StackTrace, ?usize) noreturn. 😃
Also note that the reason the build succeeds without the i = i + 1 line is that without it, there do not exist any calls to the panic handler, so it is not analyzed.
今回については_panic
とrename
することで対処した。
自作エミュレータ
以前RustでRISC-Vエミュレータを書いてNOMMU Linuxをブラウザで動かした という記事を書いたが、このときはNOMMU
を前提としていた。
virtio
は未実装なのでSV32
を実装し、SBI
部分をうまく差し替えれば自分のエミュレータで動くはずだ。 これは次の目標としたい。
まとめ
まさに欲していたもので非常に楽しくやれた。 Zig
で結構ハマったが少し理解は深まったように思う。まだ不安定な面はあれど、個人的には好きな言語だと感じた。 virtio
についてはどこかで再挑戦しつつ、自作エミュレータで動かすことを目標としたい。 これでxv6
なども理解しやすくなったかもしれないので、xv6
のリーディングや移植なども視野にいれたい。
以上。