009-彩色的顯示器

       上次講到了引用C語言開發的原因以及如何使用C語言與之前的彙編頭結合,這次,正式開始C語言的開發。

       由於我們已經指定了C語言編寫的入口函數爲OSMain(),那麼,就從這個函數開始寫吧。既然要寫一個函數,我們首先得確定這個函數的參數和返回值類型。如何確定呢?那得看誰會來調用這個函數,又想從這個函數裏獲得點什麼。由於我們是將編譯好的這個程序和之前寫的彙編頭相拼接的,那麼,調用這個函數的一定就是我們之前寫的彙編指令了,更準確的來說,是一條JMP語句。但是,彙編語言之間的函數參數傳遞都是通過寄存器來實現的,因爲對於彙編語言來說,不存在什麼局部變量,所有的程序公用寄存器和內存,所有,它們之間可以隨意使用,自然就可以很順暢的傳遞參數。但是這一點在C語言不可以,由於C語言在編譯時會給內存來劃分相對應的堆區棧區,函數之間的數據傳遞一定要通過接口,這裏的接口就體現爲參數和返回值。我們在寫一個純C程序時不必考慮這些問題,因爲只要提供好接口,並且合適地調用就萬事大吉了,至於接口處數據如何傳遞,這是編譯器的事。但是現在不行,現在我們的C函數是要用彙編語言來調用的,我們就必須明白這些參數到底是存放在了哪裏。

       我們可以寫一段簡單的C程序,通過反彙編來查看裏面的數據傳遞邏輯。通過研究我們可以知道,C語言會在函數調用的時候,將函數指針傳入esp寄存器中,而在實際內存中,函數指針的後面緊跟着的就是參數的指針,由於我們在32位環境下編寫代碼,所有的指針都應該是32位的,也就是4字節,所以,esp+4, esp+8……就分別是第一個,第二個……參數的指針。寫過變參函數的程序員都知道,變參函數中函數參數的類型是未知的,個數也是無限制的,之所以會這樣就是由於C語言函數將參數依次放在了內存中,當你寫一個普通函數的時候,我們通過函數指針類型可以知道這個範圍到哪裏,所以不允許參數越界(這完全是編譯器的限制),但是當你寫一個變參函數的時候,這個限制就被放開,自然也就無法確定參數個數了。那麼,函數的返回值會放在哪裏呢?會放在EAX中(當然,這會根據你實際聲明的變量類型來決定究竟是讀取EAX還是AX還是AL)。所以總結一下就是,當我們在C語言中調用一個函數表達式時,編譯器要做的事情是這樣的,首先,根據函數指針找到函數對應的內存位置,並且把這個地址放到esp中,其次,以esp爲基址依次向後讀取參數,然後執行函數主體,最後會讀取EAX中的值作爲函數返回值,替換函數調用在C語言表達式中的位置。這就是爲什麼如果你聲明瞭函數返回值,但是在函數中沒有寫return語句,它就會返回一個任意值的原因,因爲EAX中的值是隨機的。瞭解了這個以後再來考慮,OSMain函數的參數和返回值問題。我們在調用這個函數的時候,將需要的信息都保存在了內存的固定位置,因此只要知道這些位置就好,暫時還不需要通過參數傳遞(當然,有序組織起來直接傳參不妨視爲一個更好的方式,目前暫時擱置,以後在優化代碼的時候會重新考慮),而至於返回值,好的習慣是,在C語言主體程序結束後,給寄存器裏留下一個返回值,用於表示程序是否正常運行,如果非正常退出,可以給出錯誤碼,我們在之後還可以去進行相關處理。暫時還不需要特別關注這個錯誤碼,先留出一個接口就好,因此決定,OSMain函數是一個int返回值的無參數函數,並且設定正常退出的返回值爲0,這樣,程序框架就出來了,是我們非常熟悉的形式。

