# 這幾個禮拜完成的作品 - 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`