共计 3106 个字符,预计需要花费 8 分钟才能阅读完成。
系列目录
- 序篇
- 筹备工作
- BIOS 启动到实模式
- GDT 与保护模式
- 虚拟内存初探
- 加载并进入 kernel
- 显示与打印
- GDT 和 IDT,中断解决
- 虚拟内存欠缺
- 实现堆和 malloc
- 创立第一个内核线程
- 多线程运行与切换
- 锁与多线程同步
- 过程的实现
- 进入用户态
- 一个简略的文件系统
- 加载可执行程序
- 零碎调用的实现
- 键盘驱动
- 运行 shell
kernel 的世界
接上一篇 加载并进入 kernel,咱们终于来到了 kernel 的大门,本篇开始将正式开展 kernel 阶段的工作。有一个好消息是咱们终于能够开始以 C 语言为主的编程,仿佛能够辞别汇编的汪洋大海了,不过汇编依然会在前面用到,它们都是小规模地呈现,但都处于非常重要的要害节点上。
总的来说,kernel 的次要工作将包含以下几个局部:
- 建设欠缺的内存管理机制,这次要包含了
virtual memory
,以及heap / kmalloc
的实现; - 建设多任务管理系统,即
thread / process
的运行和治理; - 实现简略的硬件驱动,次要是
disk
和keyboard
; - 实现用户态程序的加载和运行,提供零碎调用(
system call
);
不过在开始之前,咱们须要做一些后期筹备工作,其中很重要的一项就是屏幕显示,毕竟总得能有些看得见摸得着的货色,能力让咱们能继续取得一些正反馈,而且其中 print 相干的函数也是对前面的开发调试至关重要。所以本篇的次要内容就是对屏幕显示的管制,以及打印 string 等性能的开发,相对而言没什么难度,轻松愉快。
VGA 显示
按常规,首先给出本篇的代码,次要在 src/monitor/ 目录下。
咱们用到的是 VGA text mode,一种古老的显示模式,它的原理简略来说就是用 32KB
内存来管制一个 25 行 * 80 列
的屏幕终端。这 32KB 内存被映射到了哪里呢?
答案是低 1MB 内存的 0xB800 ~ 0xBFFF
这一段,咱们能够通过拜访并批改这一段内存的值来管制屏幕显示。
当然咱们曾经关上 paging
并进入了 kernel,低 1MB 的内存曾经被映射到了 0xC0000000
以上,所以咱们能够应用 0xC000B800 ~ 0xC000BFFF
来拜访,即图中深蓝色局部。
咱们在代码里定义了显示内存的地址:
// The VGA framebuffer starts at 0xB8000.
uint16* video_memory = (uint16*)0xC00B8000;
下面说了屏幕上有 25 * 80 = 2000 个字符,每个字符须要应用 2 个 byte 管制,这样一屏幕就是 4000 个 byte,所以 32 KB 能够包容大概 8 屏的内容。不过尽管有 8 屏幕的数据,咱们为了简略起见,只管制第一屏幕的数据,超出局部就不予显示,也不反对高低翻屏等性能。
要在屏幕上某处打印字符,就是去批改(0xC00B8000
+ 对应偏移量)的地位上的内存就能够了。
字符显示
在屏幕上,一个字符由 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);
具体的代码我不贴了,次要几个步骤:
- 拼出这个打印的字符的 2-bytes 示意;
- 在以后光标的地位上打印这个字符,其实就是把 2-bytes 赋值给相应地位的显示内存上;
- 滚动屏幕,如果需要的话(溢出了最初一行);
- 将光标挪动到下一个地位;
有了最根底的打印一个字符的性能,接下来就能够实现字符串,十进制,十六进制整数的打印等性能,这样 print 相干的函数就比拟丰盛了,能够满足咱们的很多须要,不过其中我认为最重要的一个函数还没有实现,那就是 printf
。
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 架构,并且要在目前给出的编译选项下才行得通。如果想要反对更多的平台和编译器,还须要做一些扩大。不过对于咱们的我的项目而言,它应该是齐全够用的,毕竟这只是一个教学实际用的零碎,不用过于奢求这些。