2

从零开始写 OS 内核 - 显示与打印

 2 years ago
source link: https://segmentfault.com/a/1190000040179784
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

kernel 的世界

接上一篇 加载并进入 kernel,我们终于来到了kernel 的大门,本篇开始将正式展开 kernel 阶段的工作。有一个好消息是我们终于可以开始以 C 语言为主的编程,似乎可以告别汇编的汪洋大海了,不过汇编仍然会在后面用到,它们都是小规模地出现,但都处于十分重要的关键节点上。

总的来说,kernel 的主要任务将包括以下几个部分:

  • 建立完善的内存管理机制,这主要包括了 virtual memory,以及 heap / kmalloc 的实现;
  • 建立多任务管理系统,即 thread / process 的运行和管理;
  • 实现简单的硬件驱动,主要是 diskkeyboard
  • 实现用户态程序的加载和运行,提供系统调用(system call);

不过在开始之前,我们需要做一些前期准备工作,其中很重要的一项就是屏幕显示,毕竟总得能有些看得见摸得着的东西,才能让我们能持续获得一些正反馈,而且其中 print 相关的函数也是对后面的开发调试至关重要。所以本篇的主要内容就是对屏幕显示的控制,以及打印 string 等功能的开发,相对而言没什么难度,轻松愉快。

本篇的实现我主要是参考了之前推荐的 JamesM's kernel development tutorials,它讲的还是很清楚的,你也可以参考下。

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,它需要能支持多个模板参数:

void 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 架构,并且要在目前给出的编译选项下才行得通。如果想要支持更多的平台和编译器,还需要做一些扩展。不过对于我们的项目而言,它应该是完全够用的,毕竟这只是一个教学实践用的系统,不必过于苛求这些。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK