自定义内核的上下文切换对齐问题
我已经用Rust和汇编为基于32位 RISC-V的嵌入式系统编写内核好几个月了。今天我才注意到,当我运行某个任务一段时间后,我的内核会因为那个错误而panic:
{ message: is_aligned_to: align is not a power-of-two, location: Location { file: "/Users/elouan/.rustup/toolchains/nightly-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ptr/const_ptr.rs", line: 1374, column: 13 }, can_unwind: true, force_no_backtrace: false }
我试着追踪错误原因,觉得可能出在我的上下文切换上。经过一段时间后,任务的sp可能变得未对齐。
这是我的上下文切换代码:
这是保存上下文的函数:
.global trap_save_context
.type trap_save_context, @function
trap_save_context:
# Clear mstatus.MPP and mstatus.mie to avoid interruption while saving and switching context
li t0, ((3 << 11) | (1 << 3))
csrrc x0, mstatus, t0
# Move current task context struct save in caller in a0 reg to t6
mv t6, a0
# Store ra in task context structure
sw a1, OFFSET_PC(t6)
# Store word from t0(sp) in context structure
sw a2, OFFSET_SP(t6)
# Save current task context using GNU macro
# GNU macros from `src/arch/riscv32/asm/gnu_macro.S`
.set i, 1
.rept 31
save_gp_context %i
.set i, i+1
.endr
ret
以及restore_context函数:
.global trap_restore_context
.type trap_restore_context, @function
trap_restore_context:
# Move current task context struct save in caller in a0 reg to t6
mv t6, a0
# Update sp
# Restore sp from current task context structure
lw t0, OFFSET_SP(t6)
mv sp, t0
# Update mepc
lw t0, OFFSET_PC(t6)
csrw mepc, t0
# Restore current task context using GNU macro
# GNU macros from `src/arch/riscv32/asm/gnu_macro.S`
.set i, 1
.rept 31
load_gp_context %i
.set i, i+1
.endr
# Update mstatus
li t0, ((3 << 11) | (1 << 7))
csrrs x0, mstatus, t0
mret
我知道偏移量是正确的。我只是通过在任务函数、定时器中断函数处打印日志来追踪错误,错误出现在定时器中断函数之后,因此我猜是在我的asm trap函数的尾部处理阶段造成的。我在尾部处理一个指针,但我不认为问题来自这里,下面给出我的trap函数以供参考:
# Static variable representing offset in TrapFrame structure
# general purpose registers offset
.set OFFSET_GP_BASE, 0
# Supervisor Address Translation and Protection Register (satp register only exist when supervisor mode is enabled)
.set OFFSET_SATP, 128
# current hart id
.set OFFSET_HARTID, 132
# static array to use as a stack inside the trap
.set OFFSET_TRAP_STACK, 136
.global trap_entry
.type trap_entry, @function
trap_entry:
# Save task context
# Get current task ptr
la t0, TASK_HANDLER # Address in RAM
lw t1, 0(t0) # Get the value behind the ref
mv a0, t1
# Save current pc
csrr a1, mepc
# Save current sp
mv a2, sp
# Call the save context function
call trap_save_context
# Read mscratch into t7
csrr t6, mscratch
lw t1, OFFSET_TRAP_STACK(t6) # t1 = trap_stack pointer
# Branch instruction to check if t1 = 0
beqz t1, trap_no_trapstack
# Move trap stack ptr into sp
mv sp, t1
# Read CSR into function argument registers
csrr a0, mepc
csrr a1, mtval
csrr a2, mcause
csrr a3, mhartid
csrr a4, mstatus
# Move trap frame structure into a5 function argument register
mv a5, t6
# Call trap_handler rust function
call trap_handler
# Check if a re-schedule is needed or not.
call read_need_reschedule
# If a0 != 0, goto 1f, else mret
bnez a0, 1f
# If there's no need to a reschedule, restore the task context
la t0, TASK_HANDLER # Address in RAM
lw t1, 0(t0) # Get the value behind the ref
mv a0, t1
call trap_restore_context
1:
call scheduler
# Function used when the trap strack = 0, just infinite loop for debuging purpose
trap_no_trapstack:
j .
我使用了一些GNU宏来保存和加载上下文,列出如下:
´´´
# Macro used to load context from all saved general purpose registers store in t6 during a context switch
# Avoid loading sp and t6 registers to avoid overriding both
.macro load_gp_context i, basereg= t6
.if ((\i) != 1) && ((\i) != 2) && ((\i) != 31)
lw x\i, ((\i)*REG_SIZE)(\basereg)
.endif
.endm
# Macro used to store context from all general purpose registers store in t6 during a context switch
# Avoid storing sp and t6 registers, because they are already save in other structure field
.macro save_gp_context i, basereg= t6
.if ((\i) != 1) && ((\i) != 2) && ((\i) != 31)
sw x\i, ((\i)*REG_SIZE)(\basereg)
.endif
.endm
´´´
TASK_HANDLER 就是我处理的指针,我在任务函数中对它进行检查,所以我不认为问题来自那里。我对汇编不太在行,是从这个内核项目学来的,所以我猜问题出在那里。
解决方案
所以在你的 trap_entry 和上下文加载部分都存在一些问题。
先看看 trap_entry。正如你所说,trap_entry 是最初的陷阱入口点(命名得相当贴切)。要让陷阱入口点正确,它必须保留用户态的完整状态,至少对于像中断这样的异步陷阱也是如此。你当前的代码至少会把t0、t1、a0、t6、a1、a2和 RA都“搞坏”!我很惊讶你的操作系统居然在多次陷阱中还能存活下来,每次陷阱都造成如此的损坏。
实现陷阱处理程序的常见方式如下:
trap_entry:
csrrw t0, mscratch, t0 # stash t0 to scratch and get TrapFrame address into t0
sw t1, OFFSET_T1(t0)
csrrw t1, mscratch, t0 # restore mscratch value to TrapFrame address
# it's a good idea to restore it asap so your trap_entry could trap an exception in kernel code
# alternatively, another kernel-mode TrapFrame could be stashed there upon entry if you wish to have non-fatal kernel exceptions
sw t1, OFFSET_T0(t0) # and save the initial user-value of t0
# now save the rest of GPR...
# NOTE do not use call until you saved ra value
所以基本上是在早期设置陷阱处理时把任何陷阱保存区域的地址放到 mscratch,在陷阱进入时你使用mscratch来得到一个可用于保存其余寄存器的单一寄存器,然后用它来保存其余寄存器。
如需更多灵感,我会把你引到关于该主题的两篇不错的回答:
以及Redox OS中一个在同时处理用户态和内核异常时工作正常的陷阱处理程序:
至于你的上下文加载,有一个简单的错误。你使用t6来做它,这点很聪明,因为它是最后一个寄存器x31(我会直接用x31,但这只是风格上的小挑剔)。唯一的问题是,在你恢复完所有寄存器后,你会立刻把t0再次搞乱/覆盖掉。