6 色號設定(harib01f)
好了,到現在為止我們的話題都是以C語言為中心的,但我們的目的不是為了掌握C語言,而是為了制作操作系統,操作系統中是不需要條紋圖案之類的。我們繼續來做操作系統吧。
可能大家馬上就想描繪一個操作系統模樣的畫面,但在此之前要先做一件事,那就是處理顏色問題。這次使用的是320× 200的8位顏色模式,色號使用8位(二進制)數,也就是只能使用0~255的數。我想熟悉電腦顏色的人都會知道,這是非常少的。一般說起指定顏色,都是用#ffffff一類的數。這就是RGB(紅綠藍)方式,用6位十六進制數,也就是24位(二進制)來指定顏色。8位數完全不夠。那么,該怎么指定#ffffff方式的顏色呢?
這個8位彩色模式,是由程序員隨意指定0~255的數字所對應的顏色的。比如說25號顏色對應#ffffff,26號顏色對應#123456等。這種方式就叫做調色板(palette)。
如果像現在這樣,程序員不做任何設定,0號顏色就是#000000,15號顏色就是#ffffff。其他號碼的顏色,筆者也不是很清楚,所以可以按照自己的喜好來設定并使用。
筆者通過制作OSAKA知道:要想描繪一個操作系統模樣的畫面,只要有以下這16種顏色就足夠了。所以這次我們也使用這16種顏色,并給它們編上號碼0-15。
#000000:黑 #00ffff:淺亮藍 #000084:暗藍 #ff0000:亮紅 #ffffff:白 #840084:暗紫 #00ff00:亮綠 #c6c6c6:亮灰 #008484:淺暗藍 #ffff00:亮黃 #840000:暗紅 #848484:暗灰 #0000ff:亮藍 #008400:暗綠 #ff00ff:亮紫 #848400:暗黃
所以我們要給bootpack.c添加很多代碼。
■■■■■
本次的bootpack.c
void io_hlt(void); void io_cli(void); void io_out8(int port, int data); int io_load_eflags(void); void io_store_eflags(int eflags); /*就算寫在同一個源文件里,如果想在定義前使用,還是必須事先聲明一下。*/ void init_palette(void); void set_palette(int start, int end, unsigned char *rgb); void HariMain(void) { int i; /* 聲明變量。變量i是32位整數型 */ char *p; /* 變量p是BYTE [...]用的地址 */ init_palette(); /* 設定調色板 */ p = (char *) 0xa0000; /* 指定地址 */ for (i = 0; i <= 0xffff; i++) { p[i] = i & 0x0f; } for (; ; ) { io_hlt(); } } void init_palette(void) { static unsigned char table_rgb[16 * 3] = { 0x00, 0x00, 0x00, /* 0:黑 */ 0xff, 0x00, 0x00, /* 1:亮紅 */ 0x00, 0xff, 0x00, /* 2:亮綠 */ 0xff, 0xff, 0x00, /* 3:亮黃 */ 0x00, 0x00, 0xff, /* 4:亮藍 */ 0xff, 0x00, 0xff, /* 5:亮紫 */ 0x00, 0xff, 0xff, /* 6:淺亮藍 */ 0xff, 0xff, 0xff, /* 7:白 */ 0xc6, 0xc6, 0xc6, /* 8:亮灰 */ 0x84, 0x00, 0x00, /* 9:暗紅 */ 0x00, 0x84, 0x00, /* 10:暗綠 */ 0x84, 0x84, 0x00, /* 11:暗黃 */ 0x00, 0x00, 0x84, /* 12:暗青 */ 0x84, 0x00, 0x84, /* 13:暗紫 */ 0x00, 0x84, 0x84, /* 14:淺暗藍 */ 0x84, 0x84, 0x84 /* 15:暗灰 */ }; set_palette(0, 15, table_rgb); return; /* C語言中的static char語句只能用于數據,相當于匯編中的DB指令 */ } void set_palette(int start, int end, unsigned char *rgb) { int i, eflags; eflags = io_load_eflags(); /* 記錄中斷許可標志的值*/ io_cli(); /* 將中斷許可標志置為0,禁止中斷 */ io_out8(0x03c8, start); for (i = start; i <= end; i++) { io_out8(0x03c9, rgb[0] / 4); io_out8(0x03c9, rgb[1] / 4); io_out8(0x03c9, rgb[2] / 4); rgb += 3; } io_store_eflags(eflags); /* 復原中斷許可標志 */ return; }
程序的頭部羅列了很多的外部函數名,這些函數必須在naskfunc.nas中寫。這有點麻煩,但也沒辦法。先跳過這一部分,我們來看看主函數HariMain。函數里只是增加了一行調用調色板置置的函數,變更并不是太大。我們接著往下看。
■■■■■
函數init_palette開頭一段以static開始的語句,雖然很長,但結果無非就是聲明了一個常數table_rgb。它太長了,有些晦澀難懂,所以我們來簡化一下。
void init_palette(void) { table_rgb的聲明; set_palette(0, 15, table_rgb); return; }
簡而言之,就是這些內容。除了聲明之外沒什么難點,所以我們僅僅解說聲明部分。
char a[3];
C語言中,如果這樣寫,那么a就成為了常數,以匯編的語言來講就是標志符。標志符的值當然就意味著地址。并且還準備了“RESB 3”。總結一下,上面的敘述就相當于匯編里的這個語句:
a: RESB 3
nask中RESB的內容能夠保證是0,但C語言中不能保證所以里面說不定含有某種垃圾數據。
■■■■■
另外,在這個聲明的后面加上 “= { … }”,還可以寫上數據的初始值。比如:
char a[3]= { 1,2,3 };
這與下面的內容基本等價。
char a[3]; a[0] = 1; a[1] = 2; a[2] = 3;
這里,a是表示最初地址的數字,也就是說它被認為是指針。
那么這次,應該代入的值共有16× 3=48個。筆者不希望大家做如此多的賦值語句。每次賦值都至少要消耗3個字節,這樣算下來光這些賦值語句就要花費將近150字節,這太不值了。
其實寫成下面這樣一般的DB形式,不就挺好嗎。
table_rgb: DB 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, …
只要48字節就夠了。所以說,就像在匯編語言中用DB指令代替RESB指令那樣,在C語言中也有類似的指示方法,那就是在聲明時加上static。這次我們也加上它。
下面來看unsigned。它的意思是:這里所處理的數據是BYTE(char)型,但它是沒有符號(sign)的數(0或者正整數)。
char型的變量有3種模式,分別是signed型、unsigned型和未指定型。signed型用于處理-128~127的整數。它雖然也能處理負數,擴大了處理范圍,很方便,但能夠處理的最大值卻減小了一半。unsigned型能夠處理0~255的整數。未指定型是指沒有特別指定時,可由編譯器決定是unsigned還是signed。
在這個程序里,多次出現了0xff這個數值,也就是255,我們想用它來表示最大亮度,如果它被誤解成負數(0xff會被誤解成-1)就麻煩了。雖然我們不清楚亮度比0還弱會是什么概念,但無論如何不能產生這種誤解。所以我們決定將這個數設定為unsigned。順便提一句,int和short也分signed和unsigned。……好了,關于init_palette的說明就到此為止。
■■■■■
下面要講的是C語言說明部分最后的函數set_palette。這個函數雖然很短,干的事兒可不少。首先讓我們仔細看看以下精簡之后的記述吧。
void set_palette(int start, int end, unsigned char *rgb) { int i; io_out8(0x03c8, start); for (i = start; i <= end; i++) { io_out8(0x03c9, rgb[0] / 4); io_out8(0x03c9, rgb[1] / 4); io_out8(0x03c9, rgb[2] / 4); rgb += 3; } return; }
程序被如此精簡后還可以正確運行。其實可以在一開始就介紹這個程序,但由于想給大家介紹精簡之前的正確方法,所以才寫了那么長。這個先放一邊,我們來說說精簡的程序吧。
這個程序所做的事情,僅僅是多次調用io_out8。函數io_out8是干什么的呢?以后在naskfunc.nas中還要詳細說明,現在大家只要知道它是往指定裝置里傳送數據的函數就行了。
■■■■■
我們前面已經說過,CPU的管腳與內存相連。如果僅僅是與內存相連,CPU就只能完成計算和存儲的功能。但實際上,CPU還要對鍵盤的輸入有響應,要通過網卡從網絡取得信息,通過聲卡發送音樂數據,向軟盤寫入信息等。這些都是設備(device),它們當然也都要連接到CPU上。
既然CPU與設備相連,那么就有向這些設備發送電信號,或者從這些設備取得信息的指令。向設備發送電信號的是OUT指令;從設備取得電氣信號的是IN指令。正如為了區別不同的內存要使用內存地址一樣,在OUT指令和IN指令中,為了區別不同的設備,也要使用設備號碼。設備號碼在英文中稱為port(端口)。port原意為“港口”,這里形象地將CPU與各個設備交換電信號的行為比作了船舶的出港和進港。
所以,我們執行OUT指令時,出港信號就要揮淚告別CPU了。這就好像它在說:“媽媽,我要走了。我在顯卡中,會很好的,不用擔心。”我想不用說大家也會感覺得到,在C語言中,沒有與IN或OUT指令相當的語句,所以我們只好拿匯編語言來做了。唉,匯編真是關鍵時刻顯身手的語言呀。
■■■■■
如果我們讀一讀程序的話,就會發現突然蹦出了0x03c8、0x03c9之類的設備號碼,這些設備號碼到底是如何獲得的呢?隨意寫幾個數字行不行呢?這些號碼當然不是能隨便亂寫的。否則,別的什么設備胡亂動作一下,會帶來很嚴重的問題。所以事先必須仔細調查。筆者自己制作了參考網頁。
網頁的敘述太長了,不好意思(注:這一頁也是筆者寫的)。網頁中有一個項目,叫做“video DA converter”,其中有以下記述。
? 調色板的訪問步驟。
? 首先在一連串的訪問中屏蔽中斷(比如CLI)。
? 將想要設定的調色板號碼寫入0x03c8,緊接著,按R, G, B的順序寫入0x03c9。如果還想繼續設定下一個調色板,則省略調色板號碼,再按照RGB的順序寫入0x03c9就行了。
? 如果想要讀出當前調色板的狀態,首先要將調色板的號碼寫入0x03c7,再從0x03c9讀取3次。讀出的順序就是R, G, B。如果要繼續讀出下一個調色板,同樣也是省略調色板號碼的設定,按RGB的順序讀出。
? 如果最初執行了CLI,那么最后要執行STI。
我們的程序在很大程度上參考了以上內容。
■■■■■
到這里,該說明的部分都說明得差不多了。總結一下就是:
void set_palette(int start, int end, unsigned char *rgb) { int i, eflags; eflags = io_load_eflags(); /* 記錄中斷許可標志的值 */ io_cli(); /* 將許可標志置為0,禁止中斷 */ 已經說明的部分 io_store_eflags(eflags); /* 恢復許可標志的值 */ return; }
在“調色板的訪問步驟”的記述中,還寫著CLI、STI什么的。下面來看看它們可以做些什么。
首先是CLI和STI。所謂CLI,是將中斷標志(interrupt flag)置為0的指令(clear interrupt flag)。STI是要將這個中斷標志置為1的指令(set interrupt flag)。而標志,是指像以前曾出現過的進位標志一樣的各種標志,也就是說在CPU中有多種多樣的標志。更改中斷標志有什么好處呢?正如其名所示,它與CPU的中斷處理有關系。當CPU遇到中斷請求時,是立即處理中斷請求(中斷標志為1),還是忽略中斷請求(中斷標志為0),就由這個中斷標志位來設定。
那到底什么是中斷呢?大家可能會有這種疑問,可如果現在來講這個問題的話,就與我們“描繪一個操作系統模樣的畫面”這個主題漸行漸遠了,所以等以后有機會再講吧。
■■■■■
下面再來介紹一下EFLAGS這一特別的寄存器。這是由名為FLAGS的16位寄存器擴展而來的32位寄存器。FLAGS是存儲進位標志和中斷標志等標志的寄存器。進位標志可以通過JC或JNC等跳轉指令來簡單地判斷到底是0還是1。但對于中斷標志,沒有類似的JI或JNI命令,所以只能讀入EFLAGS,再檢查第9位是0還是1。順便說一下,進位標志是EFLAGS的第0位。

