Zig OS Dev 001
Zigに最近興味を持っている。
と言ってもZigのファンになって3年ほどになるが、なかなか見る時間がなかった。
それとWASMだ。僕はWASMバイナリで動くOSをRiscVでテスト的に作ろうと思っている。
理由はring0で動くWASMバイナリなOSを実装することで昨今のクラウドコンピューティングやコンテナ技術の
無駄なオーバーヘッドを無くすことができると考えている。
それとZigを選んだ理由はいくつかある。
- zigの強力なビルドシステム
- zigでcをコンパイルできる
- メモリ管理システムが魅力的
- 今後のエコシステムを作っていく上でZigは良さそう
それはさておき、まずは boot.s を見ていこう
// boot.s
.option norvc
.section .data
.section .text.init
.global _start
_start:
csrr t0, mhartid
bnez t0, 3f
csrw satp, zero
.option push
.option norelax
la gp, _global_pointer
.option pop
la a0, _bss_start
la a1, _bss_end
bgeu a0, a1, 2f
1:
#if __riscv_xlen == 64
sd zero, (a0)
#elif __riscv_xlen == 32
sw zero, (a0)
#endif
addi a0, a0, 8
bltu a0, a1, 1b
2:
la sp, _stack
li t0, (0b11 << 11) | (1 << 7) | (1 << 3)
csrw mstatus, t0
la t1, kmain
csrw mepc, t1
la t2, trap
csrw mtvec, t2
li t3, (1 << 3) | (1 << 11)
csrw mie, t3
la ra, 4f
mret
3:
4:
wfi
j 4b
OSを自作する時によく見るアセンブリだと思う。
アセンブリはあまり書きたくない言語である。
しかしzigの強力なビルドシステムのおかげで
linkerスクリプトやbootの処理やmemoryレイアウトなどをbuild.zigで指定して実行することで
比較的シンプルでわかりやすくzigのプログラムをビルドすることができる。
build.zigはこんな感じ。
const kernel = b.addExecutable(.{
.root_source_file = .{ .path = "src/kernel.zig" },
.optimize = optimize,
.target = target,
.name = "kernel",
});
kernel.code_model = .medium;
kernel.setLinkerScriptPath(.{ .path = "src/linker.lds" });
// Some of the boot-code changes depending on if we're targeting 32-bit
// or 64-bit, which is why we need the pre-processor to run first.
kernel.addCSourceFiles(&.{"src/boot.S"}, &.{
"-x", "assembler-with-cpp",
});
kernel.addCSourceFiles(&.{"src/mem.S"}, &.{
"-x", "assembler-with-cpp",
});
b.installArtifact(kernel);
linkerスクリプトは基本的なOSを作る時と同じ。
各セクションのメモリ位置をリンカスクリプトで用意。
linker.ldsはこんな感じ。
OUTPUT_ARCH( "riscv" )
ENTRY( _start )
MEMORY
{
ram (wxa) : ORIGIN = 0x80000000, LENGTH = 128M
}
PHDRS
{
text PT_LOAD;
data PT_LOAD;
bss PT_LOAD;
}
SECTIONS
{
.text : {
PROVIDE(_text_start = .);
*(.text.init) *(.text .text.*)
PROVIDE(_text_end = .);
} >ram AT>ram :text
PROVIDE(_global_pointer = .);
.rodata : {
PROVIDE(_rodata_start = .);
*(.rodata .rodata.*)
PROVIDE(_rodata_end = .);
} >ram AT>ram :text
.data : {
. = ALIGN(4096);
PROVIDE(_data_start = .);
*(.sdata .sdata.*) *(.data .data.*)
PROVIDE(_data_end = .);
} >ram AT>ram :data
.bss : {
PROVIDE(_bss_start = .);
*(.sbss .sbss.*) *(.bss .bss.*)
PROVIDE(_bss_end = .);
} >ram AT>ram :bss
PROVIDE(_memory_start = ORIGIN(ram));
PROVIDE(_stack = _bss_end + 0x80000);
PROVIDE(_memory_end = ORIGIN(ram) + LENGTH(ram));
PROVIDE(_heap_start = _stack);
PROVIDE(_heap_size = _memory_end - _heap_start);
}
一通りOSのメモリレイアウトとブートの用意ができたところで、uartを実装しよう。
RiscV-32bitで実装するのでOpenSBIのNS16550に沿ったドライバーの実装をする。
// based on the OpenSBI NS16550 driver.
const std = @import("std");
// the default UART serial device is at 0x10000000 on the QEMU RISC-V virt platform.
const uart_base: usize = 0x10000000;
const UART_RBR_OFFSET = 0; // In: Recieve Buffer Register
const UART_DLL_OFFSET = 0; // Out: Divisor Latch Low
const UART_IER_OFFSET = 1; // I/O: Interrupt Enable Register
const UART_DLM_OFFSET = 1; // Out: Divisor Latch High
const UART_FCR_OFFSET = 2; // Out: FIFO Control Register
const UART_LCR_OFFSET = 3; // Out: Line Control Register
const UART_LSR_OFFSET = 5; // In: Line Status Register
const UART_MDR1_OFFSET = 8; // I/O: Mode Register
const UART_LSR_DR = 0x01; // Receiver data ready
const UART_LSR_THRE = 0x20; // Transmit-hold-register empty
fn write_reg(offset: usize, value: u8) void {
const ptr: *volatile u8 = @ptrFromInt(uart_base + offset);
ptr.* = value;
}
fn read_reg(offset: usize) u8 {
const ptr: *volatile u8 = @ptrFromInt(uart_base + offset);
return ptr.*;
}
pub fn put_char(ch: u8) void {
// Wait for transmission bit to be empty before enqueuing more characters
// to be outputted.
while ((read_reg(UART_LSR_OFFSET) & UART_LSR_THRE) == 0) {}
write_reg(0, ch);
}
pub fn get_char() ?u8 {
// Check that we actually have a character to read, if so then we read it
// and return it.
if (read_reg(UART_LSR_OFFSET) & UART_LSR_DR == 1) {
return read_reg(UART_RBR_OFFSET);
} else {
return null;
}
}
pub fn init() void {
// 1 << 0 = 0000 0001
// 1 << 1 = 0000 0010
// lcr = 0000 0011 #1 if either one is 1
// 1 << 7 = 1000 0000
// lcr | 1 << 7 = 1000 0011
const lcr = (1 << 0) | (1 << 1);
write_reg(UART_LCR_OFFSET, lcr);
write_reg(UART_FCR_OFFSET, (1 << 0));
write_reg(UART_IER_OFFSET, (1 << 0));
write_reg(UART_LCR_OFFSET, lcr | (1 << 7));
const divisor: u16 = 592;
const divisor_least: u8 = divisor & 0xff;
const divisor_most: u8 = divisor >> 8;
write_reg(UART_DLL_OFFSET, divisor_least);
write_reg(UART_DLM_OFFSET, divisor_most);
write_reg(UART_LCR_OFFSET, lcr);
}
const Writer = std.io.Writer(u32, error{}, uart_put_str);
const uart_writer = Writer{ .context = 0 };
fn uart_put_str(_: u32, str: []const u8) !usize {
for (str) |ch| {
put_char(ch);
}
return str.len;
}
pub fn println(comptime fmt: []const u8, args: anytype) void {
uart_writer.print(fmt ++ "\n", args) catch {};
}
あとはエントリポイントである、kernel.zigでuartを初期化すれば良い。
const uart = @import("uart.zig");
const page = @import("page.zig");
// This the trap/exception entrypoint, this will be invoked any time
// we get an exception (e.g if something in the kernel goes wrong) or
// an interrupt gets delivered.
// from boot.S
export fn trap() align(4) callconv(.C) noreturn {
while (true) {}
}
// This is the kernel's entrypoint which will be invoked by the booting
// CPU (aka hart) after the boot code has executed.
export fn kmain() callconv(.C) void {
page.init();
uart.init();
uart.println("Zig is running on barebones RISC-V (rv{})!", .{@bitSizeOf(usize)});
while (true) {
if (uart.get_char()) |value| {
switch (value) {
10, 13 => uart.println("", .{}),
else => uart.println("not implemented", .{}),
}
}
}
}
実行すると、確認できる。
ヒープのメモリアドレスをriscvのobjdumpで確認すると正しいこともわかる。
ひとまず今日は終わりだ。
プロジェクトはここで進めていく。
xv6z