本书大部分实验代码都是基于BenOS实现的。本书的实验会从最简单的裸机程序开始,逐步对其进行扩展和丰富,让其具有进程调度、系统调用等现代操作系统的基本功能。
BenOS基础实验代码包含MySBI和BenOS两部分,其中MySBI是运行在M模式下的固件,为运行在S模式下的操作系统提供引导和统一的接口服务。BenOS基础实验代码的结构如图2.6所示。
图2.6 BenOS基础实验代码的结构
其中,sbi目录包含MySBI的源文件,src目录包含BenOS的源文件,include目录包含BenOS和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指令,完成模式切换。
本小节介绍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:表示程序及符号必须介于绝对地址-2GB和绝对地址+2GB之间。
◇ medany:表示程序及符号能访问PC−2GB到PC+2GB这个地址空间。
● -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文件。
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文件。