- RISC-V體系結構編程與實踐(第2版)
- 笨叔
- 3100字
- 2024-09-23 17:56:00
2.3 MySBI和BenOS基礎實驗代碼解析
本書大部分實驗代碼都是基于BenOS實現的。本書的實驗會從最簡單的裸機程序開始,逐步對其進行擴展和豐富,讓其具有進程調度、系統調用等現代操作系統的基本功能。
BenOS基礎實驗代碼包含MySBI和BenOS兩部分,其中MySBI是運行在M模式下的固件,為運行在S模式下的操作系統提供引導和統一的接口服務。BenOS基礎實驗代碼的結構如圖2.6所示。

圖2.6 BenOS基礎實驗代碼的結構
其中,sbi目錄包含MySBI的源文件,src目錄包含BenOS的源文件,include目錄包含BenOS和MySBI共用的頭文件。
2.3.1 MySBI基礎實驗代碼解析
本書的實驗并沒有采用業界流行的OpenSBI固件,而是從零開始編寫一個小型可用的SBI固件,以便從底層深入學習RISC-V體系結構。
系統上電后,RISC-V處理器運行在M模式下。通常SBI固件運行在M模式下,為運行在S模式下的操作系統提供引導服務以及SBI服務。不過本小節介紹的MySBI代碼僅僅提供引導服務,在后續的實驗中會逐步添加SBI服務。
MySBI本質上是一個裸機程序,因此先從鏈接腳本(Linker Script,LS)開始分析。
任何一種可執行程序(不論是.elf文件還是.exe文件)都是由代碼(.text)段、數據(.data)段、未初始化的數據(.bss)段等段(section)組成的。鏈接腳本最終會把大量編譯好的二進制文件(.o文件)綜合成二進制可執行文件,也就是把所有二進制文件鏈接到一個大文件中。這個大文件由總的.text/.data/.bss段描述。下面是MySBI中的一個鏈接腳本,名為sbi_linker.ld。
<benos/sbi/sbi_linker.ld>
1 OUTPUT_ARCH(riscv)
2 ENTRY(_start)
3
4 SECTIONS
5 {
6 INCLUDE "sbi/sbi_base.ld"
7 }
在第1行中,OUTPUT_ARCH說明這個鏈接腳本對應的處理器體系結構為RISC-V。
在第2行中,指定程序的入口地址為_start。
在第4行中,SECTIONS是鏈接腳本語法中的關鍵命令,用來描述輸出文件的內存布局。SECTIONS命令告訴鏈接腳本如何把輸入文件的段映射到輸出文件的各個段,如何將輸入段整合為輸出段,以及如何把輸出段放入虛擬存儲器地址(Virtual Memory Address,VMA)和加載存儲器地址(Load Memory Address,LMA)。
在第6行中,通過INCLUDE命令引入sbi/sbi_base.ld腳本。
<benos/sbi/sbi_base.ld>
1 /*
2 * 設置SBI的加載入口地址為0x8000 0000
3 */
4
5 . =0x80000000;
6
7 .text.boot : { *(.text.boot) }
8 .text : { *(.text) }
9 .rodata : { *(.rodata) }
10 .data : { *(.data) }
11 . = ALIGN(0x8);
12 bss_begin = .;
13 .bss : { *(.bss*) }
14 bss_end = .;
在第5行中,“.”非常關鍵,它代表當前位置計數器(Location Counter,LC),這里把.text段的鏈接地址設置為0x8000 0000,其中鏈接地址指的是加載地址(load address)。
在第7行中,輸出文件的.text.boot段由所有輸入文件(其中的“*”可理解為所有的.0文件,也就是二進制文件)的.text.boot段組成。
在第8行中,輸出文件的.text段由所有輸入文件的.text段組成。
在第9行中,輸出文件的.rodata段由所有輸入文件的.rodata段組成。
在第10行中,輸出文件的.data段由所有輸入文件的.data段組成。
在第11行中,設置對齊方式為按8字節對齊。
在第12~14行中,定義了一個.bss段。
因此,上述鏈接腳本定義了如下幾個段。
● .text.boot段:包含系統啟動時首先要執行的代碼,即把_start函數鏈接到0x8000 0000地址。
● .text段:代碼段。
● .rodata段:只讀數據段。
● .data段:數據段。
● .bss段:包含未初始化的全局變量和未初始化的局部靜態變量。
下面開始編寫啟動MySBI的匯編代碼,并將代碼保存為sbi_boot.S文件。
<benos/sbi/sbi_boot.S>
1 .section ".text.boot"
2
3 .globl _start
4 _start:
5 /*關閉M模式的所有中斷*/
6 csrw mie, zero
7
8 /*設置棧, 棧的大小為4 KB*/
9 la sp, stacks_start
10 li t0, 4096
11 add sp, sp, t0
12
13 /*跳轉到C語言的sbi_main()函數*/
14 tail sbi_main
15
16 .section .data
17 .align 12
18 .global stacks_start
19 stacks_start:
20 .skip 4096
啟動MySBI的匯編代碼不長,下面進行簡要分析。
在第1行中,把sbi_boot.S文件編譯、鏈接到.text.boot段。可以在鏈接腳本sbi_linker.ld中把.text.boot段鏈接到這個可執行文件的開頭,這樣程序執行時將從這個段開始。此時,處理器運行在M模式。
在第4行中,_start為程序的入口點。
在第6行中,關閉M模式的所有中斷。
在第9~11行中,初始化棧指針,為棧分配4 KB的空間。
在第14行中,跳轉到C語言的sbi_main()函數。
sbi_main.c源文件如下。
<benos/sbi/sbi_main.c>
1 #include "asm/csr.h"
2
3 #define FW_JUMP_ADDR 0x80200000
4
5 /*
6 * 運行在M模式,并且切換到S模式
7 */
8 void sbi_main(void)
9 {
10 unsigned long val;
11
12 /*設置跳轉模式為S模式 */
13 val = read_csr(mstatus);
14 val = INSERT_FIELD(val, MSTATUS_MPP, PRV_S);
15 val = INSERT_FIELD(val, MSTATUS_MPIE, 0);
16 write_csr(mstatus, val);
17
18 /*設置M模式的異常程序計數器,用于mret跳轉 */
19 write_csr(mepc, FW_JUMP_ADDR);
20 /*設置S模式的異常向量表入口地址*/
21 write_csr(stvec, FW_JUMP_ADDR);
22 /*關閉S模式的中斷*/
23 write_csr(sie, 0);
24 /*關閉S模式的頁表轉換*/
25 write_csr(satp, 0);
26
27 /*切換到S模式*/
28 asm volatile("mret");
29 }
調用sbi_main()函數的主要目的是把處理器模式從M模式切換到S模式,并跳轉到S模式的入口地址處。對于QEMU Virt實驗平臺來說,S模式的入口地址為0x8020 0000。
在第13~16行中,設置mstatus寄存器中的MPP字段為S模式,并把中斷使能保存位MPIE也清除。
在第19行中,當處理器陷入M模式時,mepc寄存器會記錄陷入時的異常地址。因此,這里設置M模式的跳轉地址為0x8020 0000,執行mret指令會跳轉到0x8020 0000地址處。
在第28行中,執行mret指令,完成模式切換。
2.3.2 BenOS基礎實驗代碼解析
本小節介紹BenOS的代碼體系結構,目前它只有串口輸出功能,類似于裸機程序。BenOS的鏈接腳本參見benos/src/linker.ld文件。
<benos/src/linker.ld>
1 SECTIONS
2 {
3 . =0x80200000,
4
5 .text.boot : { *(.text.boot) }
6 .text : { *(.text) }
7 .rodata : { *(.rodata) }
8 .data : { *(.data) }
9 . = ALIGN(0x8);
10 bss_begin = .;
11 .bss : { *(.bss*) }
12 bss_end = .;
13 }
上述鏈接腳本與benos/sbi/sbi_linker.ld文件類似,唯一的區別在于鏈接地址不一樣。BenOS的入口地址為0x8020 0000。
下面開始編寫啟動BenOS的匯編代碼,并將代碼保存為boot.S文件。
<benos/src/boot.S>
1 .section ".text.boot"
2
3 .globl _start
4 _start:
5 /*關閉中斷*/
6 csrw sie, zero
7
8 /*設置棧*/
9 la sp, stacks_start
10 li t0, 4096
11 add sp, sp, t0
12
13 call kernel_main
14
15 hang:
16 wfi
17 j hang
18
19 .section .data
20 .align 12
21 .global stacks_start
22 stacks_start:
23 .skip 4096
啟動BenOS的匯編代碼不長,下面進行簡要分析。
在第1行中,把boot.S文件編譯、鏈接到.text.boot段。可以在鏈接腳本link.ld中把.text.boot段鏈接到這個可執行文件的開頭,這樣程序執行時將從這個段開始。
在第4行中,_start為程序的入口點。此時,處理器模式運行在S模式。
在第6行中,屏蔽所有的中斷源。
在第9~11行中,初始化棧指針,為棧分配4 KB的空間。
在第13行中,跳轉到C語言的kernel_main()函數。
上述匯編代碼還是比較簡單的,只做了一件事情——設置棧,跳轉到C語言入口。
接下來,編寫C語言的kernel_main()函數。本實驗的目標是輸出一條歡迎語句,因此這個函數的實現比較簡單,將代碼保存為kernel.c文件。
<benos/src/kernel.c>
1 #include "uart.h"
2
3 void kernel_main(void)
4 {
5 uart_init();
6 uart_send_string("Welcome RISC-V!\r\n");
7
8 while (1) {
9 ;
10 }
11 }
上述代碼很簡單,主要操作是初始化串口和往串口里輸出歡迎語句。
接下來,實現簡單的串口驅動代碼。QEMU使用兼容16550規范的串口控制器。16550串口控制器內部的寄存器如表2.5所示,這些寄存器的偏移地址由芯片的A0~A2引腳確定。另外,預分頻寄存器的高/低字節與其他寄存器復用,可以通過線路控制寄存器(LCR)的DLAB字段加以區分。
表2.5 16550串口控制器內部的寄存器

