# 這幾個禮拜完成的作品 - RISC-V處理器
## 前言
- 離開成大後到聯發科報到日之間有段空檔時間,本想要出國旅遊,無奈疫情肆虐,計畫趕不上變化,因此就想找點事情做做。一直以來我夢想著可以親手打造硬體、系統、及應用程式,讓整個架構動起來,但因為學校所學的只有一些很皮毛的知識,因此我花了好一段時間研讀CPU架構、系統(Kernel)如何跟硬體(Hardware)溝通以及系統(Kernel)如何提供應用程式(Application)服務。
- 以下整理了這作品所需知識
- [x] [CPU規格](https://en.wikipedia.org/wiki/RISC-V)
- [x] [RISC-V GNU toolchain](https://github.com/riscv/riscv-gnu-toolchain)
- [x] [IEEE-754](https://en.wikipedia.org/wiki/IEEE_754)
- [x] [bitwise operate](https://hackmd.io/@sysprog/c-bitwise)
- [x] [物件導向程式設計](https://ocw.nctu.edu.tw/course_detail-v.php?bgid=8&gid=0&nid=343)
- [x] [計算機結構](http://ocw.nthu.edu.tw/ocw/index.php?page=course&cid=76&)
- [x] 作業系統
- [x] 資料結構
- [x] 離散數學
- [x] [演算法](https://ocw.nctu.edu.tw/course_detail-v.php?bgid=8&gid=0&nid=493)
## 處理器實現
- 在這段時間我實作了64位元的RISC-V處理器,並實現[The RISC-V Instruction Set Manual, Volume I: User-Level ISA](https://content.riscv.org/wp-content/uploads/2017/05/riscv-spec-v2.2.pdf)上的IMACFD擴充。除此之外我還研讀了[The RISC-V Instruction Set Manual, Volume II: Privileged Architecture](https://content.riscv.org/wp-content/uploads/2017/05/riscv-privileged-v1.10.pdf),實現RISC-V內部的控制狀態暫存器(Control Status Register),並瞭解如何處理非預期的中斷及可預期的異常。
:::info
- **RISC-V擴充**: (規格書第一冊第116-119頁,這邊是[傳送門](https://content.riscv.org/wp-content/uploads/2017/05/riscv-spec-v2.2.pdf#page=116))
- **I**擴充為最基礎的指令集包含有R-type、I-type運算指令及load、store指令及branch、jump指令
- **M**擴充為整數乘法及除法運算實現
- **A**擴充為原子操作指令,這是在多執行緒系統裡必須大量依賴的指令
- **C**擴充是實現16位元長度的精簡化指令
- **F**擴充是單精度浮點數的相關指令
- **D**擴充為雙精度浮點數的相關指令
- **RISC-V特權**: (規格書第二冊第13頁,這邊是[傳送門](https://content.riscv.org/wp-content/uploads/2017/05/riscv-privileged-v1.10.pdf#page=13))
| 模式 (Mode) | 等級 (Level) | 縮寫 (Abbreviation)|
| :--: | :--: | :--: |
| 使用者模式 (User Mode) | 0 | U |
| 監督者模式 (Supervisor Mode) | 1 | S |
| 保留 (Reserved) | 2 | - |
| 機械模式 (Machine Mode) | 3 | M |
:::
## 架構與裝置
- 然而剛剛所說的都是CPU本體的實現,接著我還實現匯流排(BUS)以及連接到匯流排上的裝置包括了Boot ROM、SRAM、DRAM、Timer、Host-Target Interface而這些裝置都被[Memory mapped](https://en.wikipedia.org/wiki/Memory-mapped_I/O)到特定的地址上。
![Block Diagram](https://i.imgur.com/ytDhicI.png)
:::info
**Boot ROM**: 裡面存放的程式碼是CPU剛啟動時第一個會去執行的程式,CPU一開始會以M-mode進行一系列暫存器初始化、某些特定中斷或異常委派給S-mode處置、陷阱入口(trap entry)地址設定、分頁設定、最後轉跳到S-mode或是U-mode開始運行核心程式或應用程式。
**Timer**: 他會依照設定的時間給予CPU中斷訊號。(規格書第二冊第40-41頁,這邊是[傳送門](https://content.riscv.org/wp-content/uploads/2017/05/riscv-privileged-v1.10.pdf#page=40))
**Host-Target Interface (HTIF)**: 可以透過這個介面操作終端機。
:::
## 系統配置
- 系統上我也實現了虛擬記憶體(virtual memory)的佈局,我採用SV39的規格配置了核心頁(kernel page)及使用者頁(user page)。而在S-mode及U-mode這兩種模式下,因為我啟用了虛擬地址,因此CPU眼中的世界全都是virtual address。
![](https://i.imgur.com/w4MbVQo.png)
![](https://i.imgur.com/olmAlfO.png)
- 其中核心頁配置了2MB的空間,並且直接對應到實體地址上(這讓核心直接可以操作MMIO的裝置,如Timer、Host-Target Interface的控制暫存器都是被分配在這地址上),然而因為權限的關係,U-mode無法存取到核心頁的內容,這也就是為什麼任何應用程式的I/O操作都必須要使用系統呼叫(system call)才能完成。
- 而在使用者頁部分,我安排了SRAM的區塊當作配置使用者頁的空間(共計32個frames,一個page可放置到一個frame內,1 page = 4KB),使用者頁內容為應用程式的程式碼、數據等內容。當剛進入應用程式時,由於SRAM空空如也,必定會觸發指令分頁錯誤(Instruction Page Fault),這時就會由U-mode轉換為S-mode並跳到陷阱入口(trap entry)經過導引後,會去處理分頁錯誤問題(原始碼內的`void fault_handle()`就是分頁錯誤處理的函式),它的操作流程就是在分頁錯誤發生處對應的page table entry屬性設為使用者頁、可讀、可寫、可執行、已被訪問,並從free frames清單中隨機抽出一個frame將實體地址紀錄到page table entry內,並開始填充這個分頁的內容到frame內,接著返回U-mode繼續執行應用程式。
:::info
- **Address**: 在M-mode中所使用的地址皆為physical address,其餘S-mode及U-mode會根據是否啟用虛擬位址來決定。
- **SV39**: 這是一種分頁模式,它的虛擬地址長度為39位元,並可以對應到56位元的實體地址上。分頁模式還有SV32、SV48、SV57、SV64等。(規格書第二冊第67頁,這邊是[傳送門](https://content.riscv.org/wp-content/uploads/2017/05/riscv-privileged-v1.10.pdf#page=67))
- **stap**: 這是一個RISC-V控制狀態暫存器(Control Status Register),它決定了要使用何種規格的分頁模式,以及第一層的頁表(page table)的實體地址。(規格書第二冊第66-68頁,這邊是[傳送門](https://content.riscv.org/wp-content/uploads/2017/05/riscv-privileged-v1.10.pdf#page=66))
**分頁錯誤 (Page Fault)**: 這邊指的page fault皆為major page fault,另外還有minor page fault與invalid page fault就不一一介紹,可參考wiki的[page fault](https://en.wikipedia.org/wiki/Page_fault)。
- **Instruction Page Fault**: 當處理器要執行指令時,發現指令座落的page並未載入到記憶體中,或者是這個page並無執行權限(non-executable),或者是U-mode想執行核心頁的內容,又或是S-mode想去執行使用者頁的內容,都會觸發Instruction Page Fault。
- **Load Page Fault**: 當使用到整數暫存器load指令(lb、lh、lw、ld)、浮點數暫存器load指令(flw、fld)時,數據座落的page並未載入到記憶體中,或者是這個page並無讀取權限(non-readable),或者是U-mode想載入核心頁的內容,又或是S-mode想載入使用者頁的內容**卻沒有開啟控制狀態暫存器mstatus中的SUM (permit Supervisor User Memory access)**(規格書第二冊第32頁,這邊是[傳送門](https://content.riscv.org/wp-content/uploads/2017/05/riscv-privileged-v1.10.pdf#page=32)),都會觸發Load Page Fault。
- **Store Page Fault**: 當使用到整數暫存器store指令(sb、sh、sw、sd)、浮點數暫存器store指令(fsw、fsd)時,數據座落的page並未載入到記憶體中,**或者是page table entry中的dirty屬性為0**(也可以設計為此狀況由硬體直接修改dirty bit不觸發分頁錯誤),或者是這個page並無寫入權限(non-writeable),或者是U-mode想寫入核心頁的內容,又或是S-mode想寫入使用者頁的內容**卻沒有開啟控制狀態暫存器mstatus中的SUM (permit Supervisor User Memory access)**(規格書第二冊第32頁,這邊是[傳送門](https://content.riscv.org/wp-content/uploads/2017/05/riscv-privileged-v1.10.pdf#page=32)),都會觸發Store Page Fault。
- **RISC-V中斷(interrupt)及異常(exception)**: (規格書第二冊第45頁,這邊是[傳送門](https://content.riscv.org/wp-content/uploads/2017/05/riscv-privileged-v1.10.pdf#page=45))
| 中斷/異常 |編碼 | 描述 |
| :--: | :--: | :--: |
| 中斷 | 0 | 使用者軟體中斷 |
| 中斷 | 1 | 監督者軟體中斷 |
| 中斷 | 2 | 保留 |
| 中斷 | 3 | 機械軟體中斷 |
| 中斷 | 4 | 使用者時鐘中斷 |
| 中斷 | 5 | 監督者時鐘中斷 |
| 中斷 | 6 | 保留 |
| 中斷 | 7 | 機械時鐘中斷 |
| 中斷 | 8 | 使用者外部中斷 |
| 中斷 | 9 | 監督者外部中斷 |
| 中斷 | 10 | 保留 |
| 中斷 | 11 | 機械外部中斷 |
| 中斷 | >11 | 保留 |
| 異常 | 0 | 指定地址對齊錯誤 |
| 異常 | 1 | 指令訪問錯誤 |
| 異常 | 2 | 違法指令 |
| 異常 | 3 | 斷點 |
| 異常 | 4 | 載入地址對齊錯誤 |
| 異常 | 5 | 載入訪問錯誤 |
| 異常 | 6 | 儲存/原子指令地址對齊錯誤 |
| 異常 | 7 | 儲存/原子指令訪問錯誤 |
| 異常 | 8 | 來自使用者的環境呼叫 |
| 異常 | 9 | 來自監督者的環境呼叫 |
| 異常 | 10 | 保留 |
| 異常 | 11 | 來自機械的環境呼叫 |
| 異常 | 12 | 指令分頁錯誤 |
| 異常 | 13 | 載入分頁錯誤 |
| 異常 | 14 | 保留 |
| 異常 | 15 | 儲存分頁錯誤 |
| 異常 | >15 | 保留 |
- **指定地址對齊錯誤**: C擴充指令地址應對齊2的倍數,其它指令地址應對齊4的倍數。
- **指令訪問錯誤**: 可能是實體地址上並無儲存裝置,或是逾越控制狀態暫存器pmpcfg及pmpaddr限制監督者及使用者訪問的地址範圍。
- **違法指令**: 可能是使用非RISC-V所規範的指令,或是控制狀態暫存器misa並未開啟該指令的擴充別 (如使用mul指令,未開啟misa中的M擴充),或是使用浮點數指令未開啟控制狀態暫存器mstatus中的FS。(規格書第二冊第33頁,這邊是[傳送門](https://content.riscv.org/wp-content/uploads/2017/05/riscv-privileged-v1.10.pdf#page=33))
- **載入訪問錯誤**: 原因類似指令訪問錯誤。
:::
## 系統啟動
:::info
原始碼放在[`sim/prog10/`](https://github.com/yutongshen/RISC-V-Simulator/tree/master/sim/prog10)目錄內
:::
- 在處理器啟動後會進到入口點(_start),之後會跳來`handle_reset`作暫存器的初始化(159-189行)。後續有可能使用到浮點數運算,所以在192-193行啟動了mstatus的FS。在196-197行設定了M-mode的[堆疊指標(stack pointer)](https://www.youtube.com/watch?v=Q2sFmqvpBe0)(影片中的stack是由下而上堆疊,而我的佈局是由上而下堆疊,在stack地址之上保留了一塊空間用來存放當中斷/異常發生時要儲存的暫存器值)。在200-202設定M-mode的陷阱入口。在205行初始化虛擬記憶體。
- `setup.S`
```=157
handle_reset:
# initialize register
li x1, 0;
li x2, 0;
li x3, 0;
li x4, 0;
li x5, 0;
li x6, 0;
li x7, 0;
li x8, 0;
li x9, 0;
li x10, 0;
li x11, 0;
li x12, 0;
li x13, 0;
li x14, 0;
li x15, 0;
li x16, 0;
li x17, 0;
li x18, 0;
li x19, 0;
li x20, 0;
li x21, 0;
li x22, 0;
li x23, 0;
li x24, 0;
li x25, 0;
li x26, 0;
li x27, 0;
li x28, 0;
li x29, 0;
li x30, 0;
li x31, 0;
# set fs
li t0, MSTATUS_FS;
csrs mstatus, t0;
# set stack point
la sp, (STACK_TOP - TRAPFRAM_SIZE);
csrw mscratch, sp;
# set machine trap entry
la t0, trap_vector;
csrw mtvec, t0;
csrr t0, mtvec;
# Initialize virtual memory
j vm_boot;
```
- 在`void vm_boot()`中,我進行了使用者頁與核心頁的布置,並設定satp讓S-mode與U-mode在虛擬地址下運行,接著清點可被使用者頁配置的free frames。在92-93行是要設定合法存取範圍(限制虛擬地址最多為57位元),即超過這個範圍的存取將會造成異常。(規格書第二冊第54-58頁,這邊是[傳送門](https://content.riscv.org/wp-content/uploads/2017/05/riscv-privileged-v1.10.pdf#page=54))。在96-104行是要把一些異常處理委派給S-mode,因為核心主要都是運行在S-mode權限下。最後就進行S-mode的陷阱入口及堆疊指標設定,接著把權限轉換為U-mode並跳到應用程式入口。
- `vm.c`
```c=65
void vm_boot()
{
extern int _entry[];
// build user and kernel page
l1_pt[0] = ((pte_t) l1_user_pt >> PAGE_SHIFT << PTE_PPN_SHIFT) | PTE_V;
l1_user_pt[0] = ((pte_t) l2_user_pt >> PAGE_SHIFT << PTE_PPN_SHIFT) | PTE_V;
l1_pt[N_PTE - 1] =
((pte_t) l1_kernel_pt >> PAGE_SHIFT << PTE_PPN_SHIFT) | PTE_V;
l1_kernel_pt[N_PTE - 1] = ((pte_t) _entry >> PAGE_SHIFT << PTE_PPN_SHIFT) |
PTE_D | PTE_A | PTE_R | PTE_W | PTE_X | PTE_V;
// set CSR satp
uint64_t satp_val =
set_field((pte_t) &l1_pt >> PAGE_SHIFT, SATP_MODE, SATP_MODE_SV39);
write_csr(satp, satp_val);
// initialize free page node
for (uint32_t i = 0; i < N_PAGE; ++i) {
free_node[i].addr = FREE_PAGE_BASE + i * 0x1000;
free_node[i].next = (void *) pa2kva(free_node + i + 1);
}
free_node[N_PAGE - 1].next = 0;
free_node_head = (page_list_t *) pa2kva(free_node);
free_node_tail = (page_list_t *) pa2kva(free_node + N_PAGE - 1);
// set supervisor legal address range for longest virtual - sv57
write_csr(pmpaddr0, (1UL << (57 - 3)) - 1U);
write_csr(pmpcfg0, PMP_R | PMP_W | PMP_X | set_field(0, PMP_A, PMP_NAPOT));
// delegate exception for supervisor
write_csr(
medeleg,
(1 << CAUSE_MISALIGNED_FETCH) |
(1 << CAUSE_USER_ECALL) |
(1 << CAUSE_BREAKPOINT) |
(1 << CAUSE_INSTRUCTION_PAGE_FAULT) |
(1 << CAUSE_LOAD_PAGE_FAULT) |
(1 << CAUSE_STORE_PAGE_FAULT)
);
// set supervisor trap entry
write_csr(stvec, pa2kva(&trap_entry));
// set supervisor trapframe pointer
uint64_t scratch;
read_csr(mscratch, scratch);
write_csr(sscratch, pa2kva(scratch));
// switch to user mode
trapframe_t tf;
memset(&tf, 0, sizeof(tf));
tf.epc = pa2uva(&user_space);
pop_trapframe(&tf);
}
```
## 應用程式
:::info
原始碼放在[`sim/prog10/`](https://github.com/yutongshen/RISC-V-Simulator/tree/master/sim/prog10)目錄內
:::
- 應用程式簡單寫了小程式,並對它進行編譯得到機械碼。
- `main.c`
```c=
#include <stdio.h>
int main(int argc, char **argv)
{
printf("hello world!!!\n");
return 0;
}
```
- `main.log`
```=1057
00000000800023e8 <main>:
800023e8: fe010113 addi sp,sp,-32
800023ec: 00113c23 sd ra,24(sp)
800023f0: 00813823 sd s0,16(sp)
800023f4: 02010413 addi s0,sp,32
800023f8: 00050793 mv a5,a0
800023fc: feb43023 sd a1,-32(s0)
80002400: fef42623 sw a5,-20(s0)
80002404: 00001517 auipc a0,0x1
80002408: 3bc50513 addi a0,a0,956 # 800037c0 <entry+0x60>
8000240c: 318000ef jal ra,80002724 <puts>
80002410: 00000793 li a5,0
80002414: 00078513 mv a0,a5
80002418: 01813083 ld ra,24(sp)
8000241c: 01013403 ld s0,16(sp)
80002420: 02010113 addi sp,sp,32
80002424: 00008067 ret
```
```=548
800037c0: 68656c6c 6f20776f 726c6421 21210000 hello world!!!..
```
:::info
這邊看到編譯器很貼心的把printf換成了puts,對於效能的優化有大幅度的提升,畢竟printf的實作比起puts複雜一些
:::
- 觀察地址8000240c的機械碼是將`int printf(char *format, ...)`翻譯成`jal ra,80002724 <puts>`
- `main.log`
```=1280
0000000080002724 <puts>:
80002724: fd010113 addi sp,sp,-48
80002728: 02113423 sd ra,40(sp)
8000272c: 02813023 sd s0,32(sp)
80002730: 03010413 addi s0,sp,48
80002734: fca43c23 sd a0,-40(s0)
80002738: 00100793 li a5,1
8000273c: fef42623 sw a5,-20(s0)
80002740: fd843503 ld a0,-40(s0)
80002744: f79ff0ef jal ra,800026bc <_puts>
80002748: 00050793 mv a5,a0
8000274c: 00078713 mv a4,a5
80002750: fec42783 lw a5,-20(s0)
80002754: 00e787bb addw a5,a5,a4
80002758: fef42623 sw a5,-20(s0)
8000275c: 00a00513 li a0,10
80002760: f1dff0ef jal ra,8000267c <putchar>
80002764: fec42783 lw a5,-20(s0)
80002768: 00078513 mv a0,a5
8000276c: 02813083 ld ra,40(sp)
80002770: 02013403 ld s0,32(sp)
80002774: 03010113 addi sp,sp,48
80002778: 00008067 ret
```
- 在puts的function中又看到地址80002760的機械碼會呼叫到`int putchar(int ch)`,而`int putchar(int ch)`需要請求HTIF的協助,把要輸出的字印在終端機上,因此我實現了system call來完成輸出字的功能。所以僅僅只是不起眼的`printf`,卻會需要使用system call來完成。
- `main.log`
```=1234
000000008000267c <putchar>:
8000267c: fe010113 addi sp,sp,-32
80002680: 00113c23 sd ra,24(sp)
80002684: 00813823 sd s0,16(sp)
80002688: 02010413 addi s0,sp,32
8000268c: 00050793 mv a5,a0
80002690: fef42623 sw a5,-20(s0)
80002694: fec42783 lw a5,-20(s0)
80002698: 00078593 mv a1,a5
8000269c: 10100513 li a0,257
800026a0: f89ff0ef jal ra,80002628 <syscall>
800026a4: 00000793 li a5,0
800026a8: 00078513 mv a0,a5
800026ac: 01813083 ld ra,24(sp)
800026b0: 01013403 ld s0,16(sp)
800026b4: 02010113 addi sp,sp,32
800026b8: 00008067 ret
```
```=1209
0000000080002628 <syscall>:
80002628: fe010113 addi sp,sp,-32
8000262c: 00813c23 sd s0,24(sp)
80002630: 02010413 addi s0,sp,32
80002634: fea43423 sd a0,-24(s0)
80002638: feb43023 sd a1,-32(s0)
8000263c: 03051513 slli a0,a0,0x30
80002640: 00b56533 or a0,a0,a1
80002644: 00000073 ecall
80002648: 00000793 li a5,0
8000264c: 00078513 mv a0,a5
80002650: 01813403 ld s0,24(sp)
80002654: 02010113 addi sp,sp,32
80002658: 00008067 ret
```
:::info
**System call**: 我這邊實現的方法是U-mode先在a0暫存器中存放system call ID,a1暫存器中存放參數,接著使用ecall指令讓CPU自陷異常,使CPU由U-mode切換成S-mode去運行對應的處置
:::
- 實際執行一次,輸出結果如下
```
hello world!!!
#### print page table ####
PAGE TABLE 80006000
ID ADDR V R W X U A D
000 80007000 1 0 0 0 0 0 0
1ff 80009000 1 0 0 0 0 0 0
PAGE TABLE 80007000
ID ADDR V R W X U A D
000 80008000 1 0 0 0 0 0 0
PAGE TABLE 80008000
ID ADDR V R W X U A D
000 00010000 1 1 1 1 1 1 1
002 00011000 1 1 1 1 1 1 1
003 00013000 1 1 1 1 1 1 1
00b 00012000 1 1 1 1 1 1 1
PAGE TABLE 80009000
ID ADDR V R W X U A D
1ff 80000000 1 1 1 1 0 1 1
```
## 連結GITHUB
- 最後,剩下不到一周就要上班了,可能再來就沒有充裕的時間可以搞這些有的沒有的研究了,我把我的原始碼放在github上,有興趣的人可以看看,如果發現我有錯誤的地方也請多指教啦。
:::info
**My Repository**: https://github.com/yutongshen/RISC-V-Simulator
:::
## Authors
[Yu-Tong Shen](https://github.com/yutongshen/)
###### tags: `RISC-V`、`Processor`、`Kernel`