《30天自制 自製操作系統》第4天

2020-08-14 23:07:50

第4天 C語言與畫面顯示的練習

1.用C語言實現記憶體寫入

現在想要在螢幕上顯示點什麼東西,需要向VRAM中數據,雖然C語言可以寫記憶體,但我們還是先用匯編來完成這個工作吧。現在在naskfunc.c中新增點東西。

_write_mem8:    ; void write_mem8(int addr, int data);
    MOV ECX, [ESP + 4]      ; [ESP + 4]中存放的是地址,蔣其讀入ECX
    MOV AL, [ESP + 8]       ; [ESP + 8]中存放的是數據,將其讀入AL
    MOV [ECX], AL
    RET

在第二天的部落格中有一張CPU記憶體部分暫存器的表,SP表示16位元的棧指針暫存器,而此處ESP表示32位元的棧指針暫存器。函數的參數會按地址從低到高的順序存放在棧中,可以通過ESP取出參數。

這段程式碼的含義就是把data數據存入記憶體的addr地址中。另外,要與C語言聯合使用的話,能自由使用的暫存器就只有EAX、ECX和EDX,因此不能隨意更改程式碼中的暫存器。

naskfunc.c中還新增了一個INSTRSET指令,由於書中有介紹就不多說了。

彙編部分完成了,就開始寫C語言程式碼了。

void io_hlt(void);
void write_mem8(int addr, int data);

void HariMain(void)
{
    int i;

    for (i = 0xa0000; i <= 0xaffff; i++)
    {
        write_mem8(i, 15);  /* MOV BYTE [i], 15 */
    }

    while (1)
    {
        io_hlt();
    }
}

不會C語言語法的讀者就先去學學C語言吧,相信我,那會事半功倍的。

這段程式碼是將地址0xa0000~0xaffff的記憶體全部寫入15,然後一直死回圈。

來執行看看效果吧。

一片白色,正是我們想要的效果。


2.條紋圖案

改成條紋圖案的操作倒是很簡單,只需要改一行程式碼:

for (i = 0xa0000; i <= 0xaffff; i++)
{
    write_mem8(i, i & 0x0f);  /* MOV BYTE [i], 15 */
}

這裏用到了與運算,如之前所說,不介紹C語言的基礎語法,相信大家也能看懂。

這段程式碼就是把調色板的16種顏色全部顯示出來,並循環往復,我們設定的螢幕寬度是320畫素,是16的整數倍,所以相同的顏色會在同一列出現,形成條紋。

最後的執行效果如下:

這圖看久了眼睛都覺得有點花。


3.挑戰指針

指針作爲C語言的核心,自然是重中之重。什麼?你沒學過指針?那你學的什麼C語言。什麼?感覺指針太難了,學不會?那我只能說C語言不適合你,寫操作系統也不適合你,去學Java、Python或Php吧。

來看看使用指針後的程式碼吧。

int i;      /* int爲32位元 */
char *p;    /* BYTE型地址 */

for (i = 0xa0000; i <= 0xaffff; i++)
{
	p = (char*) i;
	*p = i & 0x0f;  /* 代替了write_mem8(i, i & 0x0f); */
}

p = (char*) i;這句程式碼中對i進行了型別轉換,雖然在這裏字元指針和整型數據都佔4位元組,但編譯器它不聽啊,你不按規矩來,他就報警告,雖然警告沒什麼影響,但眼不見心不煩嘛,0 error 0 warning聽起來多舒服。

執行一下看結果。


4.指針的應用(1)

對程式碼做點小小的修改:

p = (char *) 0xa0000;   /* 給地址變數賦值 */

for (i = 0; i <= 0xffff; i++)
{
	*(p + i) = i & 0x0f;
}

圖我就不放了,反正效果看上面的就行了。


5.指針的應用(2)

對程式碼做點小小的修改:

p = (char *) 0xa0000;   /* 給地址變數賦值 */

for (i = 0; i <= 0xffff; i++)
{
	p[i] = i & 0x0f;
}

