具體代碼調試和講解請參看視頻:
Linux kernel Hacker, 從零構建自己的內核
一直以來,我們的操作系統加載器,秉承簡單夠用的原則,只要能把編譯好的二進制內核送進內存就可以了,所以加載器的算法是,連續讀取軟盤扇區,將扇區的內容寫入到從0x8000 開始的內存中。以下是我們內核加載器的代碼:
org 0x7c00;
LoadAddr EQU 08000h
entry:
mov ax, 0
mov ss, ax
mov ds, ax
mov es, ax
mov BX, LoadAddr ; ES:BX 數據存儲緩沖區
mov CH, 1 ;CH 用來存儲柱面號
mov DH, 0 ;DH 用來存儲磁頭號
readFloppy:
cmp byte [load_count], 0
je beginLoad
mov CL, 1 ;CL 用來存儲扇區號
mov AH, 0x02 ; AH = 02 表示要做的是讀盤操作
mov AL, 18 ; AL 表示要練習讀取幾個扇區
mov DL, 0 ;驅動器編號,一般我們只有一個軟盤驅動器,所以寫死
;為0
INT 0x13 ;調用BIOS中斷實現磁盤讀取功能
inc CH
dec byte [load_count]
JC fin
add bx, 512 * 18
jmp readFloppy
beginLoad:
jmp LoadAddr
load_count db 3 ;連續讀取幾個柱面
fin:
HLT
jmp fin
load_count 指的是要讀取的軟盤柱面數。一個1.44M軟盤,其中一個磁面有80個柱面,一個柱面有有兩面,上面和背面,每面對應一個磁道,一個磁道有18個扇區,一個扇區有512字節。上面代碼中,load_cout 的值設置為3, 也就是程序要連續讀取3個柱面,也就是要將軟盤中大約 3* 18 * 512 字節,也就是27k的內容寫入地址為08000h的內存中。
隨著我們開發的操作系統功能越來越強大,其代碼量也越來越大,現在內核編譯后,已經接近15k了,超過27k是遲早的事情,一旦超過27k,那么我們的軟盤在往內存拷貝內核時,就需要連續將4個柱面,也就是72扇區的數據寫入到內存中。按照設想,我們只要把上面load_cout的值改成4就可以了。
然而一旦改成4,問題就出現了,因為讀取4個柱面,也就要連續向內存讀入18*4 = 72個扇區的內容,現在大多數BIOS提供的int 013h軟盤讀取中斷功能,一旦發現調用代碼要讀取的內容有72扇區以上,它就會返回失敗,如果有同學嘗試著把上面代碼的load_cout該成4,然后再運行程序,就會發現內核加載失敗,也就是int 03h 的中斷調用返回失敗,這樣一來,一旦我們的內核大小超過27k, 的話,我們現在的加載器就無法正確加載了。
如果你使用的是虛擬機Bochs 來運行上面的加載器代碼,那么連續讀取軟盤超過72扇區時,Bochs提供的Bios調用會返回失敗。我們看看Bochs源碼中有個Bios功能的函數如下(rombios.c):
void
7273 int13_diskette_function(DS, ES, DI, SI, BP, ELDX, BX, DX, CX, AX, IP, CS, FLAGS)
7274 Bit16u DS, ES, DI, SI, BP, ELDX, BX, DX, CX, AX, IP, CS, FLAGS;
7275 {
......
7337 if ((drive > 1) || (head > 1) || (sector == 0) ||
7338 (num_sectors == 0) || (num_sectors > 72)) {
7339 BX_INFO("int13_diskette: read/write/verify: parameter out of range\n");
7340 SET_AH(1);
7341 set_diskette_ret_status(1);
7342 SET_AL(0); // no sectors read
7343 SET_CF(); // error occurred
7344 return;
7345 }
....
}
這個函數模擬的就是軟盤讀取BIOS int 013h中斷功能,當我們的代碼連續讀取幾個柱面的扇區時,我懷疑Bochs會把這些要讀的扇區請求積累起來,然后把要讀取的扇區一次性進行寫入,而不是請求一次就執行一次讀取動作,因此代碼中的條件判斷num_sectors > 72 就會成立,于是連續讀取超過4個柱面也就是72扇區,Bochs模擬器就會返回失敗。由于當前很多虛擬機都大量使用Bochs的源代碼,或是實現機制類似,我在mac上用的是parallels ,它的反應跟Bochs一樣,也是連續讀取軟盤超過72扇區時,返回了錯誤,因此在前面的加載器代碼中,一旦連續讀取4個柱面以上時,讀取請求就會返回失敗。
為了繞過這個限制,現在我們加載器的做法是,不再一次連續讀取18個扇區,而是一次讀取一個扇區,把這個扇區的數據先讀入一個給定的,大小為512字節的緩沖區內,然后再把該緩沖區的內容,拷貝到指定的內存中,也就是我們要多做一次沒有意義的拷貝工作。
由于我們的內核要加載到內存08000h, 因此,我將08000h前512字節,也就是起始地址為07E00h開始的512字節內存作為軟盤一個扇區數據的緩沖區,每次從軟盤讀入一個扇區數據時,先把數據寫入到這個緩沖區,然后再把這個緩沖區的數據拷貝到08000h之后的地址,代碼如下:
org 0x7c00;
LoadAddr EQU 08000h
BufferAddr EQU 7E0h
BaseOfStack equ 07c00h
entry:
mov ax, 0
mov ss, ax
mov ds, ax
mov ax, BufferAddr
mov es, ax
mov ax, 0
mov ss, ax
mov sp, BaseOfStack
mov di, ax
mov si, ax
mov BX, 0 ; ES:BX 數據存儲緩沖區
mov CH, 1 ;CH 用來存儲柱面號
mov DH, 0 ;DH 用來存儲磁頭號
mov CL, 0 ;CL 用來存儲扇區號
;每次都把扇區寫入地址 07E00處
readFloppy:
cmp byte [load_count], 0
je beginLoad
mov bx, 0
inc cl
mov AH, 0x02 ; AH = 02 表示要做的是讀盤操作
mov AL, 1 ; AL 表示要讀取幾個扇區
mov DL, 0 ;驅動器編號,一般我們只有一個軟盤驅動器,所以寫死
;為0
INT 0x13 ;調用BIOS中斷實現磁盤讀取功能
JC fin
;把剛寫入07E00的一個扇區的內容寫入從08000h開始的地址
copySector:
push si
push di
push cx
mov cx, 0200h ;緩沖區數據大小,也就是512字節
mov di, 0
mov si, 0
mov ax, word [load_section];es
mov ds, ax
copy:
cmp cx, 0
je copyend
mov al, byte [es:si] ;es:si指向07E00
mov byte [ds:di], al
inc di
inc si
dec cx
jmp copy
copyend:
pop cx
pop di
pop si
mov bx, ds
add bx, 020h
mov ax, 0
mov ds, ax
mov word [load_section], bx
mov bx, 0
;end of copySector
cmp cl, 18
jb readFloppy
inc CH
mov cl, 0
dec byte [load_count]
jmp readFloppy
beginLoad:
mov ax, 0
mov ds, ax
jmp LoadAddr
load_count db 10 ;連續讀取幾個柱面
load_section dw 0800h
fin:
HLT
jmp fin
這段代碼跟開頭的代碼,不同之處在于,第一段代碼,是連續從軟盤讀取扇區數據到指定的內存里,這么做可能存在一個隱性的難以發現的問題:
一是編譯器可能會對代碼進行優化,最終編譯出來的二進制代碼可能給匯編代碼的原意有所不同,編譯后的代碼可能會把所以讀請求積攢起來,然后一次發出讀取命令,由于代碼要讀取4個柱面,每次讀取18個扇區,最終編譯的代碼可能是把4個柱面,總共72個扇區積攢起來,然后一次讀取,這樣的話,就可能違反了Bochs虛擬機的讀取限制。
二是,在讀取數據時,只要讀取的數據不需要立刻使用的話,CPU可能會將數據讀取的請求積累起來,當有數據請求時,才把所有積攢起來的數據讀取請求一次發出,從而提高讀寫效率。
無論是那種情況,都有可能造成一次讀取超過72扇區的請求,從而被虛擬機拒絕。改動后的代碼是,當讀取一個扇區的數據后,程序立馬進行數據拷貝,這樣的話,上面提到的優化機制就不能產生作用,數據的讀取請求就不會被積攢起來,因此就不會遭遇Bochs虛擬機的讀取限制。
我們分析下第二段代碼,BufferAddr 指的是軟盤扇區的數據讀取后要寫入的緩沖區地址,注意它的值是0x7E0, 為什么是0x7E0,而不是0x7E00呢,這是因為這個值會直接付給段寄存器es, 在實模式下,尋址方式是 段地址:段偏移,轉換成直接地址就是 段地址*16 + 段偏移,由于0x7E0 * 16 = 0x7E00, 因此把0x7E00付給段寄存器,就需要把最后一個0去掉,也就是0x7E00 要除以16.
readFloppy 這段代碼通過int 013h調用,讓BIOS從軟盤中讀取一個扇區的數據,然后把數據寫入到起始地址為0x7E00的緩沖區,接著copySector這段代碼把剛寫入緩沖區的數據拷貝到內核的加載地址,也就是08000h之后,變量load_section 用來存儲的是內核加載的起始地址,由于這個值要付給段寄存器ds,所以它的實際值是0800h而不是08000h,每次往這個地址寫入512字節的數據后,下次寫入時,地址要往下偏移512字節,512除以16等于32,因此要把load_section的值加上32,也就是020h.
有了上面改動后,加載代碼能讀取任意多個扇區數據到內存而不用擔心Bochs虛擬機的限制,雖然我用的是Virtual Box, 但據說Virtual Box使用的也是Bochs代碼,所以當我連續加載扇區超過72時,Virtual Box對代碼的數據讀取請求也返回識別,使用上面的修改后,數據的讀取問題也能得到解決了。
Message Box的計時器效果
最后,我們實現一個計數器效果,在write_vga_desktop.c里,根據下面代碼進行更改:
void CMain(void) {
...
for(;;) {
char* pStr = intToHexStr(counter);
counter++;
boxfill8(shtMsgBox->buf, 160, COL8_C6C6C6, 40, 28, 119, 43);
showString(shtctl, shtMsgBox, 40, 28, COL8_000000,pStr);
io_cli();
if (fifo8_status(&keyinfo) + fifo8_status(&mouseinfo) == 0) {
io_sti();
}
...
}
我們在主循環里,讓一個變量從0開始自加,然后把其結果顯示在Message Box的主窗體里,如果大家按照上面代碼更改后,會發現界面有明顯的閃爍效果。這是我們當前圖層的刷新機制導致的,在后面課程中,我將與大家研究如何消除這種令人痛苦的閃爍現象。