# Teddy_Cat_OS 開發紀錄
:::success
家裡養了一隻會喵喵叫的泰迪熊,所以叫他泰迪貓
:::
## QEMU
### 核心除錯
- 查看暫存器資訊 `info_register`
- `llvm-objdump -d kernel.elf` 查看組語
- `kernel.map`
### GDB
- 在 qemu 執行 kernel.elf 的地方加上 `-s -S`
```diff
qemu-system-riscv64 \
-machine virt \
-nographic \
-bios none \
-kernel kernel.elf \
+ -s -S
```
- 使用 riscv gdb
```
riscv64-unknown-elf-gdb kernel.elf
```
- 執行 qemu
- 連線到正在跑的 qemu
```
target remote :1234
```
## boot
### Linker Script
#### 指定 linker script
`ld --verbose` 可以顯示預設的 linker script
#### kernel.ld
```c
ENTRY(boot) //指定從這個函式開始執行
SECTIONS {
. = 0x80200000; //設定程式起始位置
.text :{
KEEP(*(.text.boot));//保留 boot 函式,放在這邊會讓 boot 的位置長在 text 開頭
*(.text .text.*);//剩下的 text
}
.rodata : ALIGN(4) { //確保按 4 字節對齊
*(.rodata .rodata.*);
}
.data : ALIGN(4) { //確保按 4 字節對齊
*(.data .data.*);
}
.bss : ALIGN(4) { //確保按 4 字節對齊
__bss = .;//紀錄 bss 的開頭
*(.bss .bss.* .sbss .sbss.*);
__bss_end = .;//紀錄 bss 的結尾
}
. = ALIGN(4); //確保堆從 4 字節邊界開始
. += 128 * 1024; /* 128KB stack size */
__stack_top = .; //stack 會往低位長
}
```
:::info
為什麼是 0x80200000?
ChatGPT: 0x80000000 是 qemu-system-riscv64 -machine virt 預設起點
多了 0x200000 通常預留給 bootloader(如 OpenSBI)、堆疊、MMU tables、或其他 early-stage 初始化,啊這邊應該是 OpenSBI
:::
### 開機程式
#### 流程
1. 進入 boot 函式
- 這邊使用 `j kernel_main\n` 而非 function call ,是因為它本來就不預期有回傳值
2. 跳轉到 kernel_main 並且陷入無限迴圈
## risc-v Supervisor-Level
### sstatus

SPP(Supervisor Previous Privilege) 位元表示陷入監督者模式(supervisor mode)之前,hart 的執行特權等級。當發生陷阱(trap)時,若陷阱源自使用者模式(user mode),則 SPP 會被設為 0;否則設為 1。
當執行 SRET 指令(見第 3.3.2 節)從 trap 返回時,如果 SPP 為 0,則切回使用者模式;如果 SPP 為 1,則切回監督者模式。之後,SPP 會被重設為 0。
SIE(Supervisor Interrupt Enable) 位元用來啟用或停用監督者模式下的所有中斷。當 SIE 被清除(設為 0)時,在監督者模式中不會響應中斷。當 hart 在使用者模式下運行時,SIE 的值會被忽略,監督者層級的中斷仍會被啟用。監督者可以透過 sie CSR 來停用個別中斷來源。
SPIE(Supervisor Previous Interrupt Enable) 位元表示陷入監督者模式前,中斷是否啟用。當 trap 進入監督者模式時,SPIE 會被設為當時的 SIE,而 SIE 則會被清除(設為 0)。當執行 SRET 指令時,SIE 會被設為 SPIE 的值,而 SPIE 則會被設為 1。
### stvec