這和陣列操作差不多。

圖也不放了,偷懶。


6.色號設定

既然都能顯示顏色了,那自定義要顯示的顏色?當然可以!但是我們只能設定最多256種顏色,至於爲什麼只能設定256位,還記得我們在asmhead.nas中設定的8位元彩色模式吧。這個8位元彩色模式,是由程式設計師隨意指定0~255的數位所對應的顏色的。1個數字對應一種顏色,這種方式被稱爲調色板。

來看看程式碼吧。

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);
}

這個函數的作用就是定義顏色數據,然後將數據傳入下一個函數中進行處理。

這裏我們得先講講一些彙編程式碼,因爲下面 下麪的C語言程式需要用到彙編語言編寫的函數。作者搞事啊,一下子蹦出這麼多函數,我就挑幾個來講講好了。

_io_in8:    ; int io_in8(int port)
    MOV EDX, [ESP + 4]      ; port
    MOV EAX, 0
    IN  AL, 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_in8用於從埠讀入一個位元組,_io_out8用於向埠輸出一個位元組。其中,IN和OUT都是彙編中的I/O操作指令,IN用於從埠讀取數據,OUT用於向埠寫入數據。

如:IN AL, 21H表示從21H埠讀取一個位元組數據到AL。

IN AX, 21H表示從21H埠讀取兩個位元組數據到AX(21H埠的數據放入AL,22埠的數據放入AH)

OUT 21H, AL表示將AL的值寫入21H埠

OUT 21H, AX表示將AX的值寫入埠地址21H開始的兩個位元組(AL寫入21H埠,AH寫入22H埠)

注:當I/O地址大於FFH時,地址需放入DX中,而且數據傳輸只能通過使用EAX,AX或AL。

_io_cli:    ; void io_cli(void);
    CLI
    RET

_io_sti:    ; void io_sti(void);
    STI
    RET

CLI指令用於禁止中斷髮生,STI指令用於允許中斷髮生。所以這兩函數的功能也顯而易見。

_io_load_eflags:    ; int io_load_eflags(void);
    PUSHFD          ; 將標誌暫存器數據壓入棧中
    POP EAX
    RET

_io_store_eflags:   ; void io_store_eflags(int eflags);
    MOV EAX, [ESP + 4]
    PUSH    EAX
    POPFD           ; 將數據寫入標誌暫存器中
    RET

這兩函數的主要內容都已經在註釋寫好了,而且關於棧操作書上也寫得很清楚,我就不多說了。接下來繼續介紹C語言檔案的函數。

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);    /* 復原中斷許可標誌 */
}

首先用io_load_eflags儲存狀態暫存器的值,然後用io_cli將中斷標誌位置0(中斷標誌位在狀態暫存器上),禁止中斷,最後復原狀態暫存器的值,被修改的中斷標誌就會被修改回來。

程式碼中分別對0x3C8和0x3C9埠進行了操作,這兩個埠的操作步驟如下:

  1. 遮蔽中斷
  2. 將想要設定的調色板號碼寫入0x3C8,緊接着,按R、G、B的順序寫入0x3C9。如果還想繼續設定下一個調色板,則省略調色板號碼,再按照RGB的順序寫入0x3C9就行了。
  3. 恢復中斷

程式的結果就是把調色板的數位0對應黑色,1對應紅色,……,15對應暗灰。其中有個奇怪的地方就是,io_out8(0x03c9, rgb[0] / 4);這段程式碼中,當rgb[0]=0xff時,程式碼中爲rgb[0]或rgb[0] / 4的效果是一樣的;但當rgb[0]=0x84時,效果也不一樣了。而且,當rgb[0]=0xff時,程式碼爲io_out8(0x03c9, rgb[0] / 4);和程式碼爲io_out8(0x03c9, 0xcf);的效果不一樣,也不知道是爲什麼。如果有人知道請務必在下方評論告訴我。

主程式並沒有什麼變化。讓我們看看修改後的效果。

