diff --git a/README.md b/README.md index b41e55e..06a7300 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,23 @@ zig build qemu -Dgdb In another terminal: ``` -gdb zig-cache/clashos-dbg -ex 'run target remote localhost:1234' +gdb zig-cache/clashos-dbg -ex 'target remote localhost:1234' ``` -Note: this crashes GDB for me, but it works if I remove the `-ex` -parameter and execute the command at the prompt. +### Sending a New Kernel Image via Serial + +While the Raspberry Pi is running, you can use + +``` +zig build upload -Dtty=/dev/ttyUSB0 +``` + +If using QEMU, use `zig build qemu -Dpty` and note the tty path. +In another terminal window, `cat` the tty path. +In yet another terminal window, you can use the `zig build upload` +command above, with the tty path provided by QEMU. +This is compatible with using GDB with QEMU, just make sure to pass +the `-Dgdb` to both `zig build` commands. ### Actual Hardware @@ -47,7 +59,6 @@ For further changes repeat steps 3 and 4. ## Roadmap - * Ability to send a new kernel image via UART * Interface with the file system * Get rid of dependency on binutils objcopy * Interface with the video driver @@ -71,3 +82,12 @@ Where `/dev/ttyUSB0` is the device that represents the serial-to-USB cable: ``` sudo screen /dev/ttyUSB0 115200 cs8 ``` + +### Memory Layout + +``` +0x0000000 ( 0 MiB) - boot entry point +0x0000100 - kernel_main function +0x8000000 (128 MiB) - top of kernel stack, and bootloader_main function +0x8800000 (136 MiB) - top of bootloader stack +``` diff --git a/build.zig b/build.zig index 90baa4c..e30a363 100644 --- a/build.zig +++ b/build.zig @@ -5,13 +5,26 @@ const builtin = @import("builtin"); pub fn build(b: *Builder) !void { const mode = b.standardReleaseOptions(); const want_gdb = b.option(bool, "gdb", "Build for using gdb with qemu") orelse false; + const want_pty = b.option(bool, "pty", "Create a separate TTY path") orelse false; + + const arch = builtin.Arch.aarch64v8; + const environ = builtin.Environ.eabihf; + + // First we build just the bootloader executable, and then we build the actual kernel + // which uses @embedFile on the bootloader. + const bootloader = b.addStaticExecutable("bootloader", "src/bootloader.zig"); + bootloader.setLinkerScriptPath("src/bootloader.ld"); + bootloader.setBuildMode(builtin.Mode.ReleaseSmall); + bootloader.setTarget(arch, builtin.Os.freestanding, environ); const exec_name = if (want_gdb) "clashos-dbg" else "clashos"; const exe = b.addStaticExecutable(exec_name, "src/main.zig"); exe.setBuildMode(mode); - exe.setTarget(builtin.Arch.aarch64v8, builtin.Os.freestanding, builtin.Environ.eabihf); + exe.setTarget(arch, builtin.Os.freestanding, environ); const linker_script = if (want_gdb) "src/qemu-gdb.ld" else "src/linker.ld"; exe.setLinkerScriptPath(linker_script); + exe.addBuildOption([]const u8, "bootloader_exe_path", b.fmt("\"{}\"", bootloader.getOutputPath())); + exe.step.dependOn(&bootloader.step); const run_objcopy = b.addCommand(null, b.env_map, [][]const u8{ "objcopy", exe.getOutputPath(), @@ -35,7 +48,7 @@ pub fn build(b: *Builder) !void { "-serial", "null", "-serial", - "stdio", + if (want_pty) "pty" else "stdio", }); if (want_gdb) { try qemu_args.appendSlice([][]const u8{ "-S", "-s" }); @@ -43,4 +56,18 @@ pub fn build(b: *Builder) !void { const run_qemu = b.addCommand(null, b.env_map, qemu_args.toSliceConst()); qemu.dependOn(&run_qemu.step); run_qemu.step.dependOn(&exe.step); + + const send_image_tool = b.addExecutable("send_image", "tools/send_image.zig"); + + var send_img_args = std.ArrayList([]const u8).init(b.allocator); + try send_img_args.append(send_image_tool.getOutputPath()); + if (b.option([]const u8, "tty", "Specify the TTY to send images to")) |tty_path| { + try send_img_args.append(tty_path); + } + const run_send_image_tool = b.addCommand(null, b.env_map, send_img_args.toSliceConst()); + run_send_image_tool.step.dependOn(&send_image_tool.step); + + const upload = b.step("upload", "Send a new kernel image to a running instance. (See -Dtty option)"); + upload.dependOn(&run_objcopy.step); + upload.dependOn(&run_send_image_tool.step); } diff --git a/src/bootloader.ld b/src/bootloader.ld new file mode 100644 index 0000000..de4bf62 --- /dev/null +++ b/src/bootloader.ld @@ -0,0 +1,28 @@ +ENTRY(boot) + +SECTIONS { + kernel_main = 0x100; /* Must match value from kernel linker script */ + + . = 0x8800000; /* Must match debug.bootloader_address */ + + .text : ALIGN(4K) { + KEEP(*(.text.first)) + *(.text) + } + + .rodata : ALIGN(4K) { + *(.rodata) + } + + .data : ALIGN(4K) { + *(.data) + } + + .bss : ALIGN(4K) { + __bss_start = .; + *(COMMON) + *(.bss) + __bss_end = .; + } +} + diff --git a/src/bootloader.zig b/src/bootloader.zig new file mode 100644 index 0000000..b93401d --- /dev/null +++ b/src/bootloader.zig @@ -0,0 +1,28 @@ +// The bootloader is a separate executable, the hardware does not boot +// directly into it. When the kernel wants to load a new image from the +// serial port, it copies the bootloader executable code into memory at +// address bootloader_address which matches the linker script for the bootloader +// executable. Then the kernel jumps to bootloader_address. The bootloader then +// overwrites the kernel's code, which is why a separate bootloader +// executable is necessary. +const std = @import("std"); +const builtin = @import("builtin"); +const debug = @import("debug.zig"); +const serial = @import("serial.zig"); + +export fn bootloader_main(start_addr: [*]u8, len: usize) linksection(".text.first") noreturn { + var i: usize = 0; + while (i < len) : (i += 1) { + start_addr[i] = serial.readByte(); + } + asm volatile ( + \\mov sp,#0x08000000 + \\bl kernel_main + ); + unreachable; +} + +pub fn panic(message: []const u8, stack_trace: ?*builtin.StackTrace) noreturn { + serial.log("BOOTLOADER PANIC: {}\n", message); + debug.wfe_hang(); +} diff --git a/src/debug.zig b/src/debug.zig index 8d370bc..933a19b 100644 --- a/src/debug.zig +++ b/src/debug.zig @@ -19,8 +19,29 @@ const source_files = [][]const u8{ "src/main.zig", "src/mmio.zig", "src/serial.zig", + "src/bootloader.zig", }; +var already_panicking: bool = false; + +pub fn panic(stack_trace: ?*builtin.StackTrace, comptime fmt: []const u8, args: ...) noreturn { + @setCold(true); + if (already_panicking) { + serial.write("\npanicked during kernel panic\n"); + wfe_hang(); + } + already_panicking = true; + + serial.log(fmt ++ "\n", args); + + const first_trace_addr = @ptrToInt(@returnAddress()); + if (stack_trace) |t| { + dumpStackTrace(t); + } + dumpCurrentStackTrace(first_trace_addr); + wfe_hang(); +} + pub fn wfe_hang() noreturn { while (true) { asm volatile ("wfe"); @@ -200,4 +221,3 @@ fn writeStackTrace(stack_trace: *const builtin.StackTrace, dwarf_info: *std.debu ); } } - diff --git a/src/linker.ld b/src/linker.ld index 4157577..3a3825b 100644 --- a/src/linker.ld +++ b/src/linker.ld @@ -5,6 +5,9 @@ SECTIONS { .text : ALIGN(4K) { KEEP(*(.text.boot)) + __end_init = .; + . = 0x100; /* Must match address from bootloader.ld */ + KEEP(*(.text.main)) *(.text) } @@ -37,4 +40,6 @@ SECTIONS { *(.bss) __bss_end = .; } + + bootloader_main = 0x8800000; /* Must match bootloader linker script */ } diff --git a/src/main.zig b/src/main.zig index 81086d2..8569c9d 100644 --- a/src/main.zig +++ b/src/main.zig @@ -11,9 +11,11 @@ const debug = @import("debug.zig"); // could alias any uninitialized global variable in the kernel. extern var __bss_start: u8; extern var __bss_end: u8; +extern var __end_init: u8; comptime { // .text.boot to keep this in the first portion of the binary + // Note: this code cannot be changed via the bootloader. asm volatile ( \\.section .text.boot \\.globl _start @@ -32,29 +34,11 @@ comptime { ); } -var already_panicking: bool = false; - pub fn panic(message: []const u8, stack_trace: ?*builtin.StackTrace) noreturn { - @setCold(true); - if (already_panicking) { - serial.write("\npanicked during kernel panic\n"); - debug.wfe_hang(); - } - already_panicking = true; - - serial.write("\n!KERNEL PANIC!\n"); - serial.write(message); - serial.write("\n"); - - const first_trace_addr = @ptrToInt(@returnAddress()); - if (stack_trace) |t| { - debug.dumpStackTrace(t); - } - debug.dumpCurrentStackTrace(first_trace_addr); - debug.wfe_hang(); + debug.panic(stack_trace, "KERNEL PANIC: {}", message); } -export fn kernel_main() noreturn { +export fn kernel_main() linksection(".text.main") noreturn { // clear .bss @memset((*volatile [1]u8)(&__bss_start), 0, @ptrToInt(&__bss_end) - @ptrToInt(&__bss_start)); @@ -73,8 +57,94 @@ export fn kernel_main() noreturn { //fb_clear(&color_blue); + serialLoop(); +} + +const build_options = @import("build_options"); +const bootloader_code align(@alignOf(std.elf.Elf64_Ehdr)) = @embedFile("../" ++ build_options.bootloader_exe_path); + +fn serialLoop() noreturn { + const boot_magic = []u8{ 6, 6, 6 }; + var boot_magic_index: usize = 0; while (true) { - serial.putc(serial.getc()); + const byte = serial.readByte(); + if (byte == boot_magic[boot_magic_index]) { + boot_magic_index += 1; + if (boot_magic_index != boot_magic.len) + continue; + + // It's time to receive the new kernel. First + // we skip over the .text.boot bytes, verifying that they + // are unchanged. + const new_kernel_len = serial.in.readIntLittle(u32) catch unreachable; + serial.log("New kernel image detected, {Bi2}\n", new_kernel_len); + const text_boot = @intToPtr([*]const u8, 0)[0..@ptrToInt(&__end_init)]; + for (text_boot) |text_boot_byte, byte_index| { + const new_byte = serial.readByte(); + if (new_byte != text_boot_byte) { + debug.panic( + @errorReturnTrace(), + "new_kernel[{}] expected: 0x{x} actual: 0x{x}", + byte_index, + text_boot_byte, + new_byte, + ); + } + } + const start_addr = @ptrToInt(kernel_main); + const bytes_left = new_kernel_len - start_addr; + var pad = start_addr - text_boot.len; + while (pad > 0) : (pad -= 1) { + _ = serial.readByte(); + } + + // Next we copy the bootloader code to the correct memory address, + // and then jump to it. + // Read the ELF + var workaround = ([*]const u8)(&bootloader_code); // TODO remove this + const ehdr = @ptrCast(*const std.elf.Elf64_Ehdr, workaround); + var phdr_addr = workaround + ehdr.e_phoff; + var phdr_i: usize = 0; + while (phdr_i < ehdr.e_phnum) : ({ + phdr_i += 1; + phdr_addr += ehdr.e_phentsize; + }) { + const this_ph = @ptrCast(*const std.elf.Elf64_Phdr, phdr_addr); + switch (this_ph.p_type) { + std.elf.PT_LOAD => { + const src_ptr = workaround + this_ph.p_offset; + const src_len = this_ph.p_filesz; + const dest_ptr = @intToPtr([*]u8, this_ph.p_vaddr); + const dest_len = this_ph.p_memsz; + const pad_len = dest_len - src_len; + const copy_len = dest_len - pad_len; + @memcpy(dest_ptr, src_ptr, copy_len); + @memset(dest_ptr + copy_len, 0, pad_len); + }, + std.elf.PT_GNU_STACK => {}, // ignore + else => debug.panic( + @errorReturnTrace(), + "unexpected ELF Program Header load type: {}", + this_ph.p_type, + ), + } + } + serial.log("Loading new image...\n"); + asm volatile ( + \\mov sp,#0x08000000 + \\bl bootloader_main + : + : [arg0] "{x0}" (start_addr), + [arg1] "{x1}" (bytes_left) + ); + unreachable; + } + switch (byte) { + '\r' => { + serial.writeText("\n"); + }, + else => serial.writeByte(byte), + } } } diff --git a/src/qemu-gdb.ld b/src/qemu-gdb.ld index 3d9bc2e..a4dd543 100644 --- a/src/qemu-gdb.ld +++ b/src/qemu-gdb.ld @@ -5,6 +5,9 @@ SECTIONS { .text : ALIGN(4K) { KEEP(*(.text.boot)) + __end_init = .; + . = 0x100; /* Must match address from bootloader.ld */ + KEEP(*(.text.main)) *(.text) } @@ -32,4 +35,6 @@ SECTIONS { __debug_ranges_start = .; __debug_ranges_end = .; } + + bootloader_main = 0x8800000; /* Must match bootloader linker script */ } diff --git a/src/serial.zig b/src/serial.zig index b7194f6..932dcad 100644 --- a/src/serial.zig +++ b/src/serial.zig @@ -1,4 +1,5 @@ -const fmt = @import("std").fmt; +const std = @import("std"); +const fmt = std.fmt; const mmio = @import("mmio.zig"); pub const GPFSEL1 = 0x3F200004; @@ -20,13 +21,36 @@ pub const AUX_MU_CNTL_REG = 0x3F215060; pub const AUX_MU_STAT_REG = 0x3F215064; pub const AUX_MU_BAUD_REG = 0x3F215068; -pub fn putc(byte: u8) void { +pub const in = &in_stream_state; +pub const out = &out_stream_state; + +const NoError = error{}; + +var in_stream_state = std.io.InStream(NoError){ .readFn = struct { + fn readFn(self: *std.io.InStream(NoError), buffer: []u8) NoError!usize { + for (buffer) |*byte| { + byte.* = readByte(); + } + return buffer.len; + } +}.readFn }; + +var out_stream_state = std.io.OutStream(NoError){ .writeFn = struct { + fn writeFn(self: *std.io.OutStream(NoError), bytes: []const u8) NoError!void { + for (bytes) |byte| { + writeByte(byte); + } + return buffer.len; + } +}.writeFn }; + +pub fn writeByte(byte: u8) void { // Wait for UART to become ready to transmit. while ((mmio.read(AUX_MU_LSR_REG) & 0x20) == 0) {} mmio.write(AUX_MU_IO_REG, byte); } -pub fn getc() u8 { +pub fn readByte() u8 { // Wait for UART to have recieved something. while ((mmio.read(AUX_MU_LSR_REG) & 0x01) == 0) {} return @truncate(u8, mmio.read(AUX_MU_IO_REG)); @@ -34,7 +58,7 @@ pub fn getc() u8 { pub fn write(buffer: []const u8) void { for (buffer) |c| - putc(c); + writeByte(c); } /// Translates \n into \r\n @@ -42,10 +66,10 @@ pub fn writeText(buffer: []const u8) void { for (buffer) |c| { switch (c) { '\n' => { - putc('\r'); - putc('\n'); + writeByte('\r'); + writeByte('\n'); }, - else => putc(c), + else => writeByte(c), } } } @@ -74,8 +98,6 @@ pub fn init() void { mmio.write(AUX_MU_CNTL_REG, 3); } -const NoError = error{}; - pub fn log(comptime format: []const u8, args: ...) void { fmt.format({}, NoError, logBytes, format, args) catch |e| switch (e) {}; } diff --git a/tools/send_image.zig b/tools/send_image.zig new file mode 100644 index 0000000..e0da38e --- /dev/null +++ b/tools/send_image.zig @@ -0,0 +1,23 @@ +const std = @import("std"); + +pub fn main() !void { + var direct_allocator = std.heap.DirectAllocator.init(); + defer direct_allocator.deinit(); + + var arena = std.heap.ArenaAllocator.init(&direct_allocator.allocator); + defer arena.deinit(); + + const allocator = &arena.allocator; + const args = try std.os.argsAlloc(allocator); + if (args.len != 2) { + std.debug.panic("expected 2 args, found {} args", args.len); + } + const file_path = args[1]; + const new_image_data = try std.io.readFileAlloc(allocator, "clashos.bin"); + const tty_fd = try std.os.posixOpen(file_path, std.os.posix.O_WRONLY, 0); + const tty_file = std.os.File.openHandle(tty_fd); + const out = &tty_file.outStream().stream; + try out.write([]u8{ 6, 6, 6 }); + try out.writeIntLittle(u32, @intCast(u32, new_image_data.len)); + try out.write(new_image_data); +}