這系列的文章會記錄我嘗試做出一個 Linux OS kernel的過程,要主是依照 https://littleosbook.github.io/ 這系列的文章,加上自已踩下的坑,希望能幫助同樣有做一個作業系統夢想的各位。
來到第四章,我們開始學習如何讓 OS 跟硬體溝通。
通常 kernel 會以兩種方法來跟硬體作互動 :
1. memory-mapped I/O (記憶體對映輸入輸出)
2. I/O ports( 又叫 port-mapped I/O )
如果硬體使用的是 memory-mapped I/O 的話,我們可以把 data 寫到一個特別的記憶體位置,那硬體將會使用這個記憶體位置的資料,其中一個我們會用到的例子是使用 framebuffer。
如果硬體使用的是I/O ports 的話,那我們可以以 assembly 的 out
和 in
來跟硬體溝通。out
會需要到兩個參數 : 分別為 I/O port 的位置與要傳送的 data。in
則只需要用到 I/O port 位置這個參數,並回傳從硬體得到的 data。( 在螢幕上閃爍的游標就是硬體通過 I/O ports 控制 PC 的一個例子。)
接著,我們會實作在螢幕上顯示文字。
在 Linux 上的顯示事件主要是依賴於 framebuffer 來完成。framebuffer 是一個硬體裝置,它主要用於顯示記憶體中的視訊資料到螢幕上。( 詳細可參考 wiki: framebuffer )
在電腦上通過 framebuffer 顯示文字是以 memory-mapped I/O 方式完成的。它的 memory-mapped I/O 起始記憶體位置為 0x000B8000
。
這個記憶體以16位元方式儲存,不同位元分別儲存不同資訊 :
Bit: | 15 14 13 12 11 10 9 8 | 7 6 5 4 | 3 2 1 0 |
Content: | ASCII | FG | BG |
1. bit 0-3 : 以 4 個 bit 定義背景顏色
2. bit 4-7: 以 4 個 bit 定義前景顏色
3. bit 8-15: 以 8 個 bit 定義 ASCII 的字符。
假設我們現在要於 (0,0) 坐標中顯示 “A” 這個字符 (65 / 0x41),並有綠色前景 (2) 與深灰色背景 (8), 在 16 bits 中的表示為 0x4128
。而 Assembly 是 :
mov [0x000B8000], 0x4128
那如果是 (0,1) 坐標的話,就是 :
0x000B8000 + 16 = 0x000B8010
我們可以用 C 來對 framebuffer 進行操作,對 0x000B8000
的記憶體加上我們需要的值 :
char *fb = (char *) 0x000B8000; // 給字符指標指到0x000B8000
fb[0] = 'A'; // 把A(0x41)放到左邊 8 bit
fb[1] = 0x28; // 把綠色與深灰色放到右邊 8 bit
以下是把整個寫入 framebuffer 包起來的 function :
/** fb_write_cell:
* Writes a character with the given foreground and background to
* position i
* in the framebuffer.
*
* @param i The location in the framebuffer
* @param c The character
* @param fg The foreground color
* @param bg The background color
*/void fb_write_cell(unsigned int i, char c, unsigned char fg, unsigned char bg)
{
char *fb = (char *) 0x000B8000;
fb[i] = c;
fb[i + 1] = ((fg & 0x0F) << 4) | (bg & 0x0F);
}#define FB_GREEN 2
#define FB_DARK_GREY 8fb_write_cell(0, 'A', FB_GREEN, FB_DARK_GREY);
((fg & 0x0F) << 4)
是對 fg
與 0x0F
的值進行比較,再往後移 4 bit。即 :
0000 0010 & 0000 1111
=0000 0010
( 這樣可以清理左 4 bit 的值,且保留右 4bit 的值。)0000 0010 << 4
=0010 0000
( 把值往左移 4 bit)(bg & 0x0F)
=0000 1000 & 0000 1111
=0000 1000
((fg & 0x0F) << 4) | (bg & 0x0F)
=0010 0000 | 0000 1000
(即保留所有有值的 bit )0010 0000 | 0000 1000
=0010 1000
= 0x28 (hex)= 40(dec)
所以,當左邊 8 bit 的 0x41 跟右邊 8 bit 的 0x28合並起來,就會是 0x4128,也就是我們想要顯示於 framebuffer 的 前綠後黑的 “A”。
接著,我們會了解一下如何移動游標。
在 framebuffer 中,移動游標需要用到它的分別二個 I/O ports。
游標的位置是以 16 bit 的 int 來決定的,如 0 代表 (0,0)、1 代表 (0,1)、80 代表 (1,0)。( 因為 framebuffer 的總大小是 25 個 row,80 個 column,兩者都以0開始算,所以每81個 int 就會用完一個 column ,並進位一個 row。)
因為 16 bit 對 assembly 的 out
的位置參數來說超出大小,所以我們要分開兩次傳送位置,分別是頭 8 bit 再到後 8 bit。
framebuffer 的兩個 I/O ports,0x3D5
一個是接收 data ,0x3D4
一個是描述接收的 data 。
以下是 assembly 以位置 80 為例 :
out 0x3D4, 14 ; 14 告知 framebuffer 接下來要用頭 8 bit 的位置
out 0x3D5, 0x00 ; 傳 0x0050 到頭 8 bit 到 0x3D5out 0x3D4, 15 ; 15 告知 framebuffer 接下來要用後 8 bit 的位置
out 0x3D5, 0x50 ; 傳 0x0050 到後 8 bit 到 0x3D5
因為 assembly 的 out
不能直接由 C 來使用,所以我們最好把整個 assembly 包成一個 function,並開放出來 label 給 C 來 call (使用 cdecl)。
global outb ; make the label outb visible outside this file; outb - send a byte to an I/O port
; stack: [esp + 8] the data byte
; [esp + 4] the I/O port
; [esp ] return address
outb:
mov al, [esp + 8] ; move the data to be sent into the al
register
mov dx, [esp + 4] ; move the address of the I/O port into the
dx register
out dx, al ; send the data to the I/O port
ret ; return to the calling function
global outb
: 把outb
這 function 開放給外部使用。mov al, [esp + 8]
: 把 data 傳到 AL register,而 AL register 是 AX register 的一個細分,用於存儲後 8 bit 的資訊。相對的,還有 AH register,用於存儲前 8 bit 的資訊。AX register 是一個累加器 (accumulator) ,作用是註冊大部分的操作。[esp + 8]
: 因為 esp 指向記憶體堆疊的最上層 ( the top of memory address’s stack),而堆疊是由上往下成長的,所以[esp + 8]
是指 esp 的指標往下移 8 byte。mov dx, [esp + 4]
: 把 I/O port 的位置傳到 DX register。DX register 是一個 data register,主要用於乘除。- 這邊會用到
[esp + 8]
跟[esp + 4]
是因為我們在 subprogram 時會帶入多個參數,而在參數的傳遞上,cdecl (calling convention) 規定 function 中的參數是以反向傳入 stack 中,又因為我們將會傳入的參數都只有 4 byte ,因此,第一個參數位於[esp + 4]
,第二個參數位於[esp + 8]
。 out dx, al
: 最後把 AL 的數據傳到 I/O port。
我們先把上面的 function 存到 io.s
,並再創建一個 header io.h
:
#ifndef INCLUDE_IO_H
#define INCLUDE_IO_H/** outb:
* Sends the given data to the given I/O port. Defined in io.s
*
* @param port The I/O port to send the data to
* @param data The data to send to the I/O port
*/
void outb(unsigned short port, unsigned char data); #endif /* INCLUDE_IO_H */
以下是把移動游標包成 C function :
#include "io.h"/* The I/O ports */
#define FB_COMMAND_PORT 0x3D4
#define FB_DATA_PORT 0x3D5/* The I/O port commands */
#define FB_HIGH_BYTE_COMMAND 14
#define FB_LOW_BYTE_COMMAND 15/** fb_move_cursor:
* Moves the cursor of the framebuffer to the given position
*
* @param pos The new position of the cursor
*/
void fb_move_cursor(unsigned short pos)
{
outb(FB_COMMAND_PORT, FB_HIGH_BYTE_COMMAND);
outb(FB_DATA_PORT, ((pos >> 8) & 0x00FF));
outb(FB_COMMAND_PORT, FB_LOW_BYTE_COMMAND);
outb(FB_DATA_PORT, pos & 0x00FF);
}
到這邊我們先已經把移動游標從 Assembly 轉到 C 來操作。
下一章,我們繼續來了解第四章剩下的部分 — Serial Port :
自制一個 Linux 作業系統(6) — Output (Serial Port)