上一篇

小源:從零開始寫 OS (3) —— 格式化輸出?

zhuanlan.zhihu.com圖標

概要

當 cpu 訪問無效的寄存器地址、進行除零操作或進行 系統調用 時,會產生中斷。由 系統調用 產生的中斷稱為 trap 。為了能夠對中斷進行一些簡單的處理,本章我們需要:

  1. 創建 棧幀(TrapFrame) 結構體。
  2. 設置中斷入口點。

  3. 能夠保存和恢復寄存器。

創建棧幀結構體

當產生中斷時,我們需要保存當前 所有寄存器 的狀態,然後處理中斷,最後恢復寄存器狀態,繼續執行之前的命令。我們需要按照特定的格式保存寄存器,以便於我們使用 棧幀 結構體查看或修改這些寄存器。可以理解為,在一片連續的內存空間中存放了我們寄存器的狀態,我們通過這片空間的首地址(指針)來訪問他們。在創建結構體之前,我們在 Cargo.toml 中需要引入一些依賴:

[dependencies]
riscv = { git = "https://github.com/xy-plus/riscv", features = ["inline-asm"] }

riscv32 中有 32 個通用寄存器和部分特殊寄存器。在 main.rs 的同級目錄下創建 context.rs 文件,在開頭引入一些特殊寄存器:

use riscv::register::{
sstatus::Sstatus,
scause::Scause,
};

棧幀結構體的實現如下:

#[repr(C)]
pub struct TrapFrame {
pub x: [usize; 32], // General registers
pub sstatus: Sstatus, // Supervisor Status Register
pub sepc: usize, // Supervisor exception program counter
pub stval: usize, // Supervisor trap value
pub scause: Scause, // Scause register: record the cause of exception/interrupt/trap
}

[repr(C)] 表示不希望編譯器對結構體的變數順序做出改變等優化

理解並創建棧幀之後,我們便可以開始對中斷進行處理了。

設置中斷入口點

在 main.rs 的同級目錄下創建 trap/trap.asminterrupt.rs 用於處理中斷。

當我們的程序遇上中斷或異常時, cpu 會跳轉到一個指定的地址進行中斷處理。在RISCV中,這個地址由 stvec 控制寄存器保存。

ebreakecall 嚴格來說屬於主動觸發的異常。異常是在執行指令的過程中「同步」發生的,相對地中斷則是「非同步」發生的,由外部信號觸發(如時鐘、外設、IPI)。

此外,RISCV中還有兩種中斷入口模式:

  • 直接模式(Driect):直接跳轉到 stvec
  • 向量模式(Vectored):對第 i中斷 ,跳轉到 stvec + i * 4;對所有異常,仍跳轉到stvec

為了實現簡單,我們採用第一種模式,先進入統一的處理函數,之後再根據中斷/異常種類進行不同處理。

interrupt.rs 中引入 棧幀stvec ,幫助我們實現 指定中斷處理函數 的函數:

use crate::context::TrapFrame;
use riscv::register::stvec;

#[no_mangle]
pub fn init() {
extern {
fn __alltraps();
}
unsafe {
stvec::write(__alltraps as usize, stvec::TrapMode::Direct);
}
println!("++++setup interrupt !++++");
}

__alltraps 便是我們的程序在遇上中斷時, cpu 跳轉到的地址。現在我們來實現他:

# in trap.asm

.section .text
.globl __alltraps
__alltraps:
SAVE_ALL
mv a0, sp
jal rust_trap
.globl __trapret
__trapret:
RESTORE_ALL
# return from supervisor call
XRET

SAVE_ALL 用於保存所有的寄存器的狀態, RESTORE_ALL 則用於恢復所有的寄存器的狀態。為了增加代碼的可讀性,我們使用了較多的宏。在 main.rs 中引入 trap.asm 之前,我們需要先定義使用的宏:

# in trap/trap.asm

.equ XLENB, 4
.equ XLENb, 32
.macro LOAD a1, a2
lw a1, a2*XLENB(sp)
.endm
.macro STORE a1, a2
sw a1, a2*XLENB(sp)
.endm

有了上面定義的宏之後,我們就可以開始編寫 SAVE_ALLRESTORE_ALL 了。增加了這兩個部分之後, trap/trap.asm 應該長這樣:

.macro SAVE_ALL
# If coming from userspace, preserve the user stack pointer and load
# the kernel stack pointer. If we came from the kernel, sscratch
# will contain 0, and we should continue on the current stack.
csrrw sp, sscratch, sp
bnez sp, _save_context
_restore_kernel_sp:
csrr sp, sscratch
# sscratch = previous-sp, sp = kernel-sp
_save_context:
# provide room for trap frame
addi sp, sp, -36 * XLENB
# save x registers except x2 (sp)
STORE x1, 1
STORE x3, 3
# tp(x4) = hartid. DONT change.
# STORE x4, 4
STORE x5, 5
STORE x6, 6
STORE x7, 7
STORE x8, 8
STORE x9, 9
STORE x10, 10
STORE x11, 11
STORE x12, 12
STORE x13, 13
STORE x14, 14
STORE x15, 15
STORE x16, 16
STORE x17, 17
STORE x18, 18
STORE x19, 19
STORE x20, 20
STORE x21, 21
STORE x22, 22
STORE x23, 23
STORE x24, 24
STORE x25, 25
STORE x26, 26
STORE x27, 27
STORE x28, 28
STORE x29, 29
STORE x30, 30
STORE x31, 31

