使用Zig提供裸机上内核死机的堆栈跟踪

2020-06-28 06:19:58

上周,我作为一名程序员的职业生涯达到了一个令人兴奋的里程碑。这是我有生以来第一次直接在硬件上运行代码,裸机和我的代码之间没有操作系统。

在某些情况下,这个副项目的目标是创建一个2-4人的街机游戏,直接在树莓派3+上运行。

在刚刚运行Hello World之后,该软件在启动时不仅可以通过串行UART发送消息:

嗯,在编写所有这些代码时,我不可避免地会遇到错误和崩溃。当这种情况发生时,我想迅速了解这是什么意思。通常,在Zig中,当出现问题时,您会得到一个很好的堆栈跟踪,如下所示:

然而,这个示例针对的是Linux,而这个街机游戏项目是独立的。独立式中的等效代码实际上只是挂起了CPU:

-a/src/main.zig+b/src/main.zig seral.log(";Hello World!ClashOS 0.0\n";);+var x:u8=255;+x+=1;+seral.log(";get here\n";);

运行时,您将看到我们从未收到收到的消息,但我们也不会打印任何类型的错误消息或任何内容。

要理解这一点,我们可以查看默认的死机处理程序。在Zig中,您可以在根源文件中提供pub Fn死机。但如果您不提供此功能,则使用默认值:

