Panic Implementation
In this chapter, we will implement Zig's @panic()
function, which is one of Zig's builtin functions. As we proceed with the implementation of the Ymir VMM, panics will occur frequently. To make debugging easier, we will create a panic handler that displays a stack trace and clearly shows the error details whenever a panic happens.
note
You can safely skip this entire chapter.
important
The source code for this chapter is in whiz-ymir-panic
branch.
Table of Contents
Default Panic
Zig's @panic() is called when the program encounters an unrecoverable error and terminates execution. @panic()
simply invokes a registered panic handler, but the implementation of this handler is platform-dependent. On a typical OS, it displays the provided message along with a stack trace. However, in a .freestanding
environment, the default panic handler just calls @trap()
and does not even display the specified message.
This is inconvenient, really. Ymir implements its own panic handler. This handler first outputs the specified message over the serial port and also displays a stack trace. Instead of terminating Ymir, it enters an infinite loop. This allows us attaching GDB for debugging, enabling more detailed investigation.
Printing Message
Zig's panic handler takes three arguments. The first is the message to output. Unlike logging functions, it cannot output formatted strings, so this is the only message argument. The second and third relate to stack trace information, but in .freestanding
environments, these arguments are always null
. This is not an issue since the stack trace can be obtained manually using registers. Below, we define the panic handler:
var panicked = false;
fn panic(msg: []const u8, _: ?*builtin.StackTrace, _: ?usize) noreturn {
@setCold(true);
arch.disableIntr();
log.err("{s}", .{msg});
if (panicked) {
log.err("Double panic detected. Halting.", .{});
ymir.endlessHalt();
}
panicked = true;
... // スタックトレースの表示
ymir.endlessHalt();
}
@setCold()
indicates that this function (or branch) is rarely called. For some reason, Zig’s documentation does not include a description of this builtin function. It is likely similar to @branchHint(). I believe it serves as a hint to the compiler for optimization purposes, but that is just an assumption.
Within the panic handler, interrupts are disabled to prevent additional panics that could be triggered if an interrupt occurs during panic handling. For message output, we use std.log
as in other files. Since the logging functions are already implemented to use the serial port, there is no need to implement a separate serial output specifically for the panic handler.
Even with interrupts disabled, there is still a possibility of a panic occurring within the panic handler itself. To handle this, we define a global variable called panicked
. When the panic handler is invoked for the first time, this flag is set. If the handler is called again while this flag is already set, it simply halts immediately without doing anything.
At the end of the panic handler, we enter an infinite HLT loop. Since interrupts are disabled, there is no way to exit this HLT loop:
pub fn endlessHalt() noreturn {
arch.disableIntr();
while (true) arch.halt();
}
Stack Trace
Next, we display the stack trace. The stack trace can be obtained by sequentially following the values of RSP and RBP. Zig provides a utility struct called StackIterator
for retrieving stack traces, so we will use it here:
pub const panic_fn = panic;
fn panic(msg: []const u8, _: ?*builtin.StackTrace, _: ?usize) noreturn {
...
var it = std.debug.StackIterator.init(@returnAddress(), null);
var ix: usize = 0;
log.err("=== Stack Trace ==============", .{});
while (it.next()) |frame| : (ix += 1) {
log.err("#{d:0>2}: 0x{X:0>16}", .{ ix, frame });
}
...
}
Ideally, the stack trace would also show file names, function names, and line numbers, but Ymir does not implement this. Zig provides a library called std.dwarf
that can handle DWARF debug information, so those interested in implementing this feature might find it useful. Note that in such cases, you would need to load Ymir's ELF file into memory and pass it to Ymir itself.
Overriding Default Handler
To override Zig's default panic handler, define a panic()
function in the root source file:
pub const panic = ymir.panic.panic_fn;
Now, let's actually trigger a panic. It is recommended to keep the optimization level Debug
. In ReleaseFast
build, optimizations are aggressive, which may cause functions to be inlined and prevent the stack trace from being properly output.
@panic("fugafuga");
The output will look like the following:
[ERROR] panic | fugafuga
[ERROR] panic | === Stack Trace ==============
[ERROR] panic | #00: 0xFFFFFFFF80100D3E
[ERROR] panic | #01: 0xFFFFFFFF80103590
You can see that the stack trace is properly output. Since there is no debug information, line numbers in the source files are not displayed, but these can be obtained using the addr2line
command:
> addr2line -e ./zig-out/bin/ymir.elf 0xFFFFFFFF80100D3E
/home/lysithea/ymir/ymir/main.zig:95
Summary
In this chapter, we implemented Zig's builtin function @panic()
. The panic handler displays the specified message, then outputs a stack trace before entering a HLT loop. As a result, this panic handler will be triggered not only when you explicitly call @panic()
, but also in cases like alignment mismatches or overflows in Debug
build. This will make debugging much easier.