# get sp, sstatus, sepc, stval, scause
# set sscratch = 0
csrrw s0, sscratch, x0
csrr s1, sstatus
csrr s2, sepc
csrr s3, stval
csrr s4, scause
# store sp, sstatus, sepc, sbadvaddr, scause
STORE s0, 2
STORE s1, 32
STORE s2, 33
STORE s3, 34
STORE s4, 35
.endm

.macro RESTORE_ALL
LOAD s1, 32 # s1 = sstatus
LOAD s2, 33 # s2 = sepc
andi s0, s1, 1 << 8 # sstatus.SPP = 1?
bnez s0, _to_kernel # s0 = back to kernel?
_to_user:
addi s0, sp, 36*XLENB
csrw sscratch, s0 # sscratch = kernel-sp
_to_kernel:
# restore sstatus, sepc
csrw sstatus, s1
csrw sepc, s2

# restore x registers except x2 (sp)
LOAD x1, 1
LOAD x3, 3
# LOAD x4, 4
LOAD x5, 5
LOAD x6, 6
LOAD x7, 7
LOAD x8, 8
LOAD x9, 9
LOAD x10, 10
LOAD x11, 11
LOAD x12, 12
LOAD x13, 13
LOAD x14, 14
LOAD x15, 15
LOAD x16, 16
LOAD x17, 17
LOAD x18, 18
LOAD x19, 19
LOAD x20, 20
LOAD x21, 21
LOAD x22, 22
LOAD x23, 23
LOAD x24, 24
LOAD x25, 25
LOAD x26, 26
LOAD x27, 27
LOAD x28, 28
LOAD x29, 29
LOAD x30, 30
LOAD x31, 31
# restore sp last
LOAD x2, 2
.endm

.section .text
.globl __alltraps
__alltraps:
SAVE_ALL
mv a0, sp
jal rust_trap
.globl __trapret
__trapret:
RESTORE_ALL
# return from supervisor call
sret

由於這部分內容難度較高,所以只進行粗略的介紹。對細節感興趣的同學可以 點擊這裡

a0 是 riscv32 中的參數寄存器,用於存放下一個調用的函數的參數。我們給 a0 賦值為 sp ,也就是棧幀的地址。這裡調用的的函數是 rust_trap

// in interrupt.rs

global_asm!(include_str!("trap/trap.asm"));

#[no_mangle]
pub extern "C" fn rust_trap(tf: &mut TrapFrame) {
println!("trap!");
tf.increase_sepc();
}

在 riscv 中,發生中斷指令的 pc 被存入 sepc 。對於大部分情況,中斷處理完成後還回到這個指令繼續執行。但對於用戶主動觸發的異常(例如ebreak用於觸發斷點,ecall用於系統調用),中斷處理函數需要調整 sepc 以跳過這條指令。在riscv中, 一般 每條指令都是定長的4位元組(但如果開啟 壓縮指令集 可就不一定了,這也導致了一個大坑),因此只需將 sepc +4 即可,這裡我們通過 increase_sepc 完成這個功能:

// in context.rs

impl TrapFrame {
pub fn increase_sepc(self: &mut Self) {
self.sepc = self.sepc + 4;
}
}

注意!!!

這裡我們強調了 一般 。在開啟 壓縮指令集 的情況下,對於常用指令,編譯器會進行壓縮,減小程序的大小。但是有時候這並不是我們希望的。比如這裡因為我們要求每條指令都是精準的 32bits ,才能夠通過 self.sepc = self.sepc + 4 跳轉至下一條指令(否則會跳轉到奇怪的地方)。在 riscv32-xy_os.json 中,有一行 "features": "+m,+a,+c" 。默認情況下,riscv 指令集只支持加減法, +m 增加了乘除指令; +a 增加了原子操作; +c 增加了代碼壓縮。這裡的壓縮是我們不想要的,所以把 +c 刪去。

至此我們簡易的的中斷功能已經全部實現完成,讓我們設置一個中斷試試。首先在 main.rs 中增加 #![feature(asm)] ,然後修改 rust_main 為:

#[no_mangle]
pub extern "C" fn rust_main() -> ! {
interrupt::init();
unsafe{
asm!("ebreak"::::"volatile");
}
panic!("End of rust_main");
}

編譯運行,屏幕顯示:

++++setup interrupt !++++
trap!
panicked at End of rust_main, src/main.rs:51:5

可以看到,我們已經成功進入中斷處理函數,並且返回到了 rust_main ,觸發了 panic 。

預告

現在,我們已經實現了簡易的中斷機制。但是同時我們的 main.rs 看起來有一些亂。下一章,我們首先將調整代碼結構,簡化 main.rs ,然後實現時鐘中斷,並在 rust_trap 中區分中斷類型,對他們進行不同的處理。

下一篇

小源:從零開始寫 OS (5) —— 時鐘中斷?

zhuanlan.zhihu.com圖標
推薦閱讀:

相关文章