逐步掌握以下过程:
源码是如何被编译成可执行文件的。
编译成可执行文件后,计算机如何加载操作系统。
加载以后,该从哪里去运行操作系统。
操作系统的输出信息是怎么输出的呢。
QEMU模拟器提供一个RISC-V的CPU和物理内存以及总线的通信功能。
此时,需要将硬盘上的操作系统内核加载到内存中,以便操作系统的执行。
完成这项工作的是bootloader
,它负责开机(boot)以及将操作系统加载到内核(load)。在QEMU中提供了固件OpenSBI完成这项工作。
整个内核的入口点是kern/init/entry.S
#include
#include
.section .text,"ax",%progbits
.globl kern_entry
kern_entry:
la sp, bootstacktop //la,load address,即将返回地址保存在sp寄存器中
tail kern_init //tail是tail call的缩写是RISC-V的一条伪指令,相当于函数调用
.section .data
# .align 2^12
.align PGSHIFT
.global bootstack
bootstack:
.space KSTACKSIZE
.global bootstacktop
bootstacktop:
在内核的入口点处调用了唯一的函数kern_init
int kern_init(void) __attribute__((noreturn));
int kern_init(void) {
extern char edata[], end[];
memset(edata, 0, end - edata);
const char *message = "(THU.CST) os is loading ...\n";
cprintf("%s\n\n", message);
while (1);
}
这个函数打印了一个字符串"(THU.CST) os is loading …\n"之后就进入了循环阶段,打印这个字符串的函数是cprintf
,之所以不选择直接使用printf函数输出,是因为C语言的标准库函数依赖glibc
提供的运行时环境。
首先通过系统调用指令ecall
(environment call)来实现打印字符串。但是C语言不能直接写汇编代码需要在程序中加入__asm__ volatile
关键字将实现内联汇编。
// libs/sbi.c
#include
#include
//SBI编号与函数对应
//编号0-8由OpenSBI处理,否则交给中断处理程序
uint64_t SBI_SET_TIMER = 0;
uint64_t SBI_CONSOLE_PUTCHAR = 1;
uint64_t SBI_CONSOLE_GETCHAR = 2;
uint64_t SBI_CLEAR_IPI = 3;
uint64_t SBI_SEND_IPI = 4;
uint64_t SBI_REMOTE_FENCE_I = 5;
uint64_t SBI_REMOTE_SFENCE_VMA = 6;
uint64_t SBI_REMOTE_SFENCE_VMA_ASID = 7;
uint64_t SBI_SHUTDOWN = 8;
//核心函数
uint64_t sbi_call(uint64_t sbi_type, uint64_t arg0, uint64_t arg1, uint64_t arg2) {
uint64_t ret_val;
__asm__ volatile (
"mv x17, %[sbi_type]\n"
"mv x10, %[arg0]\n"
"mv x11, %[arg1]\n"
"mv x12, %[arg2]\n" //把参数的值放入寄存器
"ecall\n" //通过ecall交给OpenSBI执行
"mv %[ret_val], x10"
//OpenSBI按照riscv的calling convention,把返回值放到x10寄存器里
: [ret_val] "=r" (ret_val)
: [sbi_type] "r" (sbi_type), [arg0] "r" (arg0), [arg1] "r" (arg1), [arg2] "r" (arg2)
: "memory"
);
return ret_val;
}
void sbi_console_putchar(unsigned char ch) {
sbi_call(SBI_CONSOLE_PUTCHAR, ch, 0, 0);
}
void sbi_set_timer(unsigned long long stime_value) {
sbi_call(SBI_SET_TIMER, stime_value, 0, 0);
}
核心函数sbi_call
的基本用法就是把各个参数保存在指定的寄存器中,然后使用ecall指令,让OpenSBI执行对应系统调用编号的功能。
然后用sbi_console_putchar
将sbi_call
这个函数的字符打印功能做了一个简单的封装。
然后逐层封装,直至产生一个和printf功能类似的函数cprintf在kern/libs/stdio.c