Merge pull request #109 from SamTebbs33/feature/stacktrace-on-panic3

Add simple stacktrace logging
This commit is contained in:
Sam Tebbs 2019-12-06 20:38:33 +00:00 committed by GitHub
commit 4b870d3a65
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 477 additions and 14 deletions

View file

@ -50,8 +50,16 @@ pub fn build(b: *Builder) !void {
cp_elf_cmd.step.dependOn(&grub_cmd.step);
cp_elf_cmd.step.dependOn(&exec.step);
const modules_path = try fs.path.join(b.allocator, [_][]const u8{ b.exe_dir, "iso", "modules" });
const mkdir_modules_cmd = b.addSystemCommand([_][]const u8{ "mkdir", "-p", modules_path });
const map_file_path = try fs.path.join(b.allocator, [_][]const u8{ modules_path, "kernel.map" });
const map_file_cmd = b.addSystemCommand([_][]const u8{ "./make_map.sh", elf_path, map_file_path });
map_file_cmd.step.dependOn(&cp_elf_cmd.step);
map_file_cmd.step.dependOn(&mkdir_modules_cmd.step);
const iso_cmd = b.addSystemCommand([_][]const u8{ "grub-mkrescue", "-o", iso_path, iso_dir_path });
iso_cmd.step.dependOn(&cp_elf_cmd.step);
iso_cmd.step.dependOn(&map_file_cmd.step);
b.default_step.dependOn(&iso_cmd.step);
const run_step = b.step("run", "Run with qemu");

View file

@ -3,5 +3,6 @@ set default=0
menuentry "pluto" {
multiboot /boot/pluto.elf
module /modules/kernel.map kernel.map
boot
}

4
make_map.sh Executable file
View file

@ -0,0 +1,4 @@
#!/bin/bash
# Read the symbols from the binary, remove all the unnecessary columns with awk and emit to a map file
readelf -s $1 | grep -F "FUNC" | awk '{$1=$3=$4=$5=$6=$7=""; print $0}' | sort -k 1 > $2
echo "" >> $2

View file

@ -99,7 +99,7 @@ export nakedcc fn start_higher_half() noreturn {
asm volatile (
\\.extern KERNEL_STACK_END
\\mov $KERNEL_STACK_END, %%esp
\\mov %%esp, %%ebp
\\xor %%ebp, %%ebp
);
// Push the bootloader magic number and multiboot header address with virtual offset

View file

@ -331,9 +331,9 @@ pub fn init(mb_info: *multiboot.multiboot_info_t, mem_profile: *const MemProfile
// Map in each boot module
for (mem_profile.boot_modules) |*module| {
const mod_p_struct_start = std.mem.alignBackward(@ptrToInt(module), PAGE_SIZE_4KB);
const mod_p_struct_end = std.mem.alignForward(mod_p_struct_start + @sizeOf(multiboot.multiboot_module_t), PAGE_SIZE_4KB);
mapDir(kernel_directory, mem.physToVirt(mod_p_struct_start), mem.physToVirt(mod_p_struct_end), mod_p_struct_start, mod_p_struct_end, allocator) catch |e| {
const mod_v_struct_start = std.mem.alignBackward(@ptrToInt(module), PAGE_SIZE_4KB);
const mod_v_struct_end = std.mem.alignForward(mod_v_struct_start + @sizeOf(multiboot.multiboot_module_t), PAGE_SIZE_4KB);
mapDir(kernel_directory, mod_v_struct_start, mod_v_struct_end, mem.virtToPhys(mod_v_struct_start), mem.virtToPhys(mod_v_struct_end), allocator) catch |e| {
panic(@errorReturnTrace(), "Failed to map module struct: {}\n", e);
};
const mod_p_start = std.mem.alignBackward(module.mod_start, PAGE_SIZE_4KB);

View file

@ -10,7 +10,8 @@ const vga = @import("vga.zig");
const log = @import("log.zig");
const serial = @import("serial.zig");
const mem = if (is_test) @import(mock_path ++ "mem_mock.zig") else @import("mem.zig");
const panic_root = if (is_test) @import(mock_path ++ "panic_mock.zig").panic else @import("panic.zig").panic;
const panic_root = if (is_test) @import(mock_path ++ "panic_mock.zig") else @import("panic.zig");
const options = @import("build_options");
comptime {
switch (builtin.arch) {
@ -26,14 +27,14 @@ export var KERNEL_ADDR_OFFSET: u32 = if (builtin.is_test) 0xC0000000 else undefi
// Just call the panic function, as this need to be in the root source file
pub fn panic(msg: []const u8, error_return_trace: ?*builtin.StackTrace) noreturn {
@setCold(true);
panic_root(error_return_trace, "{}", msg);
panic_root.panic(error_return_trace, "{}", msg);
}
export fn kmain(mb_info: *multiboot.multiboot_info_t, mb_magic: u32) void {
if (mb_magic == multiboot.MULTIBOOT_BOOTLOADER_MAGIC) {
// Booted with compatible bootloader
serial.init(serial.DEFAULT_BAUDRATE, serial.Port.COM1) catch |e| {
panic_root(@errorReturnTrace(), "Failed to initialise serial: {}", e);
panic_root.panic(@errorReturnTrace(), "Failed to initialise serial: {}", e);
};
if (build_options.rt_test)
log.runtimeTests();
@ -44,10 +45,15 @@ export fn kmain(mb_info: *multiboot.multiboot_info_t, mb_magic: u32) void {
log.logInfo("Init arch " ++ @tagName(builtin.arch) ++ "\n");
arch.init(mb_info, &mem_profile, &fixed_allocator.allocator);
log.logInfo("Arch init done\n");
panic_root.init(&mem_profile, &fixed_allocator.allocator) catch |e| {
panic_root.panic(@errorReturnTrace(), "Failed to initialise panic: {}", e);
};
vga.init();
tty.init();
log.logInfo("Init done\n");
tty.print("Hello Pluto from kernel :)\n");
// The panic runtime tests must run last as they never return
if (options.rt_test) panic_root.runtimeTests();
}
}

View file

@ -29,7 +29,7 @@ extern var KERNEL_PHYSADDR_START: *u32;
extern var KERNEL_ADDR_OFFSET: *u32;
/// The size of the fixed allocator used before the heap is set up. Set to 1MiB.
const FIXED_ALLOC_SIZE = 1024 * 1024;
const FIXED_ALLOC_SIZE: usize = 1024 * 1024;
/// The kernel's virtual address offset. It's assigned in the init function and this file's tests.
/// We can't just use KERNEL_ADDR_OFFSET since using externs in the virtToPhys test is broken in

View file

@ -1,13 +1,449 @@
const std = @import("std");
const builtin = @import("builtin");
const tty = @import("tty.zig");
const arch = @import("arch.zig").internals;
const log = @import("log.zig");
const multiboot = @import("multiboot.zig");
const mem = @import("mem.zig");
const ArrayList = std.ArrayList;
const testing = std.testing;
/// The possible errors from panic code
const PanicError = error{
/// The symbol file is of an invalid format.
/// This could be because it lacks whitespace, a column or required newline characters.
InvalidSymbolFile,
};
/// An entry within a symbol map. Corresponds to one entry in a symbole file
const MapEntry = struct {
/// The address that the entry corresponds to
addr: usize,
/// The name of the function that starts at the address
func_name: []const u8,
};
const SymbolMap = struct {
symbols: ArrayList(MapEntry),
///
/// Initialise an empty symbol map.
///
/// Arguments:
/// IN allocator: *std.mem.Allocator - The allocator to use to initialise the array list.
///
/// Return: SymbolMap
/// The symbol map.
///
pub fn init(allocator: *std.mem.Allocator) SymbolMap {
return SymbolMap{
.symbols = ArrayList(MapEntry).init(allocator),
};
}
///
/// Deinitialise the symbol map, freeing all memory used.
///
pub fn deinit(self: *SymbolMap) void {
self.symbols.deinit();
}
///
/// Add a symbol map entry with a name and address.
///
/// Arguments:
/// IN name: []const u8 - The name of the entry.
/// IN addr: usize - The address for the entry.
///
/// Error: std.mem.Allocator.Error
/// * - See ArrayList.append
///
pub fn add(self: *SymbolMap, name: []const u8, addr: u32) !void {
try self.addEntry(MapEntry{ .addr = addr, .func_name = name });
}
pub fn addEntry(self: *SymbolMap, entry: MapEntry) !void {
try self.symbols.append(entry);
}
///
/// Search for the function name associated with the address.
///
/// Arguments:
/// IN addr: usize - The address to search for.
///
/// Return: ?[]const u8
/// The function name associated with that program address, or null if one wasn't found.
///
pub fn search(self: *const SymbolMap, addr: usize) ?[]const u8 {
if (self.symbols.count() == 0)
return null;
// Find the first element whose address is greater than addr
var previous_name: ?[]const u8 = null;
var it = self.symbols.iterator();
while (it.next()) |entry| {
if (entry.addr > addr)
return previous_name;
previous_name = entry.func_name;
}
return previous_name;
}
};
var symbol_map: ?SymbolMap = null;
///
/// Log a stacktrace address. Logs "(no symbols are available)" if no symbols are available,
/// "?????" if the address wasn't found in the symbol map, else logs the function name.
///
/// Arguments:
/// IN addr: usize - The address to log.
///
fn logTraceAddress(addr: usize) void {
const str = if (symbol_map) |syms| syms.search(addr) orelse "?????" else "(no symbols available)";
log.logError("{x}: {}\n", addr, str);
}
pub fn panic(trace: ?*builtin.StackTrace, comptime format: []const u8, args: ...) noreturn {
@setCold(true);
arch.disableInterrupts();
log.logInfo("KERNEL PANIC\n");
log.logInfo(format, args);
log.logInfo("HALTING\n");
log.logError("Kernel panic: " ++ format ++ "\n", args);
if (trace) |trc| {
var last_addr: u64 = 0;
for (trc.instruction_addresses) |ret_addr| {
if (ret_addr != last_addr) logTraceAddress(ret_addr);
last_addr = ret_addr;
}
} else {
const first_ret_addr = @returnAddress();
var last_addr: u64 = 0;
var it = std.debug.StackIterator.init(first_ret_addr);
while (it.next()) |ret_addr| {
if (ret_addr != last_addr) logTraceAddress(ret_addr);
last_addr = ret_addr;
}
}
arch.haltNoInterrupts();
}
///
/// Parse a hexadecimal address from the pointer up until the end pointer. Must be terminated by a
/// whitespace character.
///
/// Arguments:
/// INOUT ptr: *[*]const u8 - The address at which to start looking, updated after all
/// characters have been consumed.
/// IN end: *const u8 - The end address at which to start looking. A whitespace character must
/// be found before this.
///
/// Return: usize
/// The address parsed.
///
/// Error: PanicError || std.fmt.ParseUnsignedError
/// PanicError.InvalidSymbolFile: A terminating whitespace wasn't found before the end address.
/// std.fmt.ParseUnsignedError: See std.fmt.parseInt
///
fn parseAddr(ptr: *[*]const u8, end: *const u8) !usize {
const addr_start = ptr.*;
ptr.* = try parseNonWhitespace(ptr.*, end);
const len = @ptrToInt(ptr.*) - @ptrToInt(addr_start);
const addr_str = addr_start[0..len];
return try std.fmt.parseInt(usize, addr_str, 16);
}
///
/// Parse a single character. The address given cannot be greater than or equal to the end address
/// given.
///
/// Arguments:
/// IN ptr: [*]const u8 - The address at which to get the character from.
/// IN end: *const u8 - The end address at which to start looking. ptr cannot be greater than or
/// equal to this.
///
/// Return: u8
/// The character parsed.
///
/// Error: PanicError
/// PanicError.InvalidSymbolFile: The address given is greater than or equal to the end address.
///
fn parseChar(ptr: [*]const u8, end: *const u8) PanicError!u8 {
if (@ptrToInt(ptr) >= @ptrToInt(end)) return PanicError.InvalidSymbolFile;
return ptr[0];
}
///
/// Parse until a non-whitespace character. Must be terminated by a non-whitespace character before
/// the end address.
///
/// Arguments:
/// IN ptr: [*]const u8 - The address at which to start looking.
/// IN end: *const u8 - The end address at which to start looking. A non-whitespace character
/// must be found before this.
///
/// Return: [*]const u8
/// ptr plus the number of whitespace characters consumed.
///
/// Error: PanicError
/// PanicError.InvalidSymbolFile: A terminating non-whitespace character wasn't found before the
/// end address.
///
fn parseWhitespace(ptr: [*]const u8, end: *const u8) PanicError![*]const u8 {
var i: u32 = 0;
while (std.fmt.isWhiteSpace(try parseChar(ptr + i, end))) : (i += 1) {}
return ptr + i;
}
///
/// Parse until a whitespace character. Must be terminated by a whitespace character before the end
/// address.
///
/// Arguments:
/// IN ptr: [*]const u8 - The address at which to start looking.
/// IN end: *const u8 - The end address at which to start looking. A whitespace character must
/// be found before this.
///
/// Return: [*]const u8
/// ptr plus the number of non-whitespace characters consumed.
///
/// Error: PanicError
/// PanicError.InvalidSymbolFile: A terminating whitespace character wasn't found before the end
/// address.
///
fn parseNonWhitespace(ptr: [*]const u8, end: *const u8) PanicError![*]const u8 {
var i: u32 = 0;
while (!std.fmt.isWhiteSpace(try parseChar(ptr + i, end))) : (i += 1) {}
return ptr + i;
}
///
/// Parse a name from the pointer up until the end pointer. Must be terminated by a whitespace
/// character.
///
/// Arguments:
/// INOUT ptr: *[*]const u8 - The address at which to start looking, updated after all
/// characters have been consumed.
/// IN end: *const u8 - The end address at which to start looking. A whitespace character must
/// be found before this.
///
/// Return: []const u8
/// The name parsed.
///
/// Error: PanicError
/// PanicError.InvalidSymbolFile: A terminating whitespace wasn't found before the end address.
///
fn parseName(ptr: *[*]const u8, end: *const u8) PanicError![]const u8 {
const name_start = ptr.*;
ptr.* = try parseNonWhitespace(ptr.*, end);
const len = @ptrToInt(ptr.*) - @ptrToInt(name_start);
return name_start[0..len];
}
///
/// Parse a symbol map entry from the pointer up until the end pointer,
/// in the format of '\d+\w+[a-zA-Z0-9]+'. Must be terminated by a whitespace character.
///
/// Arguments:
/// INOUT ptr: *[*]const u8 - The address at which to start looking, updated once after the
/// address has been consumed and once again after the name has been consumed.
/// IN end: *const u8 - The end address at which to start looking. A whitespace character must
/// be found before this.
///
/// Return: MapEntry
/// The entry parsed.
///
/// Error: PanicError || std.fmt.ParseUnsignedError
/// PanicError.InvalidSymbolFile: A terminating whitespace wasn't found before the end address.
/// std.fmt.ParseUnsignedError: See parseAddr.
///
fn parseMapEntry(start: *[*]const u8, end: *const u8) !MapEntry {
var ptr = try parseWhitespace(start.*, end);
defer start.* = ptr;
const addr = try parseAddr(&ptr, end);
ptr = try parseWhitespace(ptr, end);
const name = try parseName(&ptr, end);
return MapEntry{ .addr = addr, .func_name = name };
}
///
/// Initialise the panic subsystem by looking for a boot module called "kernel.map" and loading the
/// symbols from it. Exits early if no such module was found.
///
/// Arguments:
/// IN mem_profile: *const mem.MemProfile - The memory profile from which to get the loaded boot
/// modules.
/// IN allocator: *std.mem.Allocator - The allocator to use to store the symbol map.
///
/// Error: PanicError || std.fmt.ParseUnsignedError
/// PanicError.InvalidSymbolFile: A terminating whitespace wasn't found before the end address.
/// std.fmt.ParseUnsignedError: See parseMapEntry.
///
pub fn init(mem_profile: *const mem.MemProfile, allocator: *std.mem.Allocator) !void {
log.logInfo("Init panic\n");
defer log.logInfo("Done\n");
// Exit if we haven't loaded all debug modules
if (mem_profile.boot_modules.len < 1)
return;
var kmap_start: u32 = 0;
var kmap_end: u32 = 0;
for (mem_profile.boot_modules) |module| {
const mod_start = mem.physToVirt(module.mod_start);
const mod_end = mem.physToVirt(module.mod_end) - 1;
const mod_str_ptr = mem.physToVirt(@intToPtr([*]u8, module.cmdline));
if (std.mem.eql(u8, std.mem.toSlice(u8, mod_str_ptr), "kernel.map")) {
kmap_start = mod_start;
kmap_end = mod_end;
break;
}
}
// Don't try to load the symbols if there was no symbol map file. This is a valid state so just
// exit early
if (kmap_start == 0 or kmap_end == 0)
return;
var syms = SymbolMap.init(allocator);
errdefer syms.deinit();
var file_index = kmap_start;
var kmap_ptr = @intToPtr([*]u8, kmap_start);
while (@ptrToInt(kmap_ptr) < kmap_end - 1) {
const entry = try parseMapEntry(&kmap_ptr, @intToPtr(*const u8, kmap_end));
try syms.addEntry(entry);
}
symbol_map = syms;
}
test "parseChar" {
const str: []const u8 = "plutoisthebest";
const end = @ptrCast(*const u8, str.ptr + 14);
var char = try parseChar(str.ptr, end);
testing.expectEqual(char, 'p');
char = try parseChar(str.ptr + 1, end);
testing.expectEqual(char, 'l');
testing.expectError(PanicError.InvalidSymbolFile, parseChar(str.ptr + 14, end));
}
test "parseWhitespace" {
const str: []const u8 = " a";
const end = @ptrCast(*const u8, str.ptr + 5);
var ptr = try parseWhitespace(str.ptr, end);
testing.expectEqual(@ptrToInt(str.ptr) + 4, @ptrToInt(ptr));
}
test "parseWhitespace fails without a terminating whitespace" {
const str: []const u8 = " ";
const end = @ptrCast(*const u8, str.ptr + 3);
testing.expectError(PanicError.InvalidSymbolFile, parseWhitespace(str.ptr, end));
}
test "parseNonWhitespace" {
const str: []const u8 = "ab ";
const end = @ptrCast(*const u8, str.ptr + 3);
var ptr = try parseNonWhitespace(str.ptr, end);
testing.expectEqual(@ptrToInt(str.ptr) + 2, @ptrToInt(ptr));
}
test "parseNonWhitespace fails without a terminating whitespace" {
const str: []const u8 = "abc";
const end = @ptrCast(*const u8, str.ptr + 3);
testing.expectError(PanicError.InvalidSymbolFile, parseNonWhitespace(str.ptr, end));
}
test "parseAddr" {
const str: []const u8 = "1a2b3c4d ";
const end = @ptrCast(*const u8, str.ptr + 9);
var ptr = str.ptr;
testing.expectEqual(try parseAddr(&ptr, end), 0x1a2b3c4d);
}
test "parseAddr fails without a terminating whitespace" {
const str: []const u8 = "1a2b3c4d";
const end = @ptrCast(*const u8, str.ptr + 9);
var ptr = str.ptr;
testing.expectError(PanicError.InvalidSymbolFile, parseAddr(&ptr, end));
}
test "parseAddr fails with an invalid integer" {
const str: []const u8 = "1g2t ";
const end = @ptrCast(*const u8, str.ptr + 5);
var ptr = str.ptr;
testing.expectError(error.InvalidCharacter, parseAddr(&ptr, end));
}
test "parseName" {
const str: []const u8 = "func_name ";
const end = @ptrCast(*const u8, str.ptr + 10);
var ptr = str.ptr;
testing.expectEqualSlices(u8, try parseName(&ptr, end), "func_name");
}
test "parseName fails without a terminating whitespace" {
const str: []const u8 = "func_name";
const end = @ptrCast(*const u8, str.ptr + 9);
var ptr = str.ptr;
testing.expectError(PanicError.InvalidSymbolFile, parseName(&ptr, end));
}
test "parseMapEntry" {
const str: []const u8 = "1a2b3c4d func_name\n5e6f7a8b func_name2\n";
const end = @ptrCast(*const u8, str.ptr + 39);
var ptr = str.ptr;
var actual = try parseMapEntry(&ptr, end);
var expected = MapEntry{ .addr = 0x1a2b3c4d, .func_name = "func_name" };
testing.expectEqual(actual.addr, expected.addr);
testing.expectEqualSlices(u8, actual.func_name, expected.func_name);
actual = try parseMapEntry(&ptr, end);
expected = MapEntry{ .addr = 0x5e6f7a8b, .func_name = "func_name2" };
testing.expectEqual(actual.addr, expected.addr);
testing.expectEqualSlices(u8, actual.func_name, expected.func_name);
}
test "parseMapEntry fails without a terminating whitespace" {
const str: []const u8 = "1a2b3c4d func_name";
var ptr = str.ptr;
testing.expectError(PanicError.InvalidSymbolFile, parseMapEntry(&ptr, @ptrCast(*const u8, str.ptr + 18)));
}
test "parseMapEntry fails without any characters" {
const str: []const u8 = " ";
var ptr = str.ptr;
testing.expectError(PanicError.InvalidSymbolFile, parseMapEntry(&ptr, @ptrCast(*const u8, str.ptr)));
}
test "parseMapEntry fails with an invalid address" {
const str: []const u8 = "xyz func_name";
var ptr = str.ptr;
testing.expectError(error.InvalidCharacter, parseMapEntry(&ptr, @ptrCast(*const u8, str.ptr + 13)));
}
test "parseMapEntry fails without a name" {
const str: []const u8 = "123 ";
var ptr = str.ptr;
testing.expectError(PanicError.InvalidSymbolFile, parseMapEntry(&ptr, @ptrCast(*const u8, str.ptr + 4)));
}
test "SymbolMap" {
var allocator = std.heap.direct_allocator;
var map = SymbolMap.init(allocator);
try map.add("abc"[0..], 123);
try map.addEntry(MapEntry{ .func_name = "def"[0..], .addr = 456 });
try map.add("ghi"[0..], 789);
try map.addEntry(MapEntry{ .func_name = "jkl"[0..], .addr = 1010 });
testing.expectEqual(map.search(54), null);
testing.expectEqual(map.search(122), null);
testing.expectEqual(map.search(123), "abc");
testing.expectEqual(map.search(234), "abc");
testing.expectEqual(map.search(455), "abc");
testing.expectEqual(map.search(456), "def");
testing.expectEqual(map.search(678), "def");
testing.expectEqual(map.search(788), "def");
testing.expectEqual(map.search(789), "ghi");
testing.expectEqual(map.search(1009), "ghi");
testing.expectEqual(map.search(1010), "jkl");
testing.expectEqual(map.search(2345), "jkl");
}
pub fn runtimeTests() void {
var x: u8 = 255;
x += 1;
}

View file

@ -1,7 +1,13 @@
const builtin = @import("builtin");
const std = @import("std");
const MemProfile = @import("mem_mock.zig").MemProfile;
pub fn panic(trace: ?*builtin.StackTrace, comptime format: []const u8, args: ...) noreturn {
@setCold(true);
std.debug.panic(format, args);
}
pub fn init(mem_profile: *const MemProfile, allocator: *std.mem.Allocator) !void {
// This is never run so just return an arbitrary error to satisfy the compiler
return error.NotNeeded;
}

View file

@ -42,11 +42,13 @@ def get_pre_archinit_cases():
def get_post_archinit_cases():
return [
TestCase("Arch init finishes", [r"Arch init done"]),
TestCase("Panic init", [r"Init panic", r"Done"]),
TestCase("VGA init", [r"Init vga", r"Done"]),
TestCase("VGA tests", [r"VGA: Tested max scan line", r"VGA: Tested cursor shape", r"VGA: Tested updating cursor"]),
TestCase("TTY init", [r"Init tty", r"Done"]),
TestCase("TTY tests", [r"TTY: Tested globals", r"TTY: Tested printing"]),
TestCase("Init finishes", [r"Init done"])
TestCase("Init finishes", [r"Init done"]),
TestCase("Panic tests", [r"Kernel panic: integer overflow", r"c[a-z\d]+: panic", r"c[a-z\d]+: panic.runtimeTests", r"c[a-z\d]+: kmain", r"c[a-z\d]+: start_higher_half"], "\[ERROR\] ")
]
def read_messages(proc):