int OSMain() {

    return 0;
}

       接下來的任務就是在屏幕上顯示出內容來了。我們知道在進入OSMain函數之前,我們就已經開啓了VGA彩色顯示模式,這是一個320×200分辨率的256色。換句話說,我們的屏幕上現在有640,000個像素點,每個像素點你可以在256種顏色中選擇一種來填充,由此來繪製我們的界面。在VGA模式下,顯卡支持RGB配色,紅、綠、藍三種顏色的配比各有256個等級,通常情況下我們用兩位十六進制表示一個顏色分量的比例。例如純白色就表示爲#FFFFFF,純黃色就表示爲#00FFFF。但是這樣的話我們來算一下,所有能夠顯示得顏色應該有256×256×256=16,777,216種顏色,這顯然已經遠遠超出了256種顏色,因此,我們就需要從這一千六百多萬種顏色裏選出256種顏色作爲我們要使用的顏色。這256種顏色慢慢選,慢慢試,機械性的選美工作就讓大家蘿蔔青菜了。這裏爲了把操作系統界面差不多顯示出來,就只選擇了16種顏色,我們先來做一個16色的操作系統,至於其他顏色,以後慢慢加就好了。

       所以,我們選擇了16種配色,寫出了以下basicDisplay.h文件:

#ifndef basicDisplay_h
#define basicDisplay_h

#include "asmfunc.h"
#include "global.h"

#define PORTID_PALETTE 	        ((void *)0x03c8)
#define PORTID_RGB		((void *)0x03c9)
#define VGA_MEM_END		((void *)0xafa00)

typedef struct {
	unsigned char red, green, blue;
} colorRGB;
colorRGB colorRGBMake(const char r, const char g, const char b);

#define COLOR_RGB(rgb) (colorRGBMake(rgb))
#define BLACK		0x00, 0x00, 0x00
#define RED 		0xff, 0x00, 0x00
#define GREEN		0x00, 0xff, 0x00
#define YELLOW		0xff, 0xff, 0x00
#define BLUE		0x00, 0x00, 0xff
#define PERPLE		0xff, 0x00, 0xff
#define CYAN		0x00, 0xff, 0xff
#define WHITE		0xff, 0xff, 0xff
#define GRAY		0xc6, 0xc6, 0xc6
#define DARK_RED 	0x85, 0x00, 0x00
#define DARK_GREEN	0x00, 0x85, 0x00
#define DARK_YELLOW	0x85, 0x85, 0x00
#define DARK_BLUE	0x00, 0x00, 0x85
#define DARK_PERPLE	0x85, 0x00, 0x85
#define DARK_CYAN	0x00, 0x85, 0x85
#define DARK_GREY	0x85, 0x85, 0x85

enum {
	id_black = 0,
	id_red = 1,
	id_green = 2,
	id_yellow = 3,
	id_blue = 4,
	id_perple = 5,
	id_cyan = 6,
	id_white = 7,
	id_gray = 8,
	id_darkRed = 9,
	id_darkGreen = 10,
	id_darkYellow = 11,
	id_darkBlue = 12,
	id_darkPerple = 13,
	id_darkCyan = 14,
	id_darkGray = 15
};