可以看到從左到右依次是黑、紅、綠、黃……,和我們設定的一樣。


7.繪製矩陣

繪製矩陣是完成靠C語言程式邏輯實現的,而且並沒有什麼難度,相信讀者花一點時間也能琢磨出一個實現繪製矩陣的演算法。

void boxfill8(unsigned char *vram, int xsize, unsigned char c, int x0, int y0, int x1, int y1)
{
    int x, y;
    for (y = y0; y <= y1; y++)
    {
        for (x = x0; x <= x1; x++)
        {
            vram[y * xsize + x] = c;
        }
    }
}

如下圖所示:

首先從(x0, y0)點開始填充畫素點,直到(x1, y0),然後換行繼續填充,直到(x1, y1)。很簡單的程式邏輯。

說來慚愧,之前竟然把調色板的工作方式給忘了,所以面對COL8_000000這個定義是數位時還想了一會兒是怎麼回事。因爲調色板的工作方式,一個數字代表一個顏色,只要往VRAM中寫入數位,螢幕上會顯示數位相應的顏色。

主函數中新增了繪製矩陣的程式碼,沒什麼好講的,就直接看效果吧。

也不知道五彩斑斕的黑的顏色怎麼定義,我想畫出來。


8.今天的成果

大家期待的介面它來了!

先看看程式碼。

void HariMain(void)
{
    char *vram;    /* BYTE型地址 */
    int xsize, ysize;

    init_palette();         /* 設定調色板 */

    vram = (char *) 0xa0000;   /* 給地址變數賦值 */
    xsize = 320;
    ysize = 200;

    boxfill8(vram, xsize, COL8_008484, 0,          0,          xsize - 1,  ysize - 29);
    boxfill8(vram, xsize, COL8_C6C6C6, 0,          ysize - 28, xsize - 1,  ysize - 28);
    boxfill8(vram, xsize, COL8_FFFFFF, 0,          ysize - 27, xsize - 1,  ysize - 27);
    boxfill8(vram, xsize, COL8_C6C6C6, 0,          ysize - 26, xsize - 1,  ysize - 1);

    boxfill8(vram, xsize, COL8_FFFFFF, 3,          ysize - 24, 59,         ysize - 24);
    boxfill8(vram, xsize, COL8_FFFFFF, 2,          ysize - 24, 2,          ysize - 4);
    boxfill8(vram, xsize, COL8_848484, 3,          ysize - 4,  59,         ysize - 4);
    boxfill8(vram, xsize, COL8_848484, 59,         ysize - 23, 59,         ysize - 5);
    boxfill8(vram, xsize, COL8_000000, 2,          ysize - 3,  59,         ysize - 3);
    boxfill8(vram, xsize, COL8_000000, 60,         ysize - 24, 60,         ysize - 3);

    boxfill8(vram, xsize, COL8_848484, xsize - 47, ysize - 24, xsize - 4,  ysize - 24);
    boxfill8(vram, xsize, COL8_848484, xsize - 47, ysize - 23, xsize - 47, ysize - 4);
    boxfill8(vram, xsize, COL8_FFFFFF, xsize - 47, ysize - 3,  xsize - 4,  ysize - 3);
    boxfill8(vram, xsize, COL8_FFFFFF, xsize - 3,  ysize - 24, xsize - 3,  ysize - 3);

    while (1)
    {
        io_hlt();
    }
}

老實說,程式碼沒什麼好講的。我就是來水字數的。下面 下麪是介面效果。

這介面應該能和85年的Windows介面有的一拼。


今天的內容又結束了。本以爲出完第3天的部落格之後要等半個月才能 纔能寫出這篇來,結果只花了3天時間。其中,2天學第7章的內容,1天時間寫這篇部落格。。。雖然部落格簡陋,內容也不多,也是花了我3小時才寫出來的。願與你繼續看第5天的部落格,雖然也沒寫,先留個坑再說,什麼時候學完了第8章什麼時候寫。我寫這麼多幹什麼,又沒人看。