Pub FN死机(msg:[]const U8,error_return_trace:?*builtin.StackTrace)noreturn{@setLD(True);switch(builtin.os){builtin.Os.freestanding=>;{while(True){}},Else=>;{const first_trace_addr=@ptrToInt(@rereturn Address());std.debug.panicExtra(Error_Return_){},{const first_trace_addr=@ptrToInt(@rereturn Address());std.debug.panicExtra(error_return_。

在这里,我们可以看到为什么前面的代码挂起-独立目标的默认死机处理程序是简单的while(True){}。

pub FN死机(message:[]const U8,STACK_TRACE:?*builtin.StackTrace)noreturn{seral.write(";\n!内核死机!\n";);seral.write(Message);seral.write(";\n";);While(True){}}。

这已经好多了。我们可以看到整数溢出导致了内核死机,但是在任何地方都可能发生整数溢出。打印完整的堆叠轨迹不是很好吗?

是的,是的,会的。要做到这一点,我需要做的第一件事就是从内核内部访问DWARF调试信息。但是我甚至没有文件系统。这怎么行得通呢?

放轻松!只需将矮人信息直接放入内核的内存中即可。我修改了我的链接器脚本来实现这一点:

.rodata:ALIGN(4K){*(.rodata)__DEBUG_INFO_START=.;KEEP(*(.debug_info))__DEBUG_INFO_END=.;__DEBUG_ABBRV_START=.;KEEP(*(.debug_abbrev))__DEBUG_ABBRV_END=.;__DEBUG_STR_STR=.;KEEP(*(.debug_str))__DEBUG_STR_。Keep(*(.debug_line))__debug_line_end=.;__debug_range_start=.;Keep(*(.debug_range))__debug_range_end=.;}。

lld:错误:.rodata>;>;>;/home/andy/dev/clashos/zig-cache/clashos.o:(.debug_info):0x0>;>;>;输出节的节标志不兼容.rodata:0x12lld:错误:.rodata>;>;>;内部>;:(.debug_str):0x30>;>;>;输出节.rodata:0x12lld:错误。/home/andy/dev/clashos/zig-cache/clashos.o:(.debug_line):0x0>;>;>;输出节.rodata:0x32

在对一份错误报告进行了反复讨论后,乔治·里马尔(George Rimar)建议干脆删除那项特定的检查,因为这可能是一种过于严格的执行。我在我的LLD叉子里试过了,它起作用了!调试信息现在已链接到我的内核映像中。在完成了这篇博客文章中的其余步骤后,我向上游提交了一个补丁,Rui已经将其合并到了LLD中。这将与LLVM8一起发布,同时Zig的LLD fork也有补丁。

在这一点上,编写我的内核和Zig标准库的堆栈跟踪工具之间的粘合代码是一件简单的事情。在Zig中,您不必刻意支持独立模式。不依赖于特定操作系统的代码将在独立模式下工作,这要归功于Zig的懒惰的顶层声明分析。由于标准库堆栈跟踪代码不调用任何操作系统API,因此它支持独立模式。

用于从ELF文件打开调试信息的Zig STD lib API如下所示:

但是这个内核非常简单,它甚至不在ELF文件中。它直接从二进制文件启动。我们只是将调试信息部分直接映射到内存中。为此,我们可以使用较低级别的API:/Initialize DWARF INFO。调用者有责任在调用之前初始化大多数/DwarfInfo字段。这些字段可以保持未定义状态:/*abbrev_table_list/*Compile_unit_listpub FN openDwarfDebugInfo(di:*DwarfInfo,allocator:*mem.Allocator)!void。

发布常量DwarfInfo=struct{DWARF_SEEKABLE_STREAM:*DwarfSeekableStream,dwarf_in_stream:*DwarfInStream,endian:builtin.endian,debug_info:Section,debug_abbrev:Section,debug_str:Section,debug_line:Section,debug_range:?Section,abbrev_table_list:ArrayList(缩写vTableHeader。

要将这些连接起来,粘合代码需要将带有偏移量的DwarfInfo字段初始化为std.io.SeekableStream,它可以实现为指向内存的简单指针。通过声明外部变量,然后查看它们的地址,我们可以找出链接器脚本中定义的符号在内存中的位置。

对于.debug_abbrev、.debug_str和.debug_range,我不得不将偏移量设置为0以解决LLD问题,认为出于某种原因这些部分从0开始。

var KERNEL_PARGIC_ALLOCATOR_BYTES:[100x1024]U8=未定义;VAR KERNEL_PARGIC_ALLOCATOR_STATE=std.heap.FixedBufferAllocator.init(kernel_panic_allocator_bytes[0..]);const内核_PARGIC_ALLOCATOR=&;kernel_panic_allocator_state.allocator;extern VAR__DEBUG_INFO_START:U8;外部VAR__DEBUG_INFO_END:U8;外部VAR_DEBUG_ABBRV_START:U8;外部VAR__DEBUG_ABBRV_END:U8;外部VAR__DEBUG_ABBRV_START:U8;外部VAR__DEBUG。extern var__debug_str_end:u8;extern var__debug_line_start:u8;extern var__debug_line_end:u8;extern var__debug_range_start:u8;extern var__debug_range_end:u8;fn dwarfSectionFromSymbolAbs(start:*u8,end:*u8)std.debug.DwarfInfo.Section{。}fn dwarfSectionFromSymbol(start:*u8,end:*u8)std.debug.DwarfInfo.Section{return std.debug.DwarfInfo.Section{.offset=@ptrToInt(Start),.size=@ptrToInt(End)-@ptrToInt(Start),};}fn getSelfDebugInfo()!*std.debug.DwarfInfo{const S=。var in_stream_pos:usize=0;const in_stream=&;in_stream_state;fn readFn(self:*std.io.InStream(Anyerror),buffer:[]U8)anyerror!usize{const ptr=@intToPtr([*]const U8,in_stream_pos);@memcpy(Buffer.ptr,ptr,Buffer.len);in_stream_pos+=。var SEEKABLE_STREAM_STATE=SEEKABLE_STREAM=SEEKABLE_STREAM_STATE;.earkForwardFn=SEEKFForwardFn,.getPosFn=getPosFn,.getEndPosFn=getEndPosFn,};常量SEEKABLE_STREAM=&;SEEKABLE_STREAM_STATE;Fn SEEKTOFn(SELF:*SeekableStream,Pos:u。}fn getPosFn(self:*SeekableStream)anyerror!usize{return in_stream_pos;}fn getEndPosFn(self:*SeekableStream)anyerror!usize{return@ptrToInt(&;__debug_range_end);}};if(S.had_self_debug_info)return&;S.self_debug_info;S.Self_DEBUG_INFO=标准调试.DwarfInfo{.dwarf_SEEKABLE_STREAM=S.SEEKABLE_STREAM,.dwarf_in_STREAM=S.in_STREAM,.endian=builtin.Endian.Little,.debug_info=dwarfSectionFromSymbol(&;__debug_info_start,&;__DEBUG_INFO_END),.debug_abbrev=dwarfSectionFromSymbolAbs(&;__debug_abbrev_start,&;__DEBUG_ABBRV_END),.debug_str=dwarfSection。__DEBUG_STR_START,&;__DEBUG_STR_END),.debug_line=dwarfSectionFromSymbol(&;__debug_line_start,

您可以看到,当需要分配时接受分配器作为参数的Zig常见实践对于内核开发非常方便。我们简单地静态分配一个100 KiB的缓冲区,并将其作为调试信息分配器进行传递。如果这个值变得太小,可以进行调整。

Pub FN死机(消息:[]const U8,STACK_TRACE:?*builtin.StackTrace)noreturn{序列.log(";\n!内核死机!{}\n";,message);const wwarf_info=getSelfDebugInfo()catch|err|{Serial.log(";Unable to get debug info:{}\n";,@errorName(Err));挂起()。var it=std.debug.StackIterator.init(first_trace_addr);While(it.next())|Return_Address|{std.debug.printSourceAtAddressDwarf(DWARF_INFO,SERIAL_OUT_STREAM,RETURN_ADDRESS,TRUE,//tty color on printLineFromFile,)catch|err|{Serial.log(";错过堆栈帧:{}\n";,@errorName(Err));Continue;};}挂起();}FN HANG()NORETURN{WHILE(True){}}FN printLineFromFile(out_stream:var,line_info:std.debug.LineInfo)anyerror!void{seral.log(";TODO从文件打印行\n";);}。

你好,世界!ClashOS 0.0!内核死机!?中的整数overflow/home/andy/dev/clashos/src/main.zig:166:7:0x15e0。(Clashos)TODO打印文件^?:0x1c中的行??(?)。

最后一行来自启动汇编代码,它没有源代码映射。但是通过观察内核的反汇编,我们可以看到它是正确的:

0000000000000000<;_Start>;:0:d53800a0 MRS x0,MPIDR_EL1 4:d2b82001 mov x1,#0xc1000000 8:8a210000 Bic x0,x0,x1 c:b4000040 CBZ x0,14<;Master>;10:14000003 b1c<;__Hang>;0000000000000014<;Master>;:14:b26503。

您可以看到,0x1c确实是对kernel_main的函数调用的返回地址(函数返回时将执行的下一条指令)。

你好,世界!ClashOS 0.0!内核死机!整数overflow/home/andy/dev/clashos/src/serial.zig:42:7:0x1b10 in?(Clashos)TODO打印文件^/home/andy/dev/clashos/src/main.zig:58:16:0x1110中的行?(Clashos)TODO打印文件^/home/andy/dev/clashos/src/main.zig:67:18:0xecc中的行?(Clashos)TODO打印文件^?:0x1c中的行??(?)。

它看起来不错。但是我们能不能在上面加个樱桃,让它印上香肠呢?

同样,我们没有文件系统。printLineFromFile函数如何访问源文件?

有时最简单的解决方案就是最好的解决方案。如果内核在内存中只有它自己的源代码呢?

const source_files=[][]const U8{";src/debug.zig";,";src/main.zig";,";src/mmio.zig";,";src/seral.zig";,};fn printLineFromFile(out_stream:var,line_info:std.debug.LineInfo)anyerror!void{inline for(Source_Files)|src_path|{if(std.mem.endsWith(u8,line_info.file_name,src_path)){const Contents=@EmbedFile(";../";++src_path);尝试printLineFromBuffer(。(源文件{}未添加到std/debug.zig中)\n";,line_info.file_name);}。

在这里,我们利用了inline Form和@EmbedFile,并且可以打印我们自己的源文件中的代码行。将printLineFromBuffer作为练习留给读者。

你好,世界!ClashOS 0.0!内核死机!整数overflow/home/andy/dev/clashos/src/serial.zig:42:7:0x1b10 in?(Clashos)x+=1;^/home/andy/dev/clashos/src/main.zig:58:16:0x1110 in?(Clashos)seral.om();^/home/andy/dev/clashos/src/main.zig:67:18:0xecc in?(Clashos)SOME_Function();^?:?::0x1c中??(?)。

现在,Zig提供的所有针对未定义行为的保护都会产生这样的结果。

我使用Zig的一个大目标是改进嵌入式和操作系统的开发过程。我希望你和我一样对这里的潜力感到兴奋。

如果这篇博客文章抓住了你的兴趣,也许你会想看看这个Hello World x86内核,它附带了一段我实时编码的视频。多亏了一条富有洞察力的Twitch评论,它只支持Zig代码,不需要汇编。

Zig编程语言是众筹的,不受制于任何企业或一组企业。请考虑赞助我的工作。