接上一篇 加载并进入 kernel,咱们终于来到了kernel 的大门,本篇开始将正式展开 kernel 阶段的工做。有一个好消息是咱们终于能够开始以 C 语言为主的编程,彷佛能够告别汇编的汪洋大海了,不过汇编仍然会在后面用到,它们都是小规模地出现,但都处于十分重要的关键节点上。git
总的来讲,kernel 的主要任务将包括如下几个部分:shell
virtual memory
,以及 heap / kmalloc
的实现;thread / process
的运行和管理;disk
和 keyboard
;system call
);不过在开始以前,咱们须要作一些前期准备工做,其中很重要的一项就是屏幕显示,毕竟总得能有些看得见摸得着的东西,才能让咱们能持续得到一些正反馈,并且其中 print 相关的函数也是对后面的开发调试相当重要。因此本篇的主要内容就是对屏幕显示的控制,以及打印 string 等功能的开发,相对而言没什么难度,轻松愉快。编程
按惯例,首先给出本篇的代码,主要在 src/monitor/ 目录下。segmentfault
咱们用到的是 VGA text mode,一种古老的显示模式,它的原理简单来讲就是用 32KB
内存来控制一个 25 行 * 80 列
的屏幕终端。这 32KB 内存被映射到了哪里呢?多线程
答案是低 1MB 内存的 0xB800 ~ 0xBFFF
这一段,咱们能够经过访问并修改这一段内存的值来控制屏幕显示。架构
固然咱们已经打开 paging
并进入了 kernel,低 1MB 的内存已经被映射到了 0xC0000000
以上,因此咱们可使用 0xC000B800 ~ 0xC000BFFF
来访问,即图中深蓝色部分。ide
咱们在代码里定义了显示内存的地址:函数
// The VGA framebuffer starts at 0xB8000. uint16* video_memory = (uint16*)0xC00B8000;
上面说了屏幕上有 25 * 80 = 2000 个字符,每一个字符须要使用 2 个 byte 控制,这样一屏幕就是 4000 个 byte,因此 32 KB 能够容纳大约 8 屏的内容。不过虽然有 8 屏幕的数据,咱们为了简单起见,只控制第一屏幕的数据,超出部分就不予显示,也不支持上下翻屏等功能。gitlab
要在屏幕上某处打印字符,就是去修改(0xC00B8000
+ 对应偏移量) 的位置上的内存就能够了。ui
在屏幕上,一个字符由 2 个 byte 控制,我直接贴 wiki 百科上的图了:
其中低 byte 存储了字符的 ASCII 值,高 byte 则控制颜色(包括前景色和背景色)和闪烁, 很是简单。
3 个 bit 能够显示 8 种颜色:
#define COLOR_BLACK 0 #define COLOR_BLUE 1 #define COLOR_GREEN 2 #define COLOR_CYAN 3 #define COLOR_RED 4 #define COLOR_FUCHSINE 5 #define COLOR_BROWN 6 #define COLOR_WHITE 7
前面再加上一个 bit 能够控制高亮或者普通,注意只有前景色是 4-bit 能够支持这个:
#define COLOR_LIGHT_BLACK 8 #define COLOR_LIGHT_BLUE 9 #define COLOR_LIGHT_GREEN 10 #define COLOR_LIGHT_CYAN 11 #define COLOR_LIGHT_RED 12 #define COLOR_LIGHT_FUCHSINE 13 #define COLOR_LIGHT_BROWN 14 #define COLOR_LIGHT_WHITE 15
除了字符外,屏幕上还有一个重要的角色就是光标,通常用来标记了当前所处的位置。但实际上光标位置和打印字符的位置彻底没有任何关系,你只要指定了坐标,能够在任何地方打印字符,而让光标在远处看寂寞。不过一般按照习惯,咱们老是让光标在下一个打印位置上闪烁。
因此代码里定义了光标的位置:
// Stores the cursor position. int16 cursor_x = 0; int16 cursor_y = 0;
更新光标位置,须要对几个硬件端口进行操做:
static void move_cursor_position() { // The screen is 80 characters wide. uint16 cursorLocation = cursor_y * 80 + cursor_x; // Tell the VGA board we are setting the high cursor byte. outb(0x3D4, 14); // Send the high cursor byte. outb(0x3D5, cursorLocation >> 8); // Tell the VGA board we are setting the low cursor byte. outb(0x3D4, 15); // Send the low cursor byte. outb(0x3D5, cursorLocation); }
outb
函数,以及它对应的 inb
函数,定义在 src/common/io.c 里,是操做端口用的函数。
下面咱们须要定义几个 print 功能的函数,最基础的固然是打印一个字符:
void monitor_write_char_with_color(char c, uint8 color);
详细的代码我不贴了,主要几个步骤:
有了最基础的打印一个字符的功能,接下来就能够实现字符串,十进制,十六进制整数的打印等功能,这样 print 相关的函数就比较丰富了,能够知足咱们的不少须要,不过其中我认为最重要的一个函数尚未实现,那就是 printf
。
就像 C 标准库里的 printf
,它须要能支持多个模板参数:
printf(char* str, ...);
那应该如何实现这样的函数?
其实我也不太清楚正确的作法应该是什么,这里只是介绍我我的的实现方式。这里关键就是须要能获取省略号部分的可变参数,而它们其实在 printf 函数调用时被压到了 stack 上:
所以,后面的可变参数起始位置就在 ebp + 12 的位置处。
void monitor_printf(char* str, ...) { void* ebp = get_ebp(); void* arg_ptr = ebp + 12; monitor_printf_args(str, arg_ptr); }
get_ebp 这个函数定义在了 src/common/util.S 中,很是简单:
[GLOBAL get_ebp] get_ebp: mov eax, ebp ret
其实还有一个更简单的方法就是用 char* str
的地址加 4,也能够获得后面参数的地址。
固然这个方法获取参数的方法其实并非严谨的,它彻底依赖于体系架构和编译器的行为。当前这个方案只适合于 32 位 x86 架构,而且要在目前给出的编译选项下才行得通。若是想要支持更多的平台和编译器,还须要作一些扩展。不过对于咱们的项目而言,它应该是彻底够用的,毕竟这只是一个教学实践用的系统,没必要过于苛求这些。