空白位沒有特殊意義(或許留給將來的CPU用?)
set_palette中想要做的事情是在設定調色板之前首先執行CLI,但處理結束以后一定要恢復中斷標志,因此需要記住最開始的中斷標志是什么。所以我們制作了一個函數io_load_eflags,讀取最初的eflags值。處理結束以后,可以先看看eflags的內容,再決定是否執行STI,但仔細想一想,也沒必要搞得那么復雜,干脆將eflags的值代入EFLAGS,中斷標志位就恢復為原來的值了。函數io_store_eflags就是完成這個處理的。
估計不說大家也知道了,CLI也好,STI也好,EFLAGS的讀取也好,EFLAGS的寫入也好,都不能用C語言來完成。所以我們就努力一下,用匯編語言來寫吧。
■■■■■
我們已經解釋了bootpack.c程序,那么現在就來說說naskfunc.nas。
; naskfunc ; TAB=4 [FORMAT "WCOFF"] ; 制作目標文件的模式 [INSTRSET "i486p"] ; 使用到486為止的指令 [BITS 32] ; 制作32位模式用的機器語言 [FILE "naskfunc.nas"] ; 源程序文件名 GLOBAL _io_hlt, _io_cli, _io_sti, io_stihlt GLOBAL _io_in8, _io_in16, _io_in32 GLOBAL _io_out8, _io_out16, _io_out32 GLOBAL _io_load_eflags, _io_store_eflags [SECTION .text] _io_hlt: ; void io_hlt(void); HLT RET _io_cli: ; void io_cli(void); CLI RET _io_sti: ; void io_sti(void); STI RET _io_stihlt: ; void io_stihlt(void); STI HLT RET _io_in8: ; int io_in8(int port); MOV EDX, [ESP+4] ; port MOV EAX,0 IN AL, DX RET _io_in16: ; int io_in16(int port); MOV EDX, [ESP+4] ; port MOV EAX,0 IN AX, DX RET _io_in32: ; int io_in32(int port); MOV EDX, [ESP+4] ; port IN EAX, DX RET _io_out8: ; void io_out8(int port, int data); MOV EDX, [ESP+4] ; port MOV AL, [ESP+8] ; data OUT DX, AL RET _io_out16: ; void io_out16(int port, int data); MOV EDX, [ESP+4] ; port MOV EAX, [ESP+8] ; data OUT DX, AX RET _io_out32: ; void io_out32(int port, int data); MOV EDX, [ESP+4] ; port MOV EAX, [ESP+8] ; data OUT DX, EAX RET _io_load_eflags: ; int io_load_eflags(void); PUSHFD ; 指PUSH EFLAGS POP EAX RET _io_store_eflags: ; void io_store_eflags(int eflags); MOV EAX, [ESP+4] PUSH EAX POPFD ; 指POP EFLAGS RET
到現在為止的說明,想必大家都已經懂了,尚且需要說明的只有與EFLAGS相關的部分了。如果有“MOV EAX, EFLAGS”之類的指令就簡單了,但CPU沒有這種指令。能夠用來讀寫EFLAGS的,只有PUSHFD和POPFD指令。
■■■■■
PUSHFD是“push flags double-word”的縮寫,意思是將標志位的值按雙字長壓入棧。其實它所做的,無非就是“PUSH EFLAGS”。POPFD是“pop flags double-word”的縮寫,意思是按雙字長將標志位從棧彈出。它所做的,就是“POP EFLAGS”。
棧是數據結構的一種,大家暫時只要理解到這個程度就夠了。往棧登錄數據的動作稱為push(推),請想象一下往烤箱里放面包的情景。從棧里取出數據的動作稱為pop(彈出)。
也就是說,“PUSHFD POP EAX”,是指首先將EFLAGS壓入棧,再將彈出的值代入EAX。所以說它代替了“MOV EAX, EFLAGS”。另一方面,PUSH EAX POPFD正與此相反,它相當于“MOV EFLAGS, EAX”。

