官术网_书友最值得收藏!

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文件。

主站蜘蛛池模板: 商洛市| 平和县| 应城市| 绥滨县| 类乌齐县| 忻州市| 济阳县| 夏津县| 太仓市| 扶余县| 依兰县| 桦甸市| 香格里拉县| 通化县| 社会| 通渭县| 广州市| 金乡县| 大安市| 上思县| 保德县| 白水县| 广昌县| 鸡西市| 姚安县| 宜宾县| 遂川县| 井研县| 辽中县| 石渠县| 宁津县| 浦县| 博湖县| 湖北省| 花莲县| 红桥区| 新乡市| 延安市| 精河县| 军事| 阿克陶县|