下面是16550串口的初始化代碼。
<benos/src/uart.c>
1 static unsigned int uart16550_clock = 1843200; //串口時鐘
2 #define UART_DEFAULT_BAUD 115200
3
4 void uart_init(void)
5 {
6 unsigned int divisor = uart16550_clock / (16 * UART_DEFAULT_BAUD);
7
8 /*關閉中斷*/
9 writeb(0, UART_IER);
10
11 /*打開DLAB字段,以設置波特率分頻*/
12 writeb(0x80, UART_LCR);
13 writeb((unsigned char)divisor, UART_DLL);
14 writeb((unsigned char)(divisor >> 8), UART_DLM);
15
16 /*設置串口數據格式*/
17 writeb(0x3, UART_LCR);
18
19 /*使能FIFO緩沖區,清空FIFO緩沖區,設置14字節閾值*/
20 writeb(0xc7, UART_FCR);
21 }
上述代碼關閉中斷,設置串口的波特率分頻,設置串口數據格式(一個起始位、8個數據位及1個停止位),使能FIFO緩沖區,清空FIFO緩沖區。
接下來,用如下幾個函數來發送字符串。
<benos/src/uart.c>
1 void uart_send(char c)
2 {
3 while((readb(UART_LSR) & UART_LSR_EMPTY) == 0)
4 ;
5
6 writeb(c, UART_DAT);
7 }
8
9 void uart_send_string(char *str)
10 {
11 int i;
12
13 for (i = 0; str[i] != '\0'; i++)
14 uart_send((char) str[i]);
15 }
uart_send()函數用于在while循環中判斷是否有數據需要發送,這里只需要判斷UART_LSR寄存器上的發送移位寄存器即可。
接下來,編寫Makefile文件。
<benos/Makefile文件>
1 GNU ?= riscv64-linux-gnu
2
3 COPS += -save-temps=obj -g -O0 -Wall -nostdlib -nostdinc -Iinclude -mcmodel=medany
-mabi=lp64 -march=rv64imafd -fno-PIE -fomit-frame-pointer
4
5 board ?= qemu
6
7 ifeq ($(board), qemu)
8 COPS += -DCONFIG_BOARD_QEMU
9 else ifeq ($(board), nemu)
10 COPS += -DCONFIG_BOARD_NEMU
11 endif
12
13 ##############
14 # build benos
15 ##############
16 BUILD_DIR = build_src
17 SRC_DIR = src
18
19 all : clean benos.bin mysbi.bin benos_payload.bin
20
21 #檢查進程的冗余功能是否開啟
22 CMD_PREFIX_DEFAULT := @
23 ifeq ($(V), 1)
24 CMD_PREFIX :=
25 else
26 CMD_PREFIX := $(CMD_PREFIX_DEFAULT)
27 endif
28
29 clean :
30 rm -rf $(BUILD_DIR) $(SBI_BUILD_DIR) *.bin *.map *.elf
31
32 $(BUILD_DIR)/%_c.o: $(SRC_DIR)/%.c
33 $(CMD_PREFIX)mkdir -p $(BUILD_DIR); echo " CC $@" ; $(GNU)-gcc $(COPS) -c $< -o $@
34
35 $(BUILD_DIR)/%_s.o: $(SRC_DIR)/%.S
36 $(CMD_PREFIX)mkdir -p $(BUILD_DIR); echo " AS $@"; $(GNU)-gcc $(COPS) -c $< -o $@
37
38 C_FILES = $(wildcard $(SRC_DIR)/*.c)
39 ASM_FILES = $(wildcard $(SRC_DIR)/*.S)
40 OBJ_FILES = $(C_FILES:$(SRC_DIR)/%.c=$(BUILD_DIR)/%_c.o)
41 OBJ_FILES += $(ASM_FILES:$(SRC_DIR)/%.S=$(BUILD_DIR)/%_s.o)
42
43 DEP_FILES = $(OBJ_FILES:%.o=%.d)
44 -include $(DEP_FILES)
45
46 benos.bin: $(SRC_DIR)/linker.ld $(OBJ_FILES)
47 $(CMD_PREFIX)$(GNU)-ld -T $(SRC_DIR)/linker.ld -o $(BUILD_DIR)/benos.elf
$(OBJ_FILES) -Map benos.map; echo " LD $(BUILD_DIR)/benos.elf"
48 $(CMD_PREFIX)$(GNU)-objcopy $(BUILD_DIR)/benos.elf -O binary benos.bin;
echo " OBJCOPY benos.bin"
49 $(CMD_PREFIX)cp $(BUILD_DIR)/benos.elf benos.elf
50
51 ##############
52 # build SBI
53 ##############
54 # 此處省略,建議讀者查看本書配套源代碼
上述Makefile文件使用board變量來選擇支持NEMU或者QEMU。
GNU用來指定編譯器,這里使用riscv64-linux-gnu-gcc。
COPS用來在編譯C語言和匯編語言時指定編譯選項。
● -g:表示編譯時加入調試符號表等信息。
● -Wall:表示打開所有警告信息。
● -nostdlib:表示不連接系統的標準啟動文件和標準庫文件,只把指定的文件傳遞給鏈接器。這個選項常用于編譯內核、bootloader等程序,它們不需要標準啟動文件和標準庫文件。
● -nostdinc:表示不包含C語言標準庫的頭文件。
● -mcmodel=medany:目標代碼模型,主要表示符號地址的約束。編譯器可以利用這些約束生成更有效的代碼。RISC-V上主要有兩個選項。
◇ medlow:表示程序及符號必須介于絕對地址-2 GB和絕對地址+2 GB之間。
◇ medany:表示程序及符號能訪問PC ? 2 GB到PC + 2 GB這個地址空間。
● -mabi=lp64:表示支持的數據模型。
● -march=rv64imafdc:表示處理器的指令集。
● -fno-PIE:PIE(Position Independent Executables)表示與位置無關的可執行程序。在GCC中,“-fpic”與“-fPIE”類似,只不過“-fpic”適用于編譯動態庫,“-fPIE”適用于編譯可執行程序。
上述文件會編譯和鏈接兩個可執行的ELF文件——benos.elf和mysbi.elf。這些.elf文件包含調試信息,需使用objcopy命令把.elf文件轉換為可執行的二進制文件benos.bin和mysbi.bin文件。
2.3.3 合并BenOS和MySBI
NEMU運行環境要求使用一個完整的二進制可執行文件,即需要把benos.bin和mysbi.bin合并。可以利用鏈接腳本實現這個功能,代碼如下。
<benos/src/sbi_linker_payload.ld>
1 SECTIONS
2 {
3 INCLUDE "sbi/sbi_base.ld"
4
5 . = 0x80200000;
6
7 .payload :
8 {
9 PROVIDE(_payload_start = .);
10 *(.payload)
11 . = ALIGN(8);
12 PROVIDE(_payload_end = .);
13 }
14 }
在第3行中,同樣使用INCLUDE命令引入sbi/sbi_base.ld腳本。
在第5行中,把當前的鏈接地址設置為0x8020 0000。
在第7~13行中,新建一個名為.payload的段,這個段的起始地址為0x8020 0000,這個地址是BenOS的入口地址。
在sbi_payload.S匯編文件中使用.incbin偽指令把benos.bin二進制數據嵌入.payload段,完成合并工作。
<benos/sbi/sbi_payload.S>
1 .section .payload, "ax"
2 .globl payload_bin
3 payload_bin:
4 .incbin "benos.bin"
在Makefile文件中還需要使用LD命令進行鏈接,最后生成benos_payload.elf以及benos_ payload.bin文件。