- 可以設定 s mode trap 的處理函式
- BASE 是實體或虛擬的有效記憶體位址,但要對齊到 4 bytes
trap 處理函式必須 `__attribute__((aligned(4))`
- MODE 看起來是 trap function 的位址在放上一個 mask 來設定,但 [AMD 的 docs](https://docs.amd.com/r/en-US/ug1629-microblaze-v-user-guide/Supervisor-Trap-Vector-Base-Address-Register-stvec) 又說它 read only ,目前應該是不會動到它
### Supervisor Interrupt Registers (sip and sie)

- sip 控制哪些中斷待處理
- sie 控制哪些中斷 enable
- interrupt exception code 對應到 sip 和 sie 中的第 i 位元
- 當以下兩個條件都成立時,中斷 i 會陷入(trap)至監督者模式(S-mode):
1. 目前特權模式為 S,且 sstatus 暫存器中的 SIE 位元為 1,或目前的特權模式低於 S-mode;
2. sip 與 sie 的第 i 位元都被設為 1。
#### 中斷類別說明
sip.SEIP 和 sie.SEIE 位元分別代表監督者層級的外部中斷的中斷待處理位與中斷使能位。
sip.STIP 和 sie.STIE 位元分別代表監督者層級的 timer interrupt 的中斷待處理位與中斷使能位。
若有實作,sip 中的 STIP, SEIE 為唯讀,由執行環境設定與清除。
sip.SSIP 和 sie.SSIE 位元分別代表監督者層級的軟體中斷的中斷待處理位與中斷使能位。
若有實作,sip 中的 SSIP 是可寫的,也可能由平台特定的中斷控制器設為 1。
### Counter-Enable Register (scounteren)
scounteren 是一個 32 位元的暫存器,用來控制 U-mode(使用者模式)對硬體效能監控計數器的存取權限。
### Supervisor Exception Program Counter(sepc)
當陷阱(trap)進入 S-mode 時,sepc 會被寫入當時遭中斷或發生例外的那條指令的虛擬位址。除此之外,sepc 不會被硬體自動寫入,但軟體可以顯式地寫入它。
:::info
sret 會根據這個值返回?
:::
### Supervisor Cause Register (scause)


- 觸發 trap 的是 interrupt 時,該位元會變成 1
## timer interrupt
### 不使用 opensbi
由於 mtime, mtimecmp 相關存取必須在 M mode ,但使用 OpenSBI 開機時會在 S mode ,沒辦法存取。
[參考](https://danielmangum.com/posts/risc-v-bytes-timer-interrupts/#step-2-enabling-timer-interrupts)
```c
__attribute__((section(".text.boot")))
__attribute__((naked))
void boot() {
__asm__ __volatile__(
"mv sp, %[stack_top]\n"
"li t0, 0x1800\n"
"csrc mstatus, t0\n"
"li t0, 0x802\n"
"csrs mstatus, t0\n"
"li t0, 0xa0 \n"
"csrs mie, t0\n"
"li t0, 0x20\n"
" csrs mideleg, t0\n"
" la t0, mtrap\n"
" csrw mtvec, t0\n"
" li t0, 500000\n"
" li t1, 0x2004000\n"
" sw t0, 0(t1)\n"
" la t1, supervisor\n"
" csrw mepc, t1\n"
" mret\n"
"mtrap:\n"
" li t0, 0x20\n"
" csrs mip, t0\n"
" mret\n"
"supervisor:\n"
" la t0, strap\n"
" csrw stvec, t0\n"
" j spin\n"
"strap:\n"
" j spin\n"
"spin:\n"
" j spin\n"
:
: [stack_top] "r" (__stack_top)
);
}
```
- boot 函式作為 entry
- mstatus 要設成 `0x802` ,這將 `SIE` 設成 1 ,`MPP` 設成 1

- mie 設成 `0xa0`

- mideleg
>mideleg holds trap delegation bits for individual interrupts, with the layout of bits matching those
in the mip register (i.e., STIP interrupt delegation control is located in bit 5).
- mtvec 設定 M mode interrupt 的處理函式
- mepc 設定 M mode 返回 S mode 要執行哪裡的指令
- 0x2004000 的是 mtimecmp 的位址?
:::warning
目前遇到的問題是,發現 mret 沒辦法跑到 supervisor 上
詳細一點是它會觸發 instruction access fault
儘管相關暫存器在 mret 之前都是正確的設定(利用 info register + llvm-objdump 的觀察)
:::
### 使用 OpenSBI
OpenSBI 提供一個 extension `sbi_set_timer` ,可以在 S mode 利用 ecall 呼叫它
#### boot 新增設定
- 啟用 sie 的 STIE
- 啟用 sstatus 的 SIE ,啟用 S mode 的中斷處理
- stvec 設定 S mode trap 的處理函式
`kernel entry` 必須 `__attribute__((aligned(4)))`
不然會壞掉(原因還不知道)
`kernel entry` 直接使用 OS in 1000 lines 的 code
#### sbi_set_timer
- `sbi_call` 直接使用 OS in 1000 lines 的 code
- 根據 riscv-sbi.pdf , SBI version 0.1 以前的版本, EID 是 `0x0` ,但似乎從 0.2 版本開始變成 `0x54494D45` ,直接使用 virt 預設的 bios , SBI version 是 1.0 。題外話, Linux risc-v 的 SBI version 好像 default 是 0.1
- 由於我使用的是 rv32 的架構, sbi_set_timer 的輸入要求是 `uint64_t` ,因此要把輸入拆成前後兩半,低位放 a0 ,高位放 a1 (可參考 [linux/arch/riscv/kernel/sbi.c](https://elixir.bootlin.com/linux/v6.16/source/arch/riscv/kernel/sbi.c#L96))
- 由於輸入要求是絕對時間,因此必須要實作一個能取得絕對時間的函式, `get_time`
- 根據 riscv-privileged.pdf 4.1.4 Supervisor Timers and Performance Counters ,實作會提供可以根據 time 觸發 timer interrupt 的方法,所以能取得這個 time 就能取得所需要的絕對時間
#### get_time
```c
static inline uint64_t get_time(void) {
uint32_t lo, hi, tmp;
__asm__ __volatile__(
"1:\n"
" rdtimeh %0\n"
" rdtime %1\n"
" rdtimeh %2\n"
" bne %0, %2, 1b\n"
: "=r"(hi), "=r"(lo), "=r"(tmp)
);
return ((uint64_t)hi << 32) | lo;
}
```
:::info
>The RDTIME pseudoinstruction reads the low XLEN bits of the "time" CSR, which counts wall-clock real
time that has passed from an arbitrary start time in the past. RDTIMEH is only present when XLEN=32
and reads bits 63-32 of the same real-time counter.
節錄自 The RISC-V Instruction Set Manual Volume I
Chapter 7. "Zicntr" and "Zihpm" Extensions for Counters,
Version 2.0
:::
- rdtime 可以取得 wall time
- 在 32 位元架構下,要分成 rdtime, rdtimeh 分別存取
[d10ce8f](https://github.com/weiso131/Teddy-Cat-OS/commit/d10ce8f88ebb5099223210545c2ae3ede5c299f1)
## 記憶體配置
由於 os in 1000 lines 有實作虛擬記憶體, virt 也有 MMU ,所以目標應該是會做出一個有虛擬記憶體的 OS
### alloc_pages
os in 1000 lines 最底層的記憶體配置,是先在 `kernel.ld` 宣告一個 64MB 的區塊,然後再由這個函式把這塊記憶體分成多個 page 。
:::info
這邊把區塊上限寫死在 linker script ,將無法隨著硬體 DRAM 大小增加而增加可用記憶體,然而,目前這個作業系統只會在 qemu 上跑,這個方法暫時可行,短時間不會改
:::
- 原本的實作缺乏釋放頁面資源的函式,是個可以改進的點
文中有提到 bitmap ,就是使用多個位元紀錄哪些區塊的使用狀態,由於 page 數量是固定的,這東西可以直接放在 bss ,初始化為 0 代表尚未使用
### page table
#### sv32
以下比較官方說明與 OS in 1000 lines 的實作
- 每個 page table 有 $2^{10}$ 個項目,有兩層的 page table ,第一層是 `VPN[1]` ,第二層對應 `VPN[0]`
- 
- 
- `vpn1 = (vaddr >> 22) & 0x3ff`
- `table1[vpn1] = ((pt_paddr / PAGE_SIZE) << 10)`
- 每個 page table entry 用 4 bytes 儲存
- `uint32_t *page_table`
- 一張 page table 大小剛好會是一個 page ($2^{10}\cdot4=4096$)
- 都只分配一個 page 給 `page_table`
- root page table 的 **實體頁號** 儲存在 satp
- `page_table` 除以 `PAGE_SIZE`
- page table entry bit
- V (valid)
- R (readable)
- W (writable)
- X (executable)
- U (User mode accessable)
- G (global mapping)
- A (accessed)
- 表示該虛擬頁自上次 A 位元被清除後,是否曾被讀取、寫入或取指令
- D (dirty)
- 表示該虛擬頁自上次 D 位元被清除後,是否曾被寫入
#### sfence.vma
todo
## scheduler & process
發現虛擬記憶體很麻煩所以先跑來弄這個
### context_switch
#### 教學的作法
由於他是實作協同式多工,會定期呼叫 `context_switch` ,這是一個函式呼叫,可以藉由修改 `ra` 的值,在 `ret` 之後開始執行另外一個 process
#### 搶佔式可以怎麼做?
然而,要實現搶佔式多工的 context switch ,就沒辦法使用 ret 這招,排程的時候會進入 timer interrupt ,也就是 trap 狀態,需要使用 `sret` 回到原本的特權模式,就算原本 process 就在 S mode (這個專案的 trap 都在 S mode 處理),也必須返回。
:::warning
不確定不返回的結果,會是造成巢狀迴圈,或是在下一次 timer interrupt 該處發時,根本不會進入 trap ,兩種狀態都很糟糕就是,單結果應該要如何之後補。
Todo
:::
第一個想法是直接在 `kernel_entry` 動手腳,先把 `context` 都存到 `current->context` ,排程時把 `current` 換成其他 `task` ,要離開 `kernel_entry` 的時候就把 `current` 的東西讀出來。
第二個想法就是 `sret` 觀念問題,究竟什麼情況可以離開 `trap` ,若我在 `context_switch` 時呼叫 `sret` 來做上面的事情會怎樣?
>根據 The RISC-V Instruction Set Manual: Volume II 3.3.2. Trap-Return Instructions , SRET/MRET 本來就是用來從 trap 狀態返回的
#### linmo 的作法
linmo 的與 teddy_cat_os 相同,都是搶佔式排程,可以作為參考。
相較我想到的方法,也就是儲存 timer interrupt 前的執行狀態, linmo 的作法是儲存 trap 狀態中的 context , context_switch 之後再走 trap 恢復的路徑回到原本執行的位置。詳細流程如下:
1. 根據 `mtvec` ,觸發中斷與例外由 `_isr` 處理
2. 保存中斷前狀態,觸發 `do_trap`
- 這邊可以看到它保存了 `mepc` ,這可以用來實現 context_switch?
3. 重製時間,並且觸發 `dispatcher`
4. 主要進行 `context_switch` 的函式 `_dispatch` (預設會指向 `dispatch`)
5. `hal_context_save` 保存狀態,回傳 0 來繼續執行之後的工作
6. 選擇令一個任務,並且藉由 `hal_context_restore` 切換過去, `hal_context_restore` 回傳 1 之後,另外一個 task 就會在 `hal_context_save` 的地方醒來並且 return
:::info
在 `hello.c` 查看 `mstatus` 檢查權限狀態
```
Linmo kernel is starting...
Heap initialized, 130011952 bytes available
task 1: entry=80000230 stack=80002d40 size=4096 prio_level=4 time_slice=5
task 2: entry=800001ec stack=80003df4 size=4096 prio_level=4 time_slice=5
task 3: entry=8000017c stack=80004e74 size=4096 prio_level=4 time_slice=5
task0 has id 1
task1 has id 2
task2 has id 3
Scheduler mode: Preemptive
[task 1 100000], mstatus: 1808
[task 2 200000], mstatus: 1808
[task 3 300000 - sys uptime: 0.022s]
[task 1 100001], mstatus: 88
[task 2 200001], mstatus: 88
[task 3 300001 - sys uptime: 0.052s]
...
```

>The RISC-V Instruction Set Manual Volume II: Privileged Architecture 3.1.6 Machine Status Registers (mstatus and mstatush
~~可以觀察到,在剛啟動的時候,任務是執行在 machine mode 上,但第二次之後就跑回 user mode ,但我不懂它是怎麼跑回 user mode 的。此外,既然要使用 user mode ,那一開始使用 machine mode 不會很危險嗎?我在 hello.c task0 內,把 mstatus 改成 `0x1800` ,就會把中斷關掉,永遠執行 task0 。~~
發了一個 issue 問一下這個問題,結果是我誤會 mstatus.mpp 的功能,它並沒有辦法直接取得現在的 Mode , mstatus.mpp 會變成 0 是因為 `mret` 之後會自動把 mstatus.mpp 換成有支援的最低權限,這在 virt 上就是 U mode ,但這不代表它真的切換到 U mode 。
[linmo issue #19](https://github.com/sysprog21/linmo/issues/19)
:::
7. 恢復暫存器狀態,結束 interrupt
### 實作 round robin
[fcb542b](https://github.com/weiso131/Teddy-Cat-OS/commit/fcb542b248e8932c35e3492445a91d8cd2a41cc5)
最後我想到了一個奇怪的方法?由於要觸發 context switch 必然會先觸發 timer interrupt ,此時會保存與執行相關的所有暫存器,因此 context switch 只要交換 `sp` 和 `sepc` ,就可以搭配 `kernel_entry` 作到 context switch 與跳轉的效果。
:::info
todo:
處理一個 task 結束時的處理機制
:::
#### 對於 C 嵌入式組語語法不熟悉
因為這個卡了很久之一
:::info
Todo:
補語法知識
:::
```c=
void switch_context(struct task *current, struct task *next)
{
__asm__ __volatile__(
"csrr t0, sepc\n"
"mv %0, t0\n"
"mv %1, sp\n"
"csrw sepc, %2\n"
"mv sp, %3\n"
: "=&r" (current->sepc), "=&r" (current->sp)
: "r" (next->sepc), "r" (next->sp)
: "t0", "memory"
);
}
```
#### 對 stack 保存了什麼不清楚,以為自己恢復 ra
因為這個卡了很久之二
由於 `kernel_entry` -> `handler_trap` -> `switch_context` 有多層的函式呼叫, `ra` 被保存在上一個 process 的 stack ,新的 task 保存的 ra 會是非法的,因此要先保存 `handler_trap` 的 `ra` 再 `switch_context` ,並且在之後將其保存到 stack 中對應的位置。
#### 問題
目前會在 process 初始化時,預留了 `handler_trap` 所造成的偏移
每當修改 `handler_trap` 都需要更改其值,可以思考有什麼方法可以避免這件事
```c
task->sp = (uintptr_t)task->kernel_stack + (1 << 12) - 4 * 31 - 16;//16 is stack pointer offset of handle_trap
```
:::info
在 switch_context 直接把 sp 換成進入函式之前的 sp ,然後手動恢復 `ra` ,之後直接 `ret` ,嘗試迴避 `return` 所造成的多餘暫存器恢復指令
llvm-objdump
```
802000a4: aa 80 mv ra, a0
802000a6: 82 80 ret
802000a8: f2 40 lw ra, 0x1c(sp)
802000aa: 62 44 lw s0, 0x18(sp)
802000ac: d2 44 lw s1, 0x14(sp)
802000ae: 42 49 lw s2, 0x10(sp)
802000b0: b2 49 lw s3, 0xc(sp)
802000b2: 05 61 addi sp, sp, 0x20
802000b4: 82 80 ret
```
編譯器會強制在尾巴加上暫存器復原,但前面已經 `ret` 了,後面那段根本不會執行到,雖然這增加了不必要的指令,但增加程式的可維護性
:::
## 使用者模式
### 應用程式
#### OS in 1000 lines
- 為了簡化,將編譯好的 `.elf` 轉成 `.bin` ,再轉成 `.bin.o`
:::info
`.bin.o` 是可以直接把它當 object file 的意思?
:::
- `start` 的功能包含初始化 `sp` , 並且跳轉到 `main`
- `"user.c"` 定義了一些 lib 的功能,但同時定義了 `start` , `start` 應該與 lib 功能用不同檔案定義
- `llvm-nm` ,功能與 `nm` (參考 [Linking 筆記](https://hackmd.io/wZESKJXfSuW5SCaVNDX4Qw?view))類似,但輸出似乎更簡潔?總之就是查看 `.o` 檔有哪些 symbol
:::info
os in 1000 lines 會直接把這個 symbol 寫死(`extern`)在 kernel.c ,之後分配空間會用到,但這會導致如果要用其他應用程式就得改程式,之後應該可以調整,讓它至少先能在編譯時期決定哪個檔案當使用者程式
:::
### 切換到 U mode
#### OS in 1000 lines
- 利用 `.bin.o` 的 symbol ,算出所需的空間大小,分配一些 page 儲存
- 與 kernel 一起編譯,這個使用者程式實際上會被放在 .data 裡面,可以由 `kernel.map` 得知,至於為什麼還是得複製,這是因為需要複製一段 U mode 能用的 page
- `ret` 到 `user_entry` ,再設定 `sepc` (設成 `_binary_shell_bin_start` )與 `sstatus` , `sret` 到 U mode 並執行應用程式
:::info
根據 The RISC-V Instruction Set Manual Volume II: Privileged Architecture 4.1.1 Supervisor Status Register (sstatus)
>When an SRET instruction (see Section 3.3.2) is executed to return from the trap handler, the privilege level is set to user mode if the SPP bit is 0, or supervisor mode if the SPP bit is 1; SPP is then set to 0.
`sret` 會跑到 U mode 是因為他把 `sstatus` 的 `SPP` 設成 0 了(他直接把 `sstatus` 設成 `1 << 5`) ,但文中沒有特別提到,不確定是不是刻意省略。
[issue #106](https://github.com/nuta/operating-system-in-1000-lines/issues/106)
:::
#### Teddy cat OS
>[9b5b7ae](https://github.com/weiso131/Teddy-Cat-OS/commit/9b5b7ae57df0cdfd86f7ce34575eeda20486e1d0)
目前的作法與 OS in 1000 lines 沒有太大差異,主要也只是差在想保留 kernel process 的功能。
然而,就當我以為一切順利時,意外發生了
```
PANIC: arch/riscv32/trap.c:29: unexpected trap scause=0000000f, stval=84232000, sepc=8020011a
```
觀察, `sepc` 對應到的是 `kernel_entry` 的某一行指令,然後此時 `sp` 跑去 `0x84231f58` ,檢查 `user_stack_pointer` 也設置正確。後來藉由實驗,推測是因為 `kernel_entry` 不斷存取到非法的虛擬記憶體,造成 page fault ,導致 stack overflow 到了一個合法的區塊才真正開始執行 exception handler 。詢問 chatGPT 它告訴我 riscv 預設 S mode 不能存取 U mode 的虛擬記憶體。對應 `The RISC-V Instruction Set Manual Volume II: Privileged Architecture`
>The U bit indicates whether the page is accessible to user mode. U-mode software may only access the page when U=1. If the SUM bit in the sstatus register is set, supervisor mode software may also access pages with U=1.
看來它沒騙我
結合上述文件,出問題的部份是 `sw reg, ?(sp)` 的部份,因為 `SUM` 沒打開。然而,對於上述情形,比較安全的解法,其實是在 `kernel_entry` 的部份下手,將 user sp 換成 kernel sp,因為我們無法確保 User 做了什麼事情,若它把 stack 用到接近 overflow , 此時 kernel_entry 再使用 stack 就會造成 overflow (類似前面遇到的問題) , OS in 1000 lines 有做相關的處理,但我不能直接使用該方法,因為我的排程是搶佔排程。考慮到目前的 user_app 不會有大量的 stack 使用,可以先啟用 `SUM` 來允許核心存取 user stack
在 Commit 9b5b7ae 之後,我才發現我的 start_task 沒有改,這導致如果我第一個執行的任務是 user task 會壞掉
>[5b5059d](https://github.com/weiso131/Teddy-Cat-OS/commit/5b5059d816d82fb6add4f17a2661d9fc5fd8a629)
過程中有個很蠢的 bug ,在我其實已經完成正確的版本時,沒有加在 `handle_trap` 的開頭的 printf 刪除,它不斷的印出同樣的訊息,讓我以為我還有 bug 沒修掉。
後來才發現,這是因為函式呼叫結束不會把前一層的 `ra` 復原(這是在 `return` 時讀取 sp 儲存的值來復原),這導致下一行讀取 `ra` 讀到錯的值,與後面的 `ret` 組合成無限的循環。
>[7194cef](https://github.com/weiso131/Teddy-Cat-OS/commit/7194cef73ef5cca299313b47b3185e137a90a752)
這個 commit 實現了讓 trap 總是使用 kernel stack 的功能
主要是靠著 sscratch 的交換實現的
在 TeddyCat OS 中,sscratch 的運作方式如下:
- 當執行使用者任務時,sscratch 會儲存該使用者任務的核心堆疊指標 (SP)。
- 當使用者任務進入 trap 處理時,sscratch 會儲存使用者任務執行時所使用的 SP。
- 對於核心任務,sscratch 總是被設為 0。
在 trap 進入時,kernel_entry 會透過 bne 檢查 sscratch:
- sscratch != 0 → 從使用者堆疊切換到核心堆疊
- sscratch == 0 → 已經在核心堆疊上,換回來後無事發生
也因為這個設定, `kernel_entry` 的 `csrw a0, sscratch` ,並儲存 a0 ,來復原 sp 這件事情變得無效,因為 `sscratch` 在 kernel task 會儲存 0 ,這會導致換回來變成無效的 sp 位址,在 user task 儲存 user sp ,會導致破壞總是使用 kernel stack 的特性
## 系統呼叫
### ecall
>[5f9ee7b](https://github.com/weiso131/Teddy-Cat-OS/commit/5f9ee7ba70a9b622c3d99bbc1f05ebcbcdf253ae#diff-81ba3f5532541679598c64ffec8ef2372c69db422206ea2563d4310910b9d7f0)
ECALL 指令用於向支援的執行環境發出請求。
當在 U-mode、S-mode 或 M-mode 下執行時,會分別產生:
- 來自 U-mode 的環境呼叫例外(environment-call-from-U-mode exception)
- 來自 S-mode 的環境呼叫例外(environment-call-from-S-mode exception)
- 來自 M-mode 的環境呼叫例外(environment-call-from-M-mode exception)
:::info
一個小雷, `char` 不保證是有號的
[todo: 把這裡的內容展開](https://chatgpt.com/c/68b5cb51-0544-8328-9d6d-540636374fa7)
:::