Zig错误返回跟踪
背景
在使用 Zig 编写 Riscv freestanding 的用户态/内核态切换相关代码时,遇到了在进入系统调用时遇到了访问栈顶指针从而出现 Page Fault 的异常问题。
问题解析
异常
通过单步调试与汇编分析注意到在即将执行系统调用时,会有以下汇编。
1  | 0000000080218954 <syscall.sys_exec>:  | 
sys_exec函数声明如下:
1  | fn sys_exec() u64 {  | 
通过调试发现此时a0寄存器的值为内核栈栈顶指针,以下是内存布局(参考xv6):
1  | 
  | 
而栈顶指针是不会进行映射的,所以会造成页面错误。
问题追踪到这里,已经知晓了异常的原因是页面访问错误,且访问地址是内核栈栈顶。那接下就需要了解为什么会是这个地址?
优化等级
该地址的赋值是由于在用户态访问跳板进入内核态时的汇编实现uservec会将a0寄存器恢复成栈顶指针,但这有另一个问题是为什么在没有用到参数的函数上会生成访问a0寄存器的汇编代码。
在实验过程中,我一直都是使用Zig编译系统的Debug编译优化等级,原因是未指定优化等级时默认即此。const optimize = b.standardOptimizeOption(.{});
在尝试使用ReleaseSmall/ReleaseFast时发现没有异常的问题。检查对应生成的汇编代码也没有了访问a0寄存器的指令。
Error Return Trace
在查看Zig文档中,注意到这个与语言特性相关的内容,并经过了解发现在Release/Debug的优化等级上确实有不同的结果。通过编译参数来指定关闭这个特性
1  | zig build-exe xxx.zig -target riscv64-freestanding -O Debug -fno-error-tracing  | 
在禁用该特性后,确实不会页面访问异常。
Error Return Trace
Zigzig错误返回跟踪,会在函数调用时在生成这样结构的结构体隐形参数。
This is to initialize this struct in the stack memory:
1  | pub const StackTrace = struct {  | 
A pointer to
StackTraceis passed as a secret parameter to every function that can return an error, but it’s always the first parameter, so it can likely sit in a register and stay there.
为什么仅仅在Debug模式下会开启,文档有如下描述:
Error Return Traces are enabled by default in Debug builds and disabled by default in ReleaseFast, ReleaseSafe and ReleaseSmall builds.
使用C函数约定时的差异:
- C函数不返回错误
 - 不需要追踪错误路径
 - 因此不会产生访问a0的汇编代码
 
解决方法
- 调整编译优化等级为
Release - 使用C函数约定声明汇编与Zig函数的交互函数
 
错误总结
对于用户态切换,从用户态到内核态的syscall,
a0寄存器会被赋值为TRAPFRAME,进程内核栈栈顶,此处没有被映射。
在 Debug 优化模式下,会通过a0存储一个ErrorReturnTrace的隐式结构指针,用于错误异常路径存储。
但这在Freestranding下的cpu状态切换时不一定会定义有效的a0地址导致访问一个内核态中没有映射的虚拟地址,从而报错。