Skip to content

Commit

Permalink
bootloader
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewrk committed Dec 22, 2018
1 parent f8d9964 commit ae7226c
Show file tree
Hide file tree
Showing 10 changed files with 285 additions and 37 deletions.
28 changes: 24 additions & 4 deletions README.md
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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
```
31 changes: 29 additions & 2 deletions build.zig
Expand Up @@ -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(),
Expand All @@ -35,12 +48,26 @@ 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" });
}
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);
}
28 changes: 28 additions & 0 deletions 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 = .;
}
}

28 changes: 28 additions & 0 deletions 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();
}
22 changes: 21 additions & 1 deletion src/debug.zig
Expand Up @@ -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");
Expand Down Expand Up @@ -200,4 +221,3 @@ fn writeStackTrace(stack_trace: *const builtin.StackTrace, dwarf_info: *std.debu
);
}
}

5 changes: 5 additions & 0 deletions src/linker.ld
Expand Up @@ -5,6 +5,9 @@ SECTIONS {

.text : ALIGN(4K) {
KEEP(*(.text.boot))
__end_init = .;
. = 0x100; /* Must match address from bootloader.ld */
KEEP(*(.text.main))
*(.text)
}

Expand Down Expand Up @@ -37,4 +40,6 @@ SECTIONS {
*(.bss)
__bss_end = .;
}

bootloader_main = 0x8800000; /* Must match bootloader linker script */
}
112 changes: 91 additions & 21 deletions src/main.zig
Expand Up @@ -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
Expand All @@ -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));

Expand All @@ -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),
}
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/qemu-gdb.ld
Expand Up @@ -5,6 +5,9 @@ SECTIONS {

.text : ALIGN(4K) {
KEEP(*(.text.boot))
__end_init = .;
. = 0x100; /* Must match address from bootloader.ld */
KEEP(*(.text.main))
*(.text)
}

Expand Down Expand Up @@ -32,4 +35,6 @@ SECTIONS {
__debug_ranges_start = .;
__debug_ranges_end = .;
}

bootloader_main = 0x8800000; /* Must match bootloader linker script */
}

0 comments on commit ae7226c

Please sign in to comment.