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