从SBI到stdio
OpenSBI作为运行在M态的软件(或者说固件), 提供了一些接口供我们编写内核的时候使用。
我们可以通过ecall指令(environment call)调用OpenSBI。通过寄存器传递给OpenSBI一个”调用编号“,如果编号在 0-8 之间,则由OpenSBI进行处理,否则交由我们自己的中断处理程序处理(暂未实现)。有时OpenSBI调用需要像函数调用一样传递参数,这里传递参数的方式也和函数调用一样,按照riscv的函数调用约定(calling convention)把参数放到寄存器里。可以阅读SBI的详细文档。
须知 ecall
ecall(environment call),当我们在 S 态执行这条指令时,会触发一个 ecall-from-s-mode-exception,从而进入 M 模式中的中断处理流程(如设置定时器等);当我们在 U 态执行这条指令时,会触发一个 ecall-from-u-mode-exception,从而进入 S 模式中的中断处理流程(常用来进行系统调用)。
C语言并不能直接调用ecall, 需要通过内联汇编来实现。
// libs/sbi.c
#include <sbi.h>
#include <defs.h>
//SBI编号和函数的对应
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;
//sbi_call函数是我们关注的核心
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" //mv操作把参数的数值放到寄存器里
"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); //注意这里ch隐式类型转换为int64_t
}
void sbi_set_timer(unsigned long long stime_value) {
sbi_call(SBI_SET_TIMER, stime_value, 0, 0);
}须知 函数调用与calling convention
我们知道,编译器将高级语言源代码翻译成汇编代码。对于汇编语言而言,在最简单的编程模型中,所能够利用的只有指令集中提供的指令、各通用寄存器、 CPU 的状态、内存资源。那么,在高级语言中,我们进行一次函数调用,编译器要做哪些工作利用汇编语言来实现这一功能呢?
显然并不是仅用一条指令跳转到被调用函数开头地址就行了。我们还需要考虑:
如何传递参数?
如何传递返回值?
如何保证函数返回后能从我们期望的位置继续执行?
等更多事项。通常编译器按照某种规范去翻译所有的函数调用,这种规范被称为 calling convention 。值得一提的是,为了实现函数调用,我们需要预先分配一块内存作为 调用栈 ,后面会看到调用栈在函数调用过程中极其重要。你也可以理解为什么第一章刚开始我们就要分配栈了。
这样我们就可以通过sbi_console_putchar()来输出一个字符。接下来我们要做的事情就像月饼包装,把它封了一层又一层。
console.c只是简单地封装一下
stdio.c里面实现了一些函数,注意我们已经实现了ucore版本的puts函数: cputs()
我们还在libs/printfmt.c实现了一些复杂的格式化输入输出函数。最后得到的cprintf()函数仍在kern/libs/stdio.c定义,功能和C标准库的printf()基本相同。
可能你注意到我们用到一个头文件defs.h, 我们在里面定义了一些有用的宏和类型
printfmt.c还依赖一个头文件riscv.h,这个头文件主要定义了若干和riscv架构相关的宏,尤其是将一些内联汇编的代码封装成宏,使得我们更方便地使用内联汇编来读写寄存器。当然这里我们还没有用到它的强大功能。
到现在,我们已经看过了一个最小化的内核的各个部分,虽然一些部分没有逐行细读,但我们也知道它在做什么。但一直到现在我们还没进行过编译。下面就把它编译一下跑起来。
最后更新于
这有帮助吗?