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のリーディングや移植なども視野にいれたい。
以上。