void setPalette();
#endif


       解釋一下這個代碼,4-8行暫時略過,一會會解釋。第9行這裏定義了一個指針常量,指向顯存。在CPU的尋址空間中(目前是32位字長4GB尋址空間),絕大部分由內存條提供,但是還有一部分是由顯卡來提供的,這部分RAM的作用就是用於存儲關於顯示的數據,就叫做顯存。如果你使用獨立顯卡,那這部分存儲空間就在你的顯卡上,這時對於CPU來說,雖然你有內存和顯存這兩個物理起見,但由於它們都被接在了CPU的直接尋址線上,因此對於CPU來說它們是一樣的。(這裏我們考慮的是兩種物理設備的情況,而在真實的計算機設備上,通常不只這兩種,CPU還會將一根尋址線接到硬盤,通過虛擬內存技術將外存的一部分作爲內存來使用,從而提升內存空間,緩解內存壓力)這也就是當年量產了4GB內存條以後,大家都吵着浪費內存空間的原因,因爲CPU要去訪問顯存,自然就要有一部分內存永遠無法被訪問到了,大致上只能用到3.2GB-3.6GB左右。而如果你使用的是集成顯卡,那麼顯存就得讓內存來充當了,這時候雖然你可以訪問4GB的內存空間,但是你就必須劃分出來一部分內存空間來作爲顯存使用。但是無論你使用哪種具體的物理設備作爲顯存,它都在CPU的尋址空間中,因此我們在編寫程序的時候就不需要考慮這個問題了,我們只需要知道顯存的地址是多少就可以了。在早期黑白顯示的時候,對於屏幕上每一個像素只能有兩種狀態,亮或者滅,所以一個像素只需要一個二進制位的顯存就可以了,這個時候一個地址的顯存會對應屏幕上8個像素點的亮暗信息。(這裏順帶一提,計算機剛加電的時候,屏幕會被初始化爲80×25的文本模式,利用320KB的顯存可以顯示2000個字符。)不過我們由於進入了VGA模式,每一個像素點都需要表示一個8位色,所以,一個像素點就需要對應8位,也就是1字節的顯存。那麼我們有640,000個像素點,所以0xafa00-0xbf400的地址空間就是我們的顯存了,在每一個像素裏添一個顏色代號就可以顯示對應的顏色了。

      11-14行定義了RGB色結構體,並聲明瞭與之配套的結構體構造函數。16-32行用於配合RGB構造函數,定義了16種顏色。34-51行分別定義了這些顏色的代號。53行定義的函數,接下來就要重點進行說明。下面給出basicDisplay.c的代碼:

#include "basicDIsplay.h"

colorRGB colorRGBMake(const char r, const char g, const char b) {
	colorRGB temp;
	temp.red = r;
	temp.green = g;
	temp.blue = b;
	return temp;
}

void setColor(const int colorId, const colorRGB color) {
	outportB(PORTID_PALETTE, colorId);
	outportB(PORTID_RGB, color.red / 4);
	outportB(PORTID_RGB, color.green / 4);
	outportB(PORTID_RGB, color.blue / 4);
}