■■■■■
最后要講的是io_load_eflags。它對我們而言,是第一個有返回值的函數的例子,但根據C語言的規約,執行RET語句時,EAX中的值就被看作是函數的返回值,所以這樣就可以。
另外,雖然還有幾個函數是不必要的,但因為將來會用到,所以這里就順便做了。雖然不知道什么時候用,用于什么目的,但通過到目前為止的講解也能明白其中的意義。
好了,講解完了以后執行一下吧。運行“make run”。條紋的圖案沒有變化,但顏色變了!成功了!

- Linux運維實戰:CentOS7.6操作系統從入門到精通
- Learning Windows Server Containers
- Kali Linux滲透測試全流程詳解
- 開源安全運維平臺OSSIM疑難解析:入門篇
- Java EE 8 Design Patterns and Best Practices
- Linux自動化運維:Shell與Ansible(微課版)
- Kali Linux 2018:Windows Penetration Testing
- Linux設備驅動開發
- Windows Vista終極技巧金典
- Advanced Infrastructure Penetration Testing
- Learning BeagleBone
- Linux軟件管理平臺設計與實現
- Linux指令從初學到精通
- 樹莓派+傳感器:創建智能交互項目的實用方法、工具及最佳實踐
- 計算機操作系統(第3版)(微課版)