官术网_书友最值得收藏!

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”。條紋的圖案沒有變化,但顏色變了!成功了!

主站蜘蛛池模板: 准格尔旗| 磴口县| 南投市| 秦皇岛市| 横山县| 东乡族自治县| 双鸭山市| 桐梓县| 旌德县| 驻马店市| 藁城市| 甘南县| 绍兴市| 乐山市| 武宣县| 和硕县| 天津市| 葵青区| 石景山区| 五华县| 苏州市| 卓尼县| 通州市| 洛宁县| 白沙| 宁乡县| 应用必备| 开封县| 班戈县| 平顺县| 信阳市| 墨竹工卡县| 宣化县| 右玉县| 安康市| 永善县| 澜沧| 广饶县| 西畴县| 中超| 西吉县|