void setPalette() {
	int nEflags = loadEflags();
	cleanInterrupt();
	setColor(0, COLOR_RGB(BLACK));
	setColor(1, COLOR_RGB(RED));
	setColor(2, COLOR_RGB(GREEN));
	setColor(3, COLOR_RGB(YELLOW));
	setColor(4, COLOR_RGB(BLUE));
	setColor(5, COLOR_RGB(PERPLE));
	setColor(6, COLOR_RGB(CYAN));
	setColor(7, COLOR_RGB(WHITE));
	setColor(8, COLOR_RGB(GRAY));
	setColor(9, COLOR_RGB(DARK_RED));
	setColor(10, COLOR_RGB(DARK_GREEN));
	setColor(11, COLOR_RGB(DARK_YELLOW));
	setColor(12, COLOR_RGB(DARK_BLUE));
	setColor(13, COLOR_RGB(DARK_PERPLE));
	setColor(14, COLOR_RGB(DARK_CYAN));
	setColor(15, COLOR_RGB(DARK_GREY));
	storeEflags(nEflags);
	setInterrupt();
}

      
       3-9行用於創建結構體變量創建的構造函數,不再解釋。重點解釋11-16行的setColor()函數。我們剛纔說到,需要在很多顏色中選出256中顏色,對於我們這裏來說,就是要選出16中顏色,給它表上0-15號,這樣,你給顯存裏存放0-15中的數字的時候,顯卡就能知道到底把哪個顏色顯示出去了。那如此說來,就應該有一個“東西”,能夠讓我們寫一個表格(就有點類似於GDT和IDT那樣),裏面要放着色號和顏色的對應關係,我們把這個“東西”就叫做調色盤。那麼究竟什麼是調色盤,它其實是顯卡上的一個部件,類似於寄存器又類似於內存,這麼糾結是因爲它的存儲空間要比寄存器大得多,但是性質上來說又很像寄存器。具體的電子芯片結構的問題在這裏就沒有討論的意義的,總之這是顯卡中的一部分。那麼既然它不再內存中,我們也就不能通過尋址的方式來訪問裏面的數據了,因爲它並不在CPU的尋址空間當中。那麼如何訪問到它呢?CPU要想訪問內存之外的東西,就必須通過I/O總線,這是CPU與外界輸入輸出設備連接的接口,在I/O總線上的每個設備都有一個自己的編號,CPU正是通過這個設備編號找到對應的設備的。setColor()函數的任務就是向調色盤對應的編號發送數據。向0x03c8發送數據表示將要設定的顏色編號,然後再向0x03c9發送三次數據來分辨表示R、G、B的分量。這樣就設置好了一種顏色。爲什麼要除以4呢?這是因爲,雖然RGB用了6位十六進制表示一個顏色,照理說應該能夠表示16,777,216中顏色,但是其實VGA顯卡還支持不了這麼多中顏色,而是每種顏色分量只支持64種,也就是一共64×64×64=262,144種顏色,那麼這與真正的RGB色號的對應關係是什麼呢?就是每4個一跳,也就是說(0x00, 0x00, 0x00)表示#000000,(0x01, 0x01, 0x01)表示#040404, (0x02, 0x03, 0x40)表示#080CFF,以此類推,所以,我們把RGB的顏色分量除以4就可以得到實際的編碼。21-36行就是通過調用16次setClolor()函數來完成16種顏色的編選。

       接下來介紹剛纔沒有介紹到的代碼,比如19,20,37,38行代碼的含義以及outportB函數的由來。這些莫名其妙出現的函數是哪裏來的呢?這裏需要注意的是,由於我們在編寫非常底層的操作系統代碼,這是沒有任何集成開發環境給我們使用的,更不會有什麼函數庫給我們用,所有的一切都是我們自己寫上去的。所謂C語言編程,也不過是僅僅使用了C語言語法特性而已,那些我們所熟悉的標準C語言庫是一概沒有的,因爲誰也不會知道我們即將開發出的操作系統是什麼樣的,自然也就不會有與之相匹配的庫文件了。那麼,這些函數並不是庫函數,它一定是我們自己來實現的。outportB函數是想給I/O設備發送信息,這種功能C語言是做不到的,所以,得靠外部力量來實現。這個功能用彙編語言來寫非常容易,一句OUT指令就可以搞定,所以,我們創建了asmfunc.asm文件,專門用於寫C語言無法實現的函數的函數體。

[format "WCOFF"]		; 由於該文件編譯後還需要鏈接,所以聲明文件格式
[instrset "i486p"]
[bits 32]				; 生成32位模式機器碼

[file "asmfunc.asm"]	; 文件名
	global _halt		; 包含的函數
	global _cleanInterrupt, _setInterrupt
	global _inportB, _inportW, _inportD
	global _outportB, _outportW, _outportD
	global _loadEflags, _storeEflags
	global _loadGDTR, _loadIDTR
	global _preInterruptHandler21, _preInterruptHandler2c, _preInterruptHandler27
	extern _interruptHandler21, _interruptHandler2c, _interruptHandler27
	[section .text]			; 實際的函數
_halt:
	hlt
	ret

_cleanInterrupt:
	cli
	ret
	
_setInterrupt:
	sti
	ret
	
_inportB:
	mov edx, [esp + 4]
	mov eax, 0
	in al, dx
	ret
	
_inportW:
	mov edx, [esp + 4]
	mov eax, 0
	in ax, dx
	ret
	
_inportD:
	mov edx, [esp + 4]
	mov eax, 0
	in eax, dx
	ret
	
