从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
只是简单地封装一下
// kern/driver/console.c
#include <sbi.h>
#include <console.h>
void cons_putc(int c) { sbi_console_putchar((unsigned char)c); }
stdio.c
里面实现了一些函数,注意我们已经实现了ucore版本的puts函数: cputs()
// kern/libs/stdio.c
#include <console.h>
#include <defs.h>
#include <stdio.h>
/* HIGH level console I/O */
/* *
* cputch - writes a single character @c to stdout, and it will
* increace the value of counter pointed by @cnt.
* */
static void cputch(int c, int *cnt) {
cons_putc(c);
(*cnt)++;
}
/* cputchar - writes a single character to stdout */
void cputchar(int c) { cons_putc(c); }
int cputs(const char *str) {
int cnt = 0;
char c;
while ((c = *str++) != '\0') {
cputch(c, &cnt);
}
cputch('\n', &cnt);
return cnt;
}
我们还在libs/printfmt.c
实现了一些复杂的格式化输入输出函数。最后得到的cprintf()
函数仍在kern/libs/stdio.c
定义,功能和C标准库的printf()
基本相同。
可能你注意到我们用到一个头文件defs.h
, 我们在里面定义了一些有用的宏和类型
// libs/defs.h
#ifndef __LIBS_DEFS_H__
#define __LIBS_DEFS_H__
...
/* Represents true-or-false values */
typedef int bool;
/* Explicitly-sized versions of integer types */
typedef char int8_t;
typedef unsigned char uint8_t;
typedef short int16_t;
typedef unsigned short uint16_t;
typedef int int32_t;
typedef unsigned int uint32_t;
typedef long long int64_t;
typedef unsigned long long uint64_t;
...
/* *
* Rounding operations (efficient when n is a power of 2)
* Round down to the nearest multiple of n
* */
#define ROUNDDOWN(a, n) ({ \
size_t __a = (size_t)(a); \
(typeof(a))(__a - __a % (n)); \
})
...
#endif
printfmt.c
还依赖一个头文件riscv.h
,这个头文件主要定义了若干和riscv架构相关的宏,尤其是将一些内联汇编的代码封装成宏,使得我们更方便地使用内联汇编来读写寄存器。当然这里我们还没有用到它的强大功能。
// libs/riscv.h
...
#define read_csr(reg) ({ unsigned long __tmp; \
asm volatile ("csrr %0, " #reg : "=r"(__tmp)); \
__tmp; })
//通过内联汇编包装了 csrr 指令为 read_csr() 宏
#define write_csr(reg, val) ({ \
if (__builtin_constant_p(val) && (unsigned long)(val) < 32) \
asm volatile ("csrw " #reg ", %0" :: "i"(val)); \
else \
asm volatile ("csrw " #reg ", %0" :: "r"(val)); })
...
到现在,我们已经看过了一个最小化的内核的各个部分,虽然一些部分没有逐行细读,但我们也知道它在做什么。但一直到现在我们还没进行过编译。下面就把它编译一下跑起来。
最后更新于
这有帮助吗?