從零開始寫 OS 內核 -顯示與打印

系列目錄

kernel 的世界

接上一篇 加載並進入 kernel,咱們終於來到了kernel 的大門,本篇開始將正式展開 kernel 階段的工做。有一個好消息是咱們終於能夠開始以 C 語言爲主的編程,彷佛能夠告別彙編的汪洋大海了,不過彙編仍然會在後面用到,它們都是小規模地出現,但都處於十分重要的關鍵節點上。git

總的來講,kernel 的主要任務將包括如下幾個部分:shell

  • 創建完善的內存管理機制,這主要包括了 virtual memory,以及 heap / kmalloc 的實現;
  • 創建多任務管理系統,即 thread / process 的運行和管理;
  • 實現簡單的硬件驅動,主要是 diskkeyboard
  • 實現用戶態程序的加載和運行,提供系統調用(system call);

不過在開始以前,咱們須要作一些前期準備工做,其中很重要的一項就是屏幕顯示,畢竟總得能有些看得見摸得着的東西,才能讓咱們能持續得到一些正反饋,並且其中 print 相關的函數也是對後面的開發調試相當重要。因此本篇的主要內容就是對屏幕顯示的控制,以及打印 string 等功能的開發,相對而言沒什麼難度,輕鬆愉快。編程

VGA 顯示

按慣例,首先給出本篇的代碼,主要在 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);

詳細的代碼我不貼了,主要幾個步驟:

  • 拼出這個打印的字符的 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 架構,而且要在目前給出的編譯選項下才行得通。若是想要支持更多的平臺和編譯器,還須要作一些擴展。不過對於咱們的項目而言,它應該是徹底夠用的,畢竟這只是一個教學實踐用的系統,沒必要過於苛求這些。

相關文章
相關標籤/搜索