_outportB:
	mov edx, [esp + 4]
	mov al, [esp + 8]
	out dx, al
	ret
	
_outportW:
	mov edx, [esp + 4]
	mov ax, [esp + 8]
	out dx, ax
	ret
	
_outportD:
	mov edx, [esp + 4]
	mov eax, [esp + 8]
	out dx, eax
	ret
	
_loadEflags:
	pushfd
	pop eax
	ret
	
_storeEflags:
	mov eax, [esp + 4]
	push eax
	popfd
	ret
	
_loadGDTR:
	mov ax, [esp + 4]
	mov [esp + 6], ax
	lgdt [esp + 6]
;	mov eax, [esp + 8]
;	mov [esp + 6], eax
;	lgdt [esp + 4]
	ret
	
_loadIDTR:
	mov ax, [esp + 4]
	mov [esp + 6], ax
	lidt [esp + 6]
	ret
	
_preInterruptHandler21:
	push es
	push ds
	pushad
	mov eax, esp
	push eax
	mov ax, ss
	mov ds, ax
	mov es, ax
	call _interruptHandler21
	pop eax
	popad
	pop ds
	pop es
	iretd
	
_preInterruptHandler2c:
	push es
	push ds
	pushad
	mov eax, esp
	push eax
	mov ax, ss
	mov ds, ax
	mov es, ax
	call _interruptHandler2c
	pop eax
	popad
	pop ds
	pop es
	iretd
	
_preInterruptHandler27:
	push es
	push ds
	pushad
	mov eax, esp
	push eax
	mov ax, ss
	mov ds, ax
	mov es, ax
	call _interruptHandler27
	pop eax
	popad
	pop ds
	pop es
	iretd

       現在給出的這個文件裏有很多函數是以後要用的,先給出,以後還會來講解。這裏主要講解outportB函數。46行接收傳來的第一個參數,放入edx中,顯然這應該是32位的數據,47行接收傳來的第二個參數,放入al中,顯然這應該是一個8位的數據。在48行執行out指令,edx中的數據作爲設備編號,al中的數據作爲發送的數據。如此看來,給這個函數傳遞的參數,應該是一個指針(用於指向設備),還有一個數據(用於向設備發送),函數名中的B的意思就是發送的這個數據應該是一個字節的。由於最後沒有向eax裏保存什麼數據,所以說明這個函數不需要返回值。那麼我們由此就可以寫出它在C語言中的函數聲明:

void outportB(void *pPort, char data);

       這樣就解釋清楚了outportB()函數的來歷。下面給出完整的asmfunc.h文件:

#ifndef asmfunc_h
#define asmfunc_h

void halt();

void cleanInterrupt();
void setInterrupt();

char inportB(void *pPort);
short inportW(void *pPort);
long inportD(void *pPort);

void outportB(void *pPort, char data);
void outportW(void *pPort, short data);
void outPortD(void *pPort, long data);

long loadEflags();
void storeEflags(long Eflag);

void loadGDTR(long limit, void *addr);
void loadIDTR(long limit, void *addr);

void preInterruptHandler21();
void preInterruptHandler2c();
void preInterruptHandler27();

