利用C語言繪製作業系統影象介面
有了C語言這一利器後,不多多拿來用,那就太對不起前面的一系列努力了。那麼怎麼表現C語言的強大功能呢,如果還只是一味的在介面上輸出幾行字元,那太沒意思,考慮到,我們的目標是做出像windows那樣具備舒心的影象使用者介面那樣的系統,所以在這一節,我們由字元模式切換入畫面模式,初步體驗下,那些絢麗多彩的影象介面是如何發展而成的。
要想由字元模式轉入圖形模式,我們需要操作硬體,特別是向顯示卡傳送命令,讓其進入圖形顯示模式,就如同前面我們所做的,要操作硬體,一般需要使用BIOS呼叫,以下幾行就是開啟VGA顯示卡色彩功能的程式碼:
mov al, 0x13h
mov ah, 0x00
int 0x10
其中al 的值決定了要設定顯示卡的色彩模式,下面是一些常用的模式設定:
1. 0x03, 16色字元模式
2. 0x12, VGA圖形模式, 640 * 480 * 4位彩色模式,獨特的4面儲存模式
3. 0x13, VGA圖形模式, 320 * 200 * 8位彩色模式,調色盤模式
4. 0x6a, 擴充套件VGA圖形模式, 800 * 600 * 4彩色模式
我們採用的是0x13模式,其中320*200*8 中,最後的數值8表示的是色彩值得位數,也就是我們可以用8位數值表示色彩,總共可以顯示256種色彩。
系統視訊記憶體的地址是0x000a0000,當我們執行上面幾句程式碼後,望視訊記憶體地址寫入資料,那麼螢幕就會出現相應的變化了。
我們先看看核心的彙編程式碼部分(kernel.asm):
%include "pm.inc"
org 0x9000
jmp LABEL_BEGIN
[SECTION .gdt]
; 段基址 段界限 屬性
LABEL_GDT: Descriptor 0, 0, 0
LABEL_DESC_CODE32: Descriptor 0, SegCode32Len - 1 , DA_C + DA_32
LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW
LABEL_DESC_VRAM: Descriptor 0, 0ffffffffh, DA_DRW
LABEL_DESC_STACK: Descriptor 0, TopOfStack, DA_DRWA+DA_32
GdtLen equ $ - LABEL_GDT
GdtPtr dw GdtLen - 1
dd 0
SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT
SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT
SelectorStack equ LABEL_DESC_STACK - LABEL_GDT
SelectorVram equ LABEL_DESC_VRAM - LABEL_GDT
[SECTION .s16]
[BITS 16]
LABEL_BEGIN:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0100h
mov al, 0x13
mov ah, 0
int 0x10
xor eax, eax
mov ax, cs
shl eax, 4
add eax, LABEL_SEG_CODE32
mov word [LABEL_DESC_CODE32 + 2], ax
shr eax, 16
mov byte [LABEL_DESC_CODE32 + 4], al
mov byte [LABEL_DESC_CODE32 + 7], ah
;set stack for C language
xor eax, eax
mov ax, cs
shl eax, 4
add eax, LABEL_STACK
mov word [LABEL_DESC_STACK + 2], ax
shr eax, 16
mov byte [LABEL_DESC_STACK + 4], al
mov byte [LABEL_DESC_STACK + 7], ah
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_GDT
mov dword [GdtPtr + 2], eax
lgdt [GdtPtr]
cli ;關中斷
in al, 92h
or al, 00000010b
out 92h, al
mov eax, cr0
or eax , 1
mov cr0, eax
jmp dword SelectorCode32: 0
[SECTION .s32]
[BITS 32]
LABEL_SEG_CODE32:
;initialize stack for c code
mov ax, SelectorStack
mov ss, ax
mov esp, TopOfStack
mov ax, SelectorVram
mov ds, ax
C_CODE_ENTRY:
%include "write_vga.asm"
io_hlt: ;void io_hlt(void);
HLT
RET
SegCode32Len equ $ - LABEL_SEG_CODE32
[SECTION .gs]
ALIGN 32
[BITS 32]
LABEL_STACK:
times 512 db 0
TopOfStack equ $ - LABEL_STACK
解釋下上面程式碼,我們設定了一個描述符,LABEL_DESC_VRAM, 這個描述符對應的記憶體起始地址是0,長度是0xffffffff,也就是我們把整個4G記憶體當做一段可讀可寫的記憶體,有了這個設定後,我們在C語言裡就可以隨意讀寫記憶體的任何地方。
LABEL_DESC_STACK 這個描述符用來設定一段可讀可寫的記憶體,它的起始地址是LABEL_STACK, 可以看到,程式通過語句:times 512 db 0
初始化了512位元組的記憶體。C語言的執行,特別是函式呼叫時,是需要一個堆疊來傳遞引數的,所以,要執行C語言,我們首先需要為其配置一個堆疊,該描述符所對應的這512自己記憶體就是給C語言使用的,由於堆疊只有512位元組,在後面我們使用C語言寫的程式碼中,函式的區域性變數大小不能超過512位元組,例如下面的程式碼可能就要出錯了:
void fun() {
char buf[513];
}
語句%include write_vga.asm”, 表明,我們要開發的C程式碼檔案叫write_vga.c, 我們寫完C程式碼後,會使用上一節的步驟將它編譯成彙編,然後include到我們當前的彙編檔案裡,統一編譯成可執行核心。
最後一小塊程式碼:
io_hlt: ;void io_hlt(void);
HLT
RET
作用是進入死迴圈,HLT指令會讓系統進入休眠狀態。
匯入C語言
硬體,堆疊等基層設施通過彙編準備就緒後,我們可以使用C語言開發圖形功能了。顯示器的每一個畫素對應一個點,一個點可以顯示256種不同的顏色,因此,只要我們給每個點設定成相應的顏色,那麼最終就可以繪製出特定的影象。
我們看看如何用C語言寫入視訊記憶體從而操作螢幕影象,write_ram.c:
void CMain(void) {
int i;
char*p = 0;
for (i = 0xa0000; i <= 0xaffff; i++) {
p = i;
*p = i & 0x0f;
}
for(;;) {
io_hlt();
}
}
程式碼中,我們將指標P指向地址0xa0000, 這個地址正好就是vga視訊記憶體地址,vga視訊記憶體地址從0xa0000開始,直到0xaffff結束,總共64k.接著語句:
*p = i & 0x0f 將一個數值寫入視訊記憶體,這個值可以是0-256中任意一個數值,我們程式碼裡是將i的最後4位作為畫素顏色寫入視訊記憶體,這個值是任意的,大家可以隨意設定。
在Ubuntu中寫出上面程式碼後,通過命令編譯成二進位制檔案:
gcc -m32 -fno-asynchronous-unwind-tables -s -c -o write_vga.asm write_vga.c
於是在目錄下會生成write_vga.o二進位制檔案,接著使用objconv進行反彙編:
./objconv -fnasm write_vga.asm write_vga.o
反彙編後代碼如下:
; Disassembly of file: write_vga.o
; Tue Sep 13 10:30:14 2016
; Mode: 32 bits
; Syntax: YASM/NASM
; Instruction set: 80386
global CMain: function
extern io_hlt ; near
SECTION .text align=1 execute ; section number 1, code
CMain: ; Function begin
push ebp ; 0000 _ 55
mov ebp, esp ; 0001 _ 89. E5
sub esp, 24 ; 0003 _ 83. EC, 18
mov dword [ebp-0CH], 0 ; 0006 _ C7. 45, F4, 00000000
mov dword [ebp-10H], 655360 ; 000D _ C7. 45, F0, 000A0000
jmp ?_002 ; 0014 _ EB, 17
?_001: mov eax, dword [ebp-10H] ; 0016 _ 8B. 45, F0
mov dword [ebp-0CH], eax ; 0019 _ 89. 45, F4
mov eax, dword [ebp-10H] ; 001C _ 8B. 45, F0
and eax, 0FH ; 001F _ 83. E0, 0F
mov edx, eax ; 0022 _ 89. C2
mov eax, dword [ebp-0CH] ; 0024 _ 8B. 45, F4
mov byte [eax], dl ; 0027 _ 88. 10
add dword [ebp-10H], 1 ; 0029 _ 83. 45, F0, 01
?_002: cmp dword [ebp-10H], 720895 ; 002D _ 81. 7D, F0, 000AFFFF
jle ?_001 ; 0034 _ 7E, E0
?_003: call io_hlt ; 0036 _ E8, FFFFFFFC(rel)
jmp ?_003 ; 003B _ EB, F9
; CMain End of function
SECTION .data align=1 noexecute ; section number 2, data
SECTION .bss align=1 noexecute ; section number 3, bss
在上面程式碼中去掉以section 開始的指令,這些指令會影響我們把當前彙編結合入核心kerne.asm.
同時去掉開頭的兩句:
global CMain: function
extern io_hlt
因為我們要把兩個彙編檔案結合成一個,所以這兩句宣告是多餘的。做完這些後,再用nasm編譯kernel.asm:
nasm -o kernel.bat kernel.asm
於是本地目錄下,核心檔案就編譯好了。
接著執行java工程,生成虛擬軟盤,執行結果如下:
大家注意看,kernel.bat寫入了兩個扇區,也就是說,我們核心的大小已經超過了512位元組。此時我們需要修改一下核心載入器,讓核心載入器一次讀入兩個扇區才能把核心完全載入入記憶體,開啟boot.asm,將readFloppy中的:
mov ah, 0x02
mov al, 1
改成:
mov al, 2
也就是一次讀取兩個扇區的內容,修改後再次編譯boot.asm:
nasm -o boot.bat boot.asm
最後再次執行java程式,此時生成的虛擬軟盤中,才會包含完整的核心檔案。啟動虛擬機器,載入虛擬軟盤後,執行情況如下:
大家可以看到,螢幕顯示出了條紋狀影象。