#endif

       這樣我們也能參照看出loadEflags(),storeEflags(nEflags),cleanInterrupt()和setInterrupt()函數的實現。這4個函數又是幹什麼的呢?之前介紹過CPU的中斷機制,中斷信號在什麼時候發過來是不知道的,在接收到中斷指令後CPU會停下手中的活轉去執行中斷,但是,就像之前進行模式轉換時候那樣,如果在某些重要步驟中發生了中斷,是不能夠直接去處理的,否則將會發生嚴重錯誤,我們把這樣的操作叫做原子操作,意思就是這些操作不可分,你必須一次性處理完畢,不能因爲中斷就把它拆開處理。設定調色盤的過程也是原子操作,所以需要在之前關閉中斷,之後再開啓中斷。cleanInterrupt()其實就是執行了CTI,setInterrupt()其實就是執行了STI,而另外那兩個函數的目的就在於,關閉中斷前保存一些數據,開啓中斷後再恢復,因爲有些中斷髮出的數據會在原子操作中被改掉,等再去執行中斷的時候就會發生錯誤,爲了保險起見,我們把當前的狀態先保存下來,等恢復中斷以後再恢復它。

       這樣,我們只需要在OSMain()函數中調用initPalette()函數就可以完成調色盤的初始化,接下來的任務就是給顯存裏寫東西了,但是如果你每一個像素點都專門寫一遍的話,那真的就類似了,所以我們略微封裝一下用於顯示各種界面的函數,比如說最容易想到的就是畫出一個矩形了。
       要想畫出一個矩形,我們應該瞭解矩形的位置以及大小。那麼,既然我們的研究單位是屏幕上的每一個像素點,所以,我們這裏的座標也好,長度也好,都是以像素爲單位。下面給出用於繪製圖形的代碼draw.h:

#ifndef draw_h
#define draw_h

#include "basicDisplay.h"

#define SCR_CENTER (posMake(bootInfo -> scr_x / 2, bootInfo -> scr_y / 2))

typedef struct {
	unsigned x, y;
} Position;
Position posMake(const unsigned x, const unsigned y);

typedef struct {
	unsigned x, y, height, width;
} Rect;
Rect rectMake(const unsigned x, const unsigned y, const unsigned width, const unsigned height);

void drawRect(Rect rect, unsigned char colorId);
#endif

        兩個結構體分別用於定義位置和矩形,當然有與之配套的結構體構造函數。而16行的函數用於繪製矩形,下面給出其實現文件draw.c:

#include "draw.h"

Position posMake(const unsigned x, const unsigned y) {
	Position temp;
	temp.x = x;
	temp.y = y;
	return temp;
}

Rect rectMake(const unsigned x, const unsigned y, const unsigned width, const unsigned height) {
	Rect temp;
	temp.x = x;
	temp.y = y;
	temp.height = height;
	temp.width = width;
	return temp;
}

void drawRect(Rect rect, unsigned char colorId) {
	int x, y;
	for (x = rect.x; x <= rect.x + rect.width; x++) {
		for (y = rect.y; y < rect.y + rect.height; y++) {
			int position = y * bootInfo -> scr_x + x;
			*(unsigned char *)(bootInfo -> vga_mem_strat + position) = colorId;
		}
	}
}


       兩個結構體構造函數不再解釋,62-71行的函數就是根據rect的信息,計算出需要更改的左邊點,然後把與之對應的顏色信息寫到相應的顯存中,非常簡單。有了這個工具以後,我們就可以繪製出一個差不多的圖形了,下面是initDesktop.h

#ifndef initDesktop_h
#define initDesktop_h

#include "basicDisplay.h"
#include "draw.h"

void initDesktop();

#endif


       一下是initDesktop.c:

#include "initDesktop.h"

void initDesktop() {
	setPalette();
	drawRect(rectMake(0, 0, bootInfo -> scr_x, bootInfo -> scr_y - 29), id_darkCyan);
	drawRect(rectMake(0, bootInfo -> scr_y - 28, bootInfo -> scr_x, 1), id_gray);
	drawRect(rectMake(0, bootInfo -> scr_y - 28, 1, 28), id_gray);
	drawRect(rectMake(0, bootInfo -> scr_y - 1, bootInfo -> scr_x, 1), id_gray);
	drawRect(rectMake(bootInfo -> scr_x - 2, bootInfo -> scr_y - 28, 1, 28), id_gray);
	drawRect(rectMake(1, bootInfo -> scr_y - 27, bootInfo -> scr_x - 3, 26), id_darkGray);
}


       其實就是繪製了一個看起來比較像桌面的界面而已,不再過多解釋。最後再OSMain()裏調用initDesktop()就大功告成